『低レイヤを知りたい人のための C コンパイラ作成入門』勉強ノート
call を呼ぶ前に 16 byte aligned にする
低レイヤを知りたい人のための C コンパイラ作成入門 - ステップ 14: 関数の呼び出しに対応する
x86-64 の関数呼び出しの ABI は(上のようなやり方をしている限りは)簡単ですが、注意点が一つあります。関数呼び出しをする前に RSP が 16 の倍数になっていなければいけません。push や pop は RSP を 8 バイト単位で変更するので、call 命令を発行するときに必ずしも RSP が 16 の倍数になっているとは限りません。この約束が守られていない場合、RSP が 16 の倍数になっていることを前提にしている関数が、半分の確率で落ちる謎の現象に悩まされることに m なります。関数を呼ぶ前に RSP を調整するようにして、RSP を 16 の倍数になるように調整するようにしましょう。
リファレンス実装の該当のコードは以下
https://github.com/rui314/chibicc/commit/aedbf56c3af4914e3f183223ff879734683bec73
// We need to align RSP to a 16 byte boundary before
// calling a function because it is an ABI requirement.
// RAX is set to 0 for variadic function.
int seq = labelseq++;
printf(" mov rax, rsp\n");
printf(" and rax, 15\n");
printf(" jnz .Lcall%d\n", seq);
printf(" mov rax, 0\n");
printf(" call %s\n", node->funcname);
printf(" jmp .Lend%d\n", seq);
printf(".Lcall%d:\n", seq);
printf(" sub rsp, 8\n");
printf(" mov rax, 0\n");
printf(" call %s\n", node->funcname);
printf(" add rsp, 8\n");
printf(".Lend%d:\n", seq);
printf(" push rax\n");
メモリアドレス a は、a が n バイトの倍数(n は 2 の累乗)であるときに、「n バイトアライメント」と呼ばれる。
rsp が指すメモリアドレスが 16 バイトの倍数である必要がある。
アドレス付けは基本 1 バイト単位である。なので、rsp の 2 進数下 4 桁が 0 の場合は 16 バイトアラインメントである。
// rsp を rax に移動
printf(" mov rax, rsp\n");
// rax と 15 (0b001111) を and 演算して、0 の場合は 16 バイトアラインメント
printf(" and rax, 15\n");
// 16バイ トアラインメントではない場合 (jnz: Jump if Not Zero) .Lcall へ
printf(" jnz .Lcall%d\n", seq);
// 16バイトアラインメントの場合
// 関数を呼び出す
printf(" mov rax, 0\n");
printf(" call %s\n", node->funcname);
// .Lend へ
printf(" jmp .Lend%d\n", seq);
// 16バイトアラインメントではない場合
printf(".Lcall%d:\n", seq);
// rsp から8バイト引く
// pushやpopは8バイト単位で変更するので8バイトでok
printf(" sub rsp, 8\n");
printf(" mov rax, 0\n");
// 関数を呼び出す
printf(" call %s\n", node->funcname);
// rsp に8バイト足して元の位置に戻す
printf(" add rsp, 8\n");
cf.
Intel 記法
この段階のリファレンス実装の出力はこんな感じ。
$ ./chibicc "main() { 0; }"
.intel_syntax noprefix
.global main
main:
push rbp
mov rbp, rsp
sub rsp, 0
push 0
add rsp, 8
.Lreturn.main:
mov rsp, rbp
pop rbp
ret
手元の Rust 実装がうまく動かなくなってしまったのでデバッグする。 手元の出力はこんな感じ。
$ docker compose run joe cargo run "main() { 0; }"
Compiling joe v0.1.0 (/home/user/joe)
Finished dev [unoptimized + debuginfo] target(s) in 3.31s
Running `target/debug/joe 'main() { 0; }'`
.global main
main:
push rbp
mov rbp, rsp
sub rsp, 0
push 0
.Lreturn.main:
mov rsp, rbp
pop rbp
ret
.intel_syntax noprefix
が足りていない。これは Intel 記法を使用することを指すディレクティブ。
AT&T 記法との主な差:
- オペランドの順番がディスティネーション、ソースの順番になる (AT&T はソース、ディスティネントの順)
- レジスタ名はそのまま使用する (AT&T は%プレフィックス)
- メモリアクセスに[]を使う (AT&T は())
- 即値には$プレフィックスを付けな い (AT&T は付ける)
cf. ステップ 1:整数 1 個をコンパイルする言語の作成
main() { a=3; z=5; return a+z; } => 8 expected, but got 10
expected
$ ./chibicc "main() { a=3; z=5; return a+z; }"
.intel_syntax noprefix
.global main
main:
push rbp
mov rbp, rsp
sub rsp, 16
lea rax, [rbp-16]
push rax
push 3
pop rdi
pop rax
mov [rax], rdi
push rdi
add rsp, 8
lea rax, [rbp-8]
push rax
push 5
pop rdi
pop rax
mov [rax], rdi
push rdi
add rsp, 8
lea rax, [rbp-16]
push rax
pop rax
mov rax, [rax]
push rax
lea rax, [rbp-8]
push rax
pop rax
mov rax, [rax]
push rax
pop rdi
pop rax
add rax, rdi
push rax
pop rax
jmp .Lreturn.main
.Lreturn.main:
mov rsp, rbp
pop rbp
ret
actual
docker compose run joe cargo run "main() { a=3; z=5; return a+z; }"
Compiling joe v0.1.0 (/home/user/joe)
Finished dev [unoptimized + debuginfo] target(s) in 2.08s
Running `target/debug/joe 'main() { a=3; z=5; return a+z; }'`
.intel_syntax noprefix
.global main
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov rax, rbp
sub rax, 0
push rax
push 3
pop rdi
pop rax
mov [rax], rdi
push rdi
mov rax, rbp
sub rax, 0
push rax
push 5
pop rdi
pop rax
mov [rax], rdi
push rdi
mov rax, rbp
sub rax, 0
push rax
pop rax
mov rax, [rax]
push rax
mov rax, rbp
sub rax, 0
push rax
pop rax
mov rax, [rax]
push rax
pop rdi
pop rax
add rax, rdi
push rax
pop rax
jmp .Lreturn.main
.Lreturn.main:
mov rsp, rbp
pop rbp
ret
ローカル変数
TODO: ローカル変数の扱い
符号の反転
2 の補数表現では、「全てのビットを反転して 1 を足す」と正負の反転ができる。
例えば、 3 と -3 は 0b0000_0011
-> 0b1111_1101
となる。
このトリックが動くアイディアは以下の通り。
全ビットの反転は全ビットが 1 の 2 進数、つまり 10 進数で -1 からの引き算と考えることができる。
1111 1111
- 0011 0011
= 1100 1100
(-1 - n) + 1 = -n
なので n の符号が反転する。
Expression Statement
式 (Expression) にセミコロンをつけることで文 (Statement) として扱うことができる。これを式文という。
その式に副作用がある場合のみ意味がある。
式文では評価された値は使わないので、最後に add rsp, 8
を追加してRSP (スタックポインタ) を戻す。
以下は式 x=3
を式文 x=3;
として扱う例。
cf. https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html#Expression-Statements
next
https://github.com/rui314/chibicc/commit/f5536961e8182951376e320fafe4340c3a8e12b3 から