This commit is contained in:
2025-04-21 15:34:24 +09:00
parent e7b0f70647
commit a510bce35e
10 changed files with 178 additions and 9 deletions

View File

@@ -4,7 +4,7 @@
\subsection{Deep Dive - 変数の舞台裏}
\ref{var_decl_def}節にてプログラムが値の種類を判別するためにコンパイラが型情報に基づいて読み出すべきビット長を実行ファイルに書き込まれると書いたが、実際はどうであろう。
ここではコンパイラの生成物である実行ファイルをアセンブリに分解して検証していく。なお、この付録を読むのに特別な知識は必要なく、必要に応じて説明する。
ここではコンパイラの生成物である実行ファイルをアセンブリに分解して検証していく。
まずは検証用のソースコードを用意する。今回は以下の物を用意した:
@@ -50,7 +50,6 @@ Linuxでの\texttt{gcc}コンパイラは特に指定されていなければコ
\begin{center}
\begin{verbatim}
demo: ファイル形式 elf64-x86-64
セクション .text の逆アセンブル:
@@ -87,7 +86,75 @@ demo: ファイル形式 elf64-x86-64
これはまさに宣言そのもので、\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 <main>:
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}節で示した値の種類の判別にビット長を実行ファイルに埋め込むということがらが実証された。