<< 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() {
  return 42;
}

という当たり前のコードに対して、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の倍数にセットされていることが分かった。違うじゃん

tokenstrが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時間くらいかかった