Slide 1

Slide 1 text

自作Cコンパイラ 8時間の奮闘 2024-09-07 セキュリティキャンプ アフターイベント 2024-09-14 traP & Zli 合同LT 1

Slide 2

Slide 2 text

自己紹介 sou7といいます。 東北の山奥にある、会津 大学の学部4年生です。 定理証明支援系とかを触 ってます。 ArchLinux + i3 ユーザー です。 会津を走る只見線の風景→ 2

Slide 3

Slide 3 text

SNS ActivityPub/Misskey @[email protected] GitHub @soukouki Twitter @sou7_ _ _ 3

Slide 4

Slide 4 text

趣味 シミュレーションゲーム(特に物流系) Simutrans(輸送シミュレーションゲーム) Factorio(工場建設・輸送シミュレーションゲーム) ライトノベルを読み漁ること なろうをひたすら読んだりしてます Obsidian 日記や技術メモ、ブログ記事まで全てObsidianで管理しています 4

Slide 5

Slide 5 text

S06 Cコンパイラゼミ セキュキャンのCコンパイラゼミに参加しました! Cコンパイラゼミは、その名の通りC言語のコンパイラを自作するゼ ミです。自作Cコンパイラ 10cc を作成しました。 <ここでデモを入れる> 5

Slide 6

Slide 6 text

10ccのコード 第1世代 GCC 10ccのコード 第2世代 第1世代 10ccのコード 第3世代 第2世代 達成したこと 期間中に見事 セルフホスト を達成す ることが出来ました。セルフ ホストとは、自作Cコンパイ ラ自身で、自分自身をコンパ イルすることです。図でいう 第2世代以降を作るのがセル フホストになります。 1. セルフホストの要件には、第2世代と第3世代を一致させることで不動点を 作るというのもありますが、今回は省略します。 6

Slide 7

Slide 7 text

Cコンパイラゼミで得られたこと C言語に対する深い知識 例: switch文の構文、知ってますか? → Duff's device で検索! 例: スタック領域に変数を確保するって、何が行われているの? アセンブリの知識 わからない命令の調べ方 基本的な演算, ジャンプ, レジスタの扱いなど 難しいバグについても根気強く向き合う経験 セルフホストの際には込み入ったバグが多く発生します。 コードは全部手元にあるので、気合を入れてデバッグすれば必ず解 決できます! 7

Slide 8

Slide 8 text

8時間デバッグ大会 このコンパイラを作る中で、一番苦労したデバッグの話をします。こ のデバッグには計 8時間 かかりました。 そんなバグについて解説していきます。みなさんも何がバグの原因だ ったのかを想像しながら聞いてみて下さい。 8

Slide 9

Slide 9 text

時は2024年7月21日 Cコンパイラゼミはセキュキャン本番前から週に1回作業会を行って いました。このデバッグはその際に発生したものです。 9

Slide 10

Slide 10 text

第2世代を作ることは出来たが、第2世代にどんな入力 を与えても空のアセンブリを吐く 第1世代を使って、第2世代をコンパイルすることに成功しました。し かい、動かしてみるとどうにも変です。 第2世代にどんなCのコードを読み込ませても、空のアセンブリしか 吐かないのです。関数を入れても、文字列を入れても上手く動きませ ん。 10

Slide 11

Slide 11 text

フランケンコンパイル セルフホストのデバッグをする際に、いきなり全部セルフホストをす ると大変なので、「フランケンコンパイル」という手法を使います。 10cc のソースコードの一部を 10cc (第1世代)でコンパイルし、残り をGCCでコンパイルして、どのファイルがバグの原因なのかを探りま す。 GCCとの相互運用性を保つために、 10cc とGCCの出力するアセンブ リが互換性を持つようにしないといけません。今回は、構造体のアラ イメントをきちんと実装するだけで、フランケンコンパイルが出来る ようになりました。(バグが出始めてから2時間経過) 1. フランケンコンパイルという名称は、Cコンパイラゼミの講師であるhsjoihs氏が名付けたものです。 セキュキャンのCコンパイルゼミでしか使われていない独自用語らしいで す。 2. 実際には、構造体のアライメントをテストするために、 offsetof というものも実装しました。 11

Slide 12

Slide 12 text

10cc の構造 10cc は、以下のように4つの部分と、それを呼び出す main.c に別れ ています。それぞれの部分ごとにフランケンコンパイルを試していき ます。 字句解析 構文解析 意味解析 コード生成 トークン列 構文木 構文木 ソースコード アセンブリ 1. 他にも map.c というファイルがあったりしますが、ここでは省略します。 12

Slide 13

Slide 13 text

main.c が怪しい フランケンコンパイルを色々試すと、次の条件でコンパイルした場合 にバグることがわかりました。 main.c を第1世代でコンパイル それ以外をGCCでコンパイル 13

Slide 14

Slide 14 text

10ccのコード 第1世代 GCC 10ccのコード 第2世代 第1世代 10ccのコード 第3世代 第2世代 ここで main.c を睨みつけた くなるのですが、実はそう上 手くは行きません。 直接 main.c に問題があるの ではなく、 main.c をコンパ イルした第1世代のどこかに 問題があるということです。 main.c をいくら睨みつけて も、 main.c のC言語のコード は正しいのです。 14

Slide 15

Slide 15 text

むずい main.c が怪しいというところまで突き止めたものの、そこから先に 進みません。最終手段としてアセンブリを沢山読むという手もありま すが、数千行のアセンブリを読み解きたくはありません。 当時のDiscord上でのやりとり 15

Slide 16

Slide 16 text

違った方向からテスト 行き詰まってしまったので、今度は違う部分に注目してデバッグを進 めてみます。アセンブリが出力されないということは、本来アセンブ リを出力するはずだったコード生成に何らかの異常が起きているとい うことです。 コード生成にprintfを仕込んでみると、構文木が来ていません。 構文解析にprintfを仕込んでみると、元のトークン列が空です。 字句解析にprintfを仕込んでみると、そもそもソースコードが読 み込めていません! → ソースコードを読み込む部分にバグがあることがわかりました。 16

Slide 17

Slide 17 text

ソースコードを読み込む部分を書き直し なぜかわかりませんが、ソースコードを読み込む部分が上手く動いて なかったので、その部分を全て書き換えました。これで一段落! → が、このデバッグにはまだ続きがあります。 17

Slide 18

Slide 18 text

この時点でのエラー状況 map.cだけ自作コンパイラ、それ以外はgcc → 成功 main.cだけ自作コンパイラ、それ以外はgcc → 謎のエラー 「関数の戻り値の型がありません」!? 字句解析だけ → 失敗(セグフォ) 構文解析だけ → 成功 意味解析だけ → 成功 コード生成だけ → 成功 次に字句解析だけ 10cc のパターンを追ってみることにしました。 18

Slide 19

Slide 19 text

GDBでデバッグ C言語のセグフォはスタックトレースすら見れないので、まずはGDB を使ってスタックトレースを確認します。すると、意味解析のlibc関 数の呼び出しでセグフォが起きていました。 19

Slide 20

Slide 20 text

第1世代 (壊れている) 10ccのコード GCC 10ccのコード 第1世代(壊) 第2世代 (壊れている) この状況から、以下の形でバ グがあると予想できます。 1. 10cc のどこかにバグが あり、壊れた第1世代が 出来る。 2. その第1世代で字句解析 をコンパイルして、壊れ た第2世代が出来る。 20

Slide 21

Slide 21 text

3. 第2世代を動かすと、字句解析部分でメモリなどを壊している。 4. メモリなどが壊れた結果、第2世代の意味解析でセグフォが起き る。 構文解析 意味解析 トークン列(壊) 構文木(壊) ソースコード 字句解析(壊) セグフォ! 1. 実装していない機能があることに気づいて実装したり、16バイトアライメントを疑ったりとひと悶着あったのですが、割愛します。 21

Slide 22

Slide 22 text

memcmpが怪しい GDBでデバッグを続けると、 memcmp という標準ライブラリの関数を 呼び出した際にセグフォが起きていました。 さらに、 memcmp に渡している引数を確認すると、ヒープメモリに確 保しているはずのアドレスが 0xf7caf015 と、異様に短いことがわか りました。 メモリアドレスを処理するどこかで、64ビット必要なのに32ビット で計算してしまったのでしょう。意味解析より前の、どこかで。 1. 実は、GDBの出力を読み誤った結果、最初は memcmp ではなく strlen が原因だと思いこんでいたりしました。 2. この環境(x86_64, ArchLinux)では、正しいヒープ領域のアドレスは 0x611b3a3ca2a0 のように、16進数で12桁になります。しかし、今回の結果は8桁しかありません。 22

Slide 23

Slide 23 text

意味解析は 10cc の後半に位置する処理です。今回使ったポインタは 字句解析の箇所で生成したので、字句解析→意味解析のどこかで誤っ た処理が行われているとわかりました。 字句解析 構文解析 意味解析 コード生成 トークン列 構文木 構文木 ソースコード アセンブリ こんなふうに怪しい範囲が広いときは、皆さんどうするかおわかりで すね? 23

Slide 24

Slide 24 text

†2分探索† こうなったときは伝家の宝刀、†2分探索† を使います!処理の真 ん中あたりにおもむろに printf を仕込んで、どこでおかしくなって いるのかを探ります。 すると、字句解析のあたりですでに上位32ビットが失われていること がわかりました。さらに†2分探索†を続けると… 24

Slide 25

Slide 25 text

怪しい箇所を発見! cur = new_token(TK_SYMBOL, cur, p++, 1, file, line); このプログラムは正しいC言語のプログラムです。しかし、Cコンパ イラを自作した人は分かるはずです。 インクリメントは怪しい と。 バグの匂いがプンプンします。 1. これを解いている際には、関数呼び出しとインクリメントが組み合わさったせいなのでは?と思っていました。実際は関数呼び出しは冤罪でした。 25

Slide 26

Slide 26 text

アセンブリを読むしかない ここまで原因が絞れたら、次はアセンブリを読むしかありません。デ バッグ出力を加えたアセンブリは、字句解析のものだけで約4万行に なります。この中から該当のコードを探します。 問題の箇所(100行くらい)を見つけたら、次はスタックの状態を考え ながらアセンブリを読み込みます。 26

Slide 27

Slide 27 text

バグの原因 バグ : mov edi, [rax] 正常 : mov rdi, [rax] アセンブリの命令が、 rdi レジスタ(64ビット)ではなく、 edi レジス タ(32ビット)を使っていました。これが原因で上位32ビットが消えて しまっていたのです。 たった1文字の違いで、8時間もデバッグをする羽目になりました。 さらに、前半の main.c のバグについても、この rdi と edi のバグが 原因になっていることがわかりました。 27

Slide 28

Slide 28 text

まとめ セルフホストには、めちゃくちゃ大変なバグが続出します。 それを乗り越えて動作すると、とても大きな達成感がありあま す。 みんなもCコンパイラをセルフホストさせてみよう! (学部1-3年生へ)来年はセキュキャンに挑戦してみると楽しいよ! たった1文字の間違いで8時間もデバッグすることもあるの で気合を入れて頑張ろう! 28

Slide 29

Slide 29 text

その他 第一只見川橋梁の写真はMaedaAkihiko氏の著作物です。Creative Commons Attribution-Share Alike 4.0 29