This commit is contained in:
2025-04-21 22:20:36 +09:00
parent a510bce35e
commit 04335dd817
12 changed files with 69 additions and 36 deletions

View File

@@ -3,7 +3,8 @@
\subsection{Deep Dive - 変数の舞台裏}
\ref{var_decl_def}節にてプログラムが値の種類を判別するためにコンパイラが型情報に基づいて読み出すべきビット長を実行ファイルに書き込まれると書いたが、実際はどうであろう。
\ref{var_decl_def}節にてプログラムが値の種類を判別するためにコンパイラが型情報に基づいて読み出すべき\\
ビット長を実行ファイルに書き込まれると書いたが、実際はどうであろう。
ここではコンパイラの生成物である実行ファイルをアセンブリに分解して検証していく。
まずは検証用のソースコードを用意する。今回は以下の物を用意した:
@@ -15,11 +16,13 @@
このコードをコンパイルするにあたって、解読を容易にするために少しフラグを変更する必要がある。
Linuxでの\texttt{gcc}コンパイラは特に指定されていなければコンパイラの機能を使用するための\texttt{libgcc}\texttt{libc}というC言語の標準的な関数(\texttt{printf}, etc.)などが宣言・定義されたライブラリをリンクします。
これによりソフトウェアの日常的な機能の再開発を避けれるが、人間が読めるアセンブリに直すとその部分のコードが析出してしまい、目的のコードが埋もれてしまう。
これによりソフトウェアの日常的な機能の再開発を避けれるが、人間が読めるアセンブリに直すと\\
その部分のコードが析出してしまい、目的のコードが埋もれてしまう。
なので、これらライブラリの自動リンクをやめる必要がある。そこで今回は\texttt{-nostdlib}フラグと\texttt{-nolibc}フラグを追加する。\cite{gcc_man}
最近のコンパイラは非常に賢く、無意味なコードを取り除いて最適化しようとする。
だが今回の検証用コードはただ単に変数を宣言・定義するだけで、実際に使用されないのでそのままではコンパイラに無意味と見なされ、勝手に変数の宣言・定義のコードを削除してしまう。
だが今回の検証用コードはただ単に変数を宣言・定義するだけで、実際に使用されないのでそのままではコンパイラに\\
無意味と見なされ、勝手に変数の宣言・定義のコードを削除してしまう。
なのでここにコンパイラによる最適化を無効にするために\texttt{-O0}フラグも追加する。\cite{gcc_man}
最終的なコンパイルコマンドはこのようになる:
@@ -30,7 +33,7 @@ Linuxでの\texttt{gcc}コンパイラは特に指定されていなければコ
\end{verbatim}
\end{center}
上記のコマンドを実行するとリンカからエントリーポイント:プログラムの始まりが定義されていないとエラーを吐くが解読に問題はないのでそのままでよい。
上記のコマンドを実行するとリンカからエントリーポイント:プログラムの始まりが定義されていないとエラーを吐くが解読に問題はないのでそのままでよい。
次に生成物である\texttt{demo}をデコンパイル(実行ファイルからアセンブリに戻す作業)を行うためにGNU Binutils\footnote{\url{https://www.gnu.org/software/binutils/}}\texttt{objdump}を利用する。
@@ -50,17 +53,17 @@ Linuxでの\texttt{gcc}コンパイラは特に指定されていなければコ
\begin{center}
\begin{verbatim}
demo: ファイル形式 elf64-x86-64
demo-x86_64: ファイル形式 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
0000000000001000 <main>:
1000: 55 push rbp
1001: 48 89 e5 mov rbp,rsp
1004: c7 45 fc ff 00 00 00 mov DWORD PTR [rbp-0x4],0xff
100b: 90 nop
100c: 5d pop rbp
100d: c3 ret
\end{verbatim}
\end{center}
@@ -69,28 +72,29 @@ demo: ファイル形式 elf64-x86-64
このアセンブリの口語訳は以下となる:
\begin{itemize}
\item \texttt{rbp}レジスタ(CPU内にあるごく小さな変数)の内容をスタック(メモリ上にある最初に入れたデータは最後に出る仕組みを持つデータ保持の構造)の最上部に積む。
\item \texttt{rbp}レジスタ(CPU内にあるごく小さな変数)の内容をスタック(メモリ上にある最初に入れた\\データは最後に出る仕組みを持つデータ保持の構造)の最上部に積む。
\item \texttt{rsp}レジスタの内容を\texttt{rbp}レジスタに書き込む。
\item 16進数値\texttt{0xff}(10進数で255)を\texttt{rbp}レジスタの内容から\texttt{0x4}を引いたアドレス(メモリ上の場所を表す住所のような数値)が指している場所に書き込む。
\item 16進数値\texttt{0xff}(10進数で255)を\texttt{rbp}レジスタの内容から\texttt{0x4}を引いたアドレス(メモリ上の\\場所を表す住所のような数値)が指している場所から\texttt{DWORD}(4バイト)分の領域に書き込む。
\item なにもしない。
\item スタックの最上部にある値を取り、それを\texttt{rbp}レジスタに読み込む。
\item 呼び出し元の関数に制御を戻す。
\end{itemize}
\texttt{rbp}レジスタは主に関数内でのみ有効なローカル変数を格納するレジスタである。
また、\texttt{rsp}レジスタはスタックポインタといい、現在実行されている関数に関するデータの読み書きに使用される。
また、\texttt{rsp}\\レジスタはスタックポインタといい、現在実行されている関数に関するデータの読み書きに使用される。
今回の例では、まず\texttt{rbp}レジスタの内容をスタックに退避させ、現在のスタックポインタが指しているアドレスを\texttt{rbp}レジスタにコピーする。
次に、\texttt{rbp}が指しているアドレスから4つ分移動させる。
この時の4は単位が1バイトであるため$4*8=32$ビット分の空を確保している。
次に、\texttt{rbp}が指しているアドレスから4つ分移動させ\\スタックの4バイト分の領域を確保する。
1バイトは8ビットであるため$4*8=32$ビット分の空を確保している。
これはまさに宣言そのもので、\texttt{int}型は32ビットのサイズを持つのでちゃんと当てはまる。
更にこの空いた場所に\texttt{movl}命令を使用して16進数値\texttt{0xff}、10進数の255を移動させている。
これが定義であり、宣言した場所に値を代入するというC言語の\texttt{a = 255;}と一致する。
更にこの空いた場所に\texttt{mov}命令を使用して16進数値\texttt{0xff}、10進数の255を移動させている。
これが\\定義であり、宣言した場所に値を代入するというC言語の\texttt{a = 255;}と一致する。
\texttt{x86\_64}の様なCISC(複雑命令セットコンピュータ)は宣言と定義を一つの命令で行っている。
\newpage
では、小さく単純な命令セットを持つRISC(縮小命令セットコンピュータ)ではどうだろうか。
では、小さく単純な命令セットを持つRISC(縮小命令セットコンピュータ)ではどうだろうか。\\
コンパイラをRISC-V 64ビットアーキテクチャ用のツールチェーンに変更して再度検証してみる。
まずは、以下のコマンドでコンパイルする:
@@ -149,12 +153,14 @@ demo-riscv: ファイル形式 elf64-littleriscv
\end{itemize}
やはり、命令の種類が少ないRISCでは命令の数が多くなっている。
だが最初の4つと最後の4つは関数のスタックフレームに関する命令で無視してよい。
見るべきものは\texttt{100f0}\texttt{100f4}である。
だが最初の4つと最後の4つは\\関数のスタックフレームに関する命令で無視してよい。
見るべきものは\texttt{100f0}\texttt{100f4}である。\\
そこでは32ビットの値255を\texttt{a5}レジスタに記憶し、スタックにコピーしている。
ここでは64ビット長のレジスタに32ビット値を代入することで変数の宣言と定義を同時にしている。
しかしレジスタは一時的なデータの保持に使用されるので関数の寿命の間、いつでも参照できるようにスタックに移動させているのである。
\texttt{x86\_64}との違いは読み出すバイト数が命令引数のキーワードとして明示されているか否かで、\texttt{RISC-V}では読み出すサイズによって命令が分けられている(eq. \texttt{SB}: 1バイト、\texttt{SH}: 2バイト、\\
\texttt{SW}: 4バイト)\cite{riscv}
比べてみると、CISCとRISCではやることは同じであるスタックに変数の場所を作り、値を入れる。
比べてみると、CISCとRISCではやることは同じであるスタックに変数のサイズ分の場所を作り、値を入れる。
以上より、\ref{var_decl_def}節で示した値の種類の判別にビット長を実行ファイルに埋め込むということがらが実証された。