\appendix \section{付録} \subsection{Deep Dive - 変数の舞台裏} \ref{var_decl_def}節にてプログラムが値の種類を判別するためにコンパイラが型情報に基づいて読み出すべきビット長を実行ファイルに書き込まれると書いたが、実際はどうであろう。 ここではコンパイラの生成物である実行ファイルをアセンブリに分解して検証していく。 まずは検証用のソースコードを用意する。今回は以下の物を用意した: \defaultlistingstyle \lstinputlisting[language=C,title={検証用コード}]{../programs/demo.c} なお、実行環境は\ref{exec_env}節のものとする。 このコードをコンパイルするにあたって、解読を容易にするために少しフラグを変更する必要がある。 Linuxでの\texttt{gcc}コンパイラは特に指定されていなければコンパイラの機能を使用するための\texttt{libgcc}と\texttt{libc}というC言語の標準的な関数(\texttt{printf}, etc.)などが宣言・定義されたライブラリをリンクします。 これによりソフトウェアの日常的な機能の再開発を避けれるが、人間が読めるアセンブリに直すとその部分のコードが析出してしまい、目的のコードが埋もれてしまう。 なので、これらライブラリの自動リンクをやめる必要がある。そこで今回は\texttt{-nostdlib}フラグと\texttt{-nolibc}フラグを追加する。\cite{gcc_man} 最近のコンパイラは非常に賢く、無意味なコードを取り除いて最適化しようとする。 だが今回の検証用コードはただ単に変数を宣言・定義するだけで、実際に使用されないのでそのままではコンパイラに無意味と見なされ、勝手に変数の宣言・定義のコードを削除してしまう。 なのでここにコンパイラによる最適化を無効にするために\texttt{-O0}フラグも追加する。\cite{gcc_man} 最終的なコンパイルコマンドはこのようになる: \begin{center} \begin{verbatim} gcc -Wall -nostdlib -nolibc -O0 demo.c -o demo \end{verbatim} \end{center} 上記のコマンドを実行するとリンカからエントリーポイント:プログラムの始まり、が定義されていないとエラーを吐くが解読に問題はないのでそのままでよい。 次に生成物である\texttt{demo}をデコンパイル(実行ファイルからアセンブリに戻す作業)を行うためにGNU Binutils\footnote{\url{https://www.gnu.org/software/binutils/}}の\texttt{objdump}を利用する。 \newpage 以下がデコンパイルコマンドとなる: \begin{center} \begin{verbatim} objdump -Mintel -d demo \end{verbatim} \end{center} このコマンドの\texttt{-Mintel}フラグはデコンパイル時にアセンブリを私が個人的に読みやすいIntel記法で表記すると\texttt{objdump}に命令できる。 上記のコマンドを実行すると次のような出力になる: \begin{center} \begin{verbatim} demo: ファイル形式 elf64-x86-64 セクション .text の逆アセンブル: 0000000000401000
: 401000: 55 push rbp 401001: 48 89 e5 mov rbp,rsp 401004: c7 45 fc ff 00 00 00 mov DWORD PTR [rbp-0x4],0xff 40100b: 90 nop 40100c: 5d pop rbp 40100d: c3 ret \end{verbatim} \end{center} これで\texttt{demo}のデコンパイル結果が出た。\footnote{コンパイル時に\texttt{-nostdlib -nolibc}を指定しないとデコンパイル結果が100行以上になる。} このアセンブリの口語訳は以下となる: \begin{itemize} \item \texttt{rbp}レジスタ(CPU内にあるごく小さな変数)の内容をスタック(メモリ上にある最初に入れたデータは最後に出る仕組みを持つデータ保持の構造)の最上部に積む。 \item \texttt{rsp}レジスタの内容を\texttt{rbp}レジスタに書き込む。 \item 16進数値\texttt{0xff}(10進数で255)を\texttt{rbp}レジスタの内容から\texttt{0x4}を引いたアドレス(メモリ上の場所を表す住所のような数値)が指している場所に書き込む。 \item なにもしない。 \item スタックの最上部にある値を取り、それを\texttt{rbp}レジスタに読み込む。 \item 呼び出し元の関数に制御を戻す。 \end{itemize} \texttt{rbp}レジスタは主に関数内でのみ有効なローカル変数を格納するレジスタである。 また、\texttt{rsp}レジスタはスタックポインタといい、現在実行されている関数に関するデータの読み書きに使用される。 今回の例では、まず\texttt{rbp}レジスタの内容をスタックに退避させ、現在のスタックポインタが指しているアドレスを\texttt{rbp}レジスタにコピーする。 次に、\texttt{rbp}が指しているアドレスから4つ分移動させる。 この時の4は単位が1バイトであるため$4*8=32$ビット分の空を確保している。 これはまさに宣言そのもので、\texttt{int}型は32ビットのサイズを持つのでちゃんと当てはまる。 更にこの空いた場所に\texttt{movl}命令を使用して16進数値\texttt{0xff}、10進数の255を移動させている。 これが定義であり、宣言した場所に値を代入するというC言語の\texttt{a = 255;}と一致する。 \texttt{x86\_64}の様なCISC(複雑命令セットコンピュータ)は宣言と定義を一つの命令で行っている。 \newpage では、小さく単純な命令セットを持つRISC(縮小命令セットコンピュータ)ではどうだろうか。 コンパイラをRISC-V 64ビットアーキテクチャ用のツールチェーンに変更して再度検証してみる。 まずは、以下のコマンドでコンパイルする: \begin{center} \begin{verbatim} riscv64-none-elf-gcc-14.2.1 -Wall -nostdlib -nolibc -O0 demo.c -o demo-riscv \end{verbatim} \end{center} そして以下のコマンドでデコンパイルする: \begin{center} \begin{verbatim} riscv64-none-elf-objdump -d demo-riscv \end{verbatim} \end{center} デコンパイル結果は以下の通りである: \begin{center} \begin{verbatim} demo-riscv: ファイル形式 elf64-littleriscv セクション .text の逆アセンブル: 00000000000100e8
: 100e8: 1101 addi sp,sp,-32 100ea: ec06 sd ra,24(sp) 100ec: e822 sd s0,16(sp) 100ee: 1000 addi s0,sp,32 100f0: 0ff00793 li a5,255 100f4: fef42623 sw a5,-20(s0) 100f8: 0001 nop 100fa: 60e2 ld ra,24(sp) 100fc: 6442 ld s0,16(sp) 100fe: 6105 addi sp,sp,32 10100: 8082 ret \end{verbatim} \end{center} このアセンブリの口語訳は以下となる\cite{riscv}: \begin{itemize} \item \texttt{sp}(スタックポインタ)レジスタに$\textrm{\texttt{sp}レジスタの値}+(-32)$の加算結果を代入する。 \item \texttt{ra}レジスタから$\textrm{\texttt{sp}レジスタが示めしているアドレス}+24$のアドレスが指している場所に64ビット値をコピーする。 \item \texttt{s0}レジスタから$\textrm{\texttt{sp}レジスタが示めしているアドレス}+16$のアドレスが指している場所に64ビット値をコピーする。 \item \texttt{s0}(スタックポインタ)レジスタに$\textrm{\texttt{sp}レジスタの値}+32$の加算結果を代入する。 \item \texttt{a5}レジスタに10進数値255を代入する。 \item \texttt{a5}レジスタから$\textrm{\texttt{sp}レジスタが示めしているアドレス}+(-20)$のアドレスが指している場所に32ビット値をコピーする。 \item なにもしない \item $\textrm{\texttt{sp}レジスタが示めしているアドレス}+24$のアドレスが指している場所から\texttt{ra}レジスタに64ビット値をロードする。 \newpage \item $\textrm{\texttt{sp}レジスタが示めしているアドレス}+16$のアドレスが指している場所から\texttt{s0}レジスタに64ビット値をロードする。 \item \texttt{sp}(スタックポインタ)レジスタに$\textrm{\texttt{sp}レジスタの値}+32$の加算結果を代入する。 \item 呼び出し元の関数に制御を戻す。 \end{itemize} やはり、命令の種類が少ないRISCでは命令の数が多くなっている。 だが最初の4つと最後の4つは関数のスタックフレームに関する命令で無視してよい。 見るべきものは\texttt{100f0}と\texttt{100f4}である。 そこでは32ビットの値255を\texttt{a5}レジスタに記憶し、スタックにコピーしている。 ここでは64ビット長のレジスタに32ビット値を代入することで変数の宣言と定義を同時にしている。 しかしレジスタは一時的なデータの保持に使用されるので関数の寿命の間、いつでも参照できるようにスタックに移動させているのである。 比べてみると、CISCとRISCではやることは同じである:スタックに変数の場所を作り、値を入れる。 以上より、\ref{var_decl_def}節で示した値の種類の判別にビット長を実行ファイルに埋め込むということがらが実証された。