Files
information-processing-1_2n…/section/appendix.tex
2025-04-18 00:06:52 +09:00

94 lines
5.8 KiB
TeX

\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 <main>:
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;}と一致する。
\newpage
以上より、\ref{var_decl_def}節で示した値の種類の判別にビット長を実行ファイルに埋め込むということがらが実証された。