メインコンテンツまでスキップ

『低レイヤを知りたい人のための C コンパイラ作成入門』勉強ノート

低レイヤを知りたい人のための 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");

データ構造アライメント - Wikipedia

メモリアドレス - Wikipedia

バイトマシン - Wikipedia

メモリアドレス 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; として扱う例。

mainの
リターンアドレス
mainの...
mainの呼び出し時点の
RBP
mainの呼び出し時点の...
x
x
main() { x=3; return x; }
main() { x=3; return x; }
RBP
RBP
push rbp
mov rbp, rsp
sub rsp, 8
push rbp...
RSP
RSP
mainの
リターンアドレス
mainの...
mainの呼び出し時点の
RBP
mainの呼び出し時点の...
x
x
x のアドレス
x のアドレス
3
3
RBP
RBP
RSP
RSP
lea rax, [rbp-8]
push rax
push 3
lea rax, [rbp-8...
mainの
リターンアドレス
mainの...
mainの呼び出し時点の
RBP
mainの呼び出し時点の...
x (3)
x (3)
pop rdi
pop rax
mov [rax], rdi
pop rdi...
RBP
RBP
RSP
RSP
rdi=3
rax=xのアドレス
rdi=3...
mainの
リターンアドレス
mainの...
mainの呼び出し時点の
RBP
mainの呼び出し時点の...
x (3)
x (3)
3
3
push rdi
add rsp, 8
push rdi...
RBP
RBP
RSP
RSP
rdi=3
rax=xのアドレス
rdi=3...
mainの
リターンアドレス
mainの...
mainの呼び出し時点の
RBP
mainの呼び出し時点の...
x (3)
x (3)
3
3
lea rax, [rbp-8]
push rax
pop rax
mov rax, [rax]
push rax
pop rax
jmp .Lreturn.main
lea rax, [rbp-8]...
RBP
RBP
RSP
RSP
rax=3
rax=3
mainの
リターンアドレス
mainの...
mainの呼び出し時点の
RBP
mainの呼び出し時点の...
x (3)
x (3)
3
3
.Lreturn.main:
mov rsp, rbp
pop rbp
ret
.Lreturn.main:...
RBP
RBP
RSP
RSP
rax=3
rbp=mainの呼び出し時点のRBP
rax=3...
Text is not SVG - cannot display

draw.io

cf. https://www.gnu.org/software/gnu-c-manual/gnu-c-manual.html#Expression-Statements

next

https://www.sigbus.info/compilerbook#%E3%82%B9%E3%83%86%E3%83%83%E3%83%9720-sizeof%E6%BC%94%E7%AE%97%E5%AD%90

https://github.com/rui314/chibicc/commits/reference?after=ce61154cf542e630bc3e40262fdacdf20bf91b90+34

https://github.com/rui314/chibicc/commit/f5536961e8182951376e320fafe4340c3a8e12b3 から

References