<< 2024-07-07のCコンパイラゼミ | Cコンパイラゼミ2024 | 2024-07-28のCコンパイラゼミ >>
- 自作コンパイラでコンパイル出来た!
- 2 世代目コンパイラが C のコードをコンパイルしようとすると、関数の中にコードのない虚無が出力されるw
- こんなに中途半端なバグり方をすることがあるのか
hsjoihsさんが記録を残してくれたので、そこから辿って書いておく
undefined reference
- アセンブリに
.globl
が付いてないのが原因だった - Compiler Explorerは
.globl
を省略していた
errno
- よくわからないので、とりあえずerrnoを使わないようにした
- ヘッダファイルの中の方まで見に行ったけれど、マクロを定義しているだけに見えた
- よくわかんないものは見なかったことにする
2世代目コンパイラのバグ
普通のデバッグは
- 最近書いたところ
- エラーが起きた場所の近く を探して、そのC言語のコードを見ればバグが分かる
けれど、2世代目コンパイラのバグは問題箇所のC言語のコードには問題がなく、「問題箇所をコンパイルして得られたアセンブリ」に問題がある(つまりはそれをコード生成した場所)
2世代目コンパイラがCのコードをコンパイルしようとすると、コードのない虚無が生成される
hsjoihs「そう! これがセルフホストデバッグの醍醐味なんですよ」 hsjoihs「『ソースコードをコンパイラが食って実行形式が出てくる』という流れがあって、実行形式がバグっているときというのは、普通はソースコードにバグがあって、正しくない指示を書いている」
長い長い(8時間+休憩1時間)のデバッグの始まり
関数や文字列のデータを扱うmap.cに問題があるのかと思ってた頃があったなぁ
問題を分割するために、1つのファイルを自作コンパイラで、それ以外のファイルをGCCでコンパイルすることにした。hsjoihsさんか、Rui Ueyamaさんのどっちかがフランケンコンパイルと名付けたらしい。フランケンバイナリ、フランケンビルドはRuiさんが言ってたとのこと。
構造体のアラインメントの実装
- フランケンコンパイルのためには、GCCでコンパイルしたアセンブリと、自作コンパイラで実装したアセンブリをリンクする必要がある。
- アラインメントの実装は思っていたよりも簡単だった
- アラインメントだけ実装したらGCCで生成したアセンブリとリンクできるようになった
- 今までは一切GCCとの相互運用性は考えてなかったので意外だった
ここにhsjoihsさんが作った意地悪なテストケースが用意してある GitHub - hsjoihs/c-compiler: seccamp2018 c compiler
offsetofの実装
- フランケンコンパイルのために、GCCと構造体のオフセットを合わせないといけない
- そのテストのためには
__builtin_offsetof
を実装すると良さそうだったので、実装した- こういうのをコンパイラマジックという
- 普通にバグが見つかったので、これで試して良かった
- これでバグが見つからないままやってたら、フランケンコンパイルで死ぬほど面倒くさいデバッグをする羽目になるところだった
RSPを16ビットにアラインメントする
- strcpyだかmemcpyだかでのセグフォだったので、16バイトアライメントをまず疑った
- 低レイヤを知りたい人のためのCコンパイラ作成入門に載ってたRSPを16ビットにアラインメントするやつが原因じゃないかと疑ってコード生成を確認した
- 確認した結果、すべてのpush/popで8ビット分ずらしている箇所は見つからなかった
- (当時は16バイトアライメントを16ビットアライメントだと勘違いしていた。当然この当時の実装では正常に動かない)
GCCと自作Cコンパイラでのコンパイル
第2世代で出るバグは、第1世代が原因で、第1世代の何が原因なのかを追いづらいというやつ
第2世代と第3世代のバイナリが一致したら、そこが不動点となって、それ以降はバイナリが変化しないということが分かる
自作コンパイラを通すファイルを変えると、変えるごとに違うエラーが出るという状況になる
変なエラーとしては、
という当たり前のコードに対して、int main
の段階で型名が無いよ(???)というエラーが出たりした
2世代目コンパイラがCのコードをコンパイルしようとすると、コードのない虚無が生成される(リターンズ)
- コード生成の段階から順番に追っていって、最終的に
read_file
が文字列を読み込んでいないというのが原因だと分かった read_file
が何故か2世代目でファイルを読めなくなっていた(?)ので、read_file
を簡単な実装に置き換えてお茶を濁すことにした
make selftest4のセグフォ
自作コンパイラの謎状況
- map.cだけ自作コンパイラ、それ以外はgcc ⇒ 成功
- main.cだけ自作コンパイラ、それ以外はgcc ⇒ 謎エラー(関数の戻り値の型がありません)
- tokenize.cだけ ⇒ 失敗(セグフォ)
- parse.cだけ ⇒ 成功
- analyse.cだけ ⇒ 成功
- gem.cだけ ⇒ 成功
謎エラーは謎すぎるので、とりあえずセグフォから追うことにした
GDBでデバッグしたところ、parser捨てるようにしたでセグフォが出ていることが分かった
parserで最初のトークンを読み込めていない? ⇒ GDBの画面を読み間違えていた。セグフォだと思ってたものはセグフォじゃなかった。
ポインタ同士の減算
- トークナイザを読んでいると、ポインタ同士の減算をしていた。
- ポインタ同士の減算は割り算を提供しないといけなくて、それをまだ実装していなかった
- C言語にはcompatibleという概念があり、いい感じに型が合ってるか合ってないかをそれで判断してる
- 今回の自作コンパイラでは、サイズが合っていて配列→ポインタの変換が行えればcompatibleだということにした
16-byte align
デバッガの読み間違いに気づき、さらにデバッグをしていると、次のような命令でセグフォが起きていることに気づいた
Program received signal SIGSEGV, Segmentation fault.
__memcmp_evex_movbe () at ../sysdeps/x86_64/multiarch/memcmp-evex-movbe.S:132
132 VPCMP $4, (%rdi), %VMM(2), %k1{%k2}
(GDB) fin
Run till exit from #0 __memcmp_evex_movbe () at ../sysdeps/x86_64/multiarch/memcmp-evex-movbe.S:132
hsjoihs「あっ VMM とか書いてある! 16 byte align 違反の疑いがあります」 hsjoihs「x86-64 は、一部の長いレジスタを操作するときに、16 byte align されている必要があるんですよ。それを踏んでいる疑いがあります」
hsjoihs「これは険しいやつでしたね。これ自力で気づくの無理なんだよな」
しかし、デバッガでRSPレジスタをよくよく確認したところ、ちゃんと呼び出し時に16の倍数にセットされていることが分かった。違うじゃん
token→strが32ビットの数になっている
GDB+printfデバッグ
- さらにGDBでデバッグを進めていると、memcmpに渡す第1引数のメモリの値を見れないことに気づく
- hsjoihsさんがmemcmpに渡しているアドレスがやけに短いことに気づく
- 64ビットのアドレスが32ビットに打ち切られていそう
token->str
の中身はread_fileで1MB mallocしたところなので、ヒープメモリのアドレスのはず- parse.cにprintfを設置してデバッグすると、どうにもparseの前の段階で正しくないアドレスが指定されているみたい
- tokenize.cにもprintfを設置してデバッグすると、
cur = new_token(TK_SYMBOL, cur, p++, 1, file, line);
というコードで問題が起こっていそう- インクリメントしたものを関数に渡す部分、いかにも怪しそう
new_token
側で引数を確認すると、すでに正しくない値が来ていた。つまり、p++
して引数に渡す部分がダメそうp++
を別の行にすると上手く動くようになった。ここからは生成されるアセンブリを読むしか無い
アセンブリにらめっこ
- アセンブリの行数がエグい
- デバッグ出力マシマシにした所、アセンブリのファイルが4万行ある。ここから問題のアセンブリを探さないといけない
- コード生成部分で出したコメント+アセンブリだけで8000行もある
- pの引数を漁って確認した
- すると、途中のインクリメントの命令のときに
mov edi, [rax]
としているのを見つけたmov edi, [rax]
vs.mov rdi, [rax]
- これを修正したところ、
make selftest4
は成功するようになった - ついでに
read_file
の修正をrevertしたところ、なんと動くようになった。これが原因だったのか - ここまで最初の
read_file
から数えると8時間、直接的にセグフォから数えると3時間のバグ修正だった- 途中で晩ごはん休憩1時間挟む
- 社会人じゃん
- 最後にObsidianに今日の作業を纏めたら1時間くらいかかった