サイト「低レイヤを知りたい人のためのCコンパイラ作成入門」をやってみる 1回目
掲題通りです。コンパイラを作成することでコード解析とアセンブラ出力が学習できれば。
参照サイト
環境
想定する実行環境が64bit LinuxということでWSLをインストールします。 Windows側では/User/hoge/ubuntuというフォルダを用意します。 WSL側で/home/hoge/.bashrcの末尾に cd /mnt/c/users/hoge/ubuntu と記載しておき、WSL起動時に開発ディレクトリにアクセスできるようにしています。
簡単な例
int main() { return 42; }
$ gcc -o test1 test1.c $ ./test1 $ echo $? 42
gccして終了コードを$?で見れることが分かる。
次に上記コードに対応するアセンブリプログラムを書いてみる
.intel_syntax noprefix .global main main: mov rax, 42 ret
呼び出し規約で関数の戻り値はレジスタraxに入れる、とのこと。gccして$?を参照すると先ほどと同様終了コードをみることが出来た。
関数呼び出しのコードがアセンブラでどうなるか
関数呼び出しはただのジャンプと違い、呼び出された関数の終了後、元の場所に戻らなければいけない。 そこで元のアドレス「リターンアドレス」をスタックメモリに保存しておく。 なぜスタックなのかは、多段呼び出しに対応するため。
スタックはスタックの一番上のアドレスを記憶しておけばよい。この一番上のアドレスを記憶しているのがスタックポインタ。x86-64には
がサポートされている。ちなみにスタックにデータを積むことを「プッシュ」と言い、データを取り出すことを「ポップ」という(復習)。(キュー構造ではデータ追加がenqueue、データ取り出しがdequeue)
以下、関数呼び出しの実例。引数x, yを足し合わせる関数plusとそれを呼び出して戻り値を終了コードに乗せるmain関数をアセンブラで書いたもの。Cコードは割愛。
.intel_syntax noprefix #アセンブリ文法指定 .global plus, main #plusとmainがプログラム全体から見えることを指示 plus: add rsi, rdi mov rax, rsi ret main: mov rdi, 3 #お約束 mov rsi, 4 #第一引数はrdi、第二引数はrsiレジスタ call plus ret
関数を呼び出す命令callは以下のことを行う。
- callの次の命令のアドレスをスタックにプッシュ(上述の「リターンアドレス」?)
- callの引数アドレスにジャンプ
関数の戻り値はraxに入れておく決まりなので、plus関数のret命令の前にraxレジスタへの格納命令がある。
plus関数の最後にret命令がある。ret命令は以下のことを行う。
- スタックからアドレスを1つポップ(取り出す)
- 取り出したアドレスにジャンプ
mainの中でcall plusの次の命令のアドレスをスタックにプッシュしていたので、plus関数のret命令によりcall plusの次のアドレスに飛ぶことができる。
章の(個人的)まとめ
- 関数戻り値はraxレジスタに格納
- callとretは対。
次章
次は「電卓レベルの言語の作成」をやります。