PHPerKaigi 2019で、C/Goコンパイラを作る過程で学んだことについて話しました。
コンパイラ作りの魅力を語る@DQNEOphperkaigi 2019.3.30
View Slide
自己紹介@DQNEO どきゅねおアメリカ版メルカリを開発しています● PHPer● C初心者● Go中級者
目次● デモ● コンパイラとは何か?何をしているのか?● なんで作ろうと思ったのか?● どうやって作ったのか?
趣味: コンパイラ作りCコンパイラhttps://github.com/DQNEO/8cc.go8ccをGoに移植したものGoコンパイラhttps://github.com/DQNEO/minigoGoで書いたGoコンパイラ
デモ: CコンパイラFizz Buzz
デモ: GoコンパイラFizz Buzz
PHPerとCコンパイラphp-src(C)C compilerAssemblyBinary/usr/bin/phpスクリプト処理系
コンパイラとは何か
コンパイラとは何か広義:言語Xのコードを言語Yに変換するソフトウェア狭義:言語Xのコードを低レベル言語(アセンブリ/機械語/etc)に変換するプログラム
コンパイラは何をしているのか
Cコンパイラ (8cc)int sum(int a, int b) {return a + b;}sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretC言語 GNU assembler
コンパイラを作ってわかったことアセンブリの知識が必要
コンパイラを作ってわかったこと● (多くの)コンパイラはアセンブリを吐く● アセンブリを理解し読み書きするスキル必要= CPUのはたらきを理解すること
sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretアセンブリ言語 ≒ マシン語● 各行がCPUへの「命令」● どの行も、ハードウェアに対して副作用を生じる(レジスタ(後述)またはメモリ上のデータを変更する)
sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretGNU AssemblerCPUへの命令
sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretGNU assemblerメモリ読み書き
sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretGNU assemblerレジスタ読み書き
コンパイラを作ってわかったこと“レジスタ”が主役※私の主観です
CPU内にある超高速な一時記憶装置CPU← レジスタ“レジスタ”が主役出典: http://ftp.procmail.org/~sskulrat/Courses/2006F-170/lectures/chap14/part1.html
Cコンパイラ (8cc)int sum(int a, int b) {return a + b;}sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretC GNU Assembler関数 → 関数変数 → メモリ何番目
コンパイラを作ってわかったこと● C言語上の関数はアセンブリ上でも関数● アセンブリには変数がない。○ ハードウェア(メモリ/レジスタ)に「書く、読む」● 「書き込む」「読み取る」という動作が基本
値とは何か
高級言語視点値: 式を評価すると得られるもの1;
mov $1, %rax低級言語視点
値とは何か1; mov $1, %raxC GNU assembler
値とは何か式を評価して値を得る↓ある手続きを実行した際に、ハードウェアのある場所に値を書き込む
値とは副作用である※私個人の考えです
値とは何か8ccの場合、一連の処理の最後に必ずraxに値を書き込む約束になっている。raxが保持しているデータ、これが「値」の実体
式を評価して値を得るx + y;mov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %rax演算の結果をraxに書き込む
隠れた副作用実はrcxに副作用が残ったまま。コンパイラ側で、このrcx値は再利用しないように気をつける。これにより、副作用が1個(式の結果が1値)のように見せかけている。x + y;mov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %rax
関数の戻り値int sum(int x, int y) {return x + y;}sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretraxに値を書き込んで関数を抜ける。これが戻り値。受け取る側は、raxから値を読み出す。int sum(int x, int y) {return x + y;}
関数の戻り値int sum(int x, int y) {return x + y;}sum:push %rbpmov %rsp, %rbppush %rdipush %rsimov -8(%rbp), %raxmov -16(%rbp), %rcxadd %rcx, %raxleaveretint sum(int x, int y) {return x + y;}「純粋な関数」も、ハードウェアレベルでは副作用がある
コンパイラを作ってわかったこと● if文とfor文は仕組みがほぼ同じ○ 条件判定とジャンプ● ローカル変数とグローバル変数は扱いが全然違う● 「スタック」とローカル変数の関係
コンパイラ作りで得たもの1● アセンブラの読み書き能力● 字句解析・構文解析の知識● 対象言語(C,Go)の仕様に詳しくなった
コンパイラ作りで得たもの2● 原典にあたる習慣○ 言語仕様書○ Intel CPUマニュアル○ gdb, gasマニュアル
Intel® 64 and IA-32 ArchitecturesSoftware Developer’s Manual
コンパイラ作りで得たもの3● Segmentation Fault に負けない心○ gdb/gccなどのコンパイラツール系スキル
簡単なC/Goのコードなら脳内コンパイルできる
何でコンパイラを作ろうと思ったのか?
もともとは普通のPHPアプリケーション開発者だった
コンパイラ知識ゼロからのスタート● 字句解析・構文解析の解説を読んでも理解不能● C,Goもそんなに詳しくない「プログラムが動く原理を知りたい」
Rebuild.fm153回https://rebuild.fm/153/Rui Ueyama さんがCコンパイラ(8cc)を作った話きっかけめちゃくちゃ面白い!!
● C言語製Cコンパイラ● スクラッチから開発● インクリメンタル開発8cchttps://github.com/rui314/8cc
さっそく git clone
8cc 1コミット目#include #include int main(int argc, char **argv) {int val;if (scanf("%d", &val) == EOF) {perror("scanf");exit(1);}printf("\t.text\n\t"".global mymain\n""mymain:\n\t""mov $%d, %%eax\n\t""ret\n", val);return 0;}#include extern int mymain(void);int main(int argc, char **argv) {int val = mymain();printf("%d\n", val);return 0;}https://github.com/rui314/8cc/commit/3764b2071b9601067b81976d80175a0851d0f209
なんとなくわかる!しかし、ただ読んでるだけでは身につかなそうGoに移植してみよう!
1コミット移植してみたhttps://github.com/DQNEO/8cc.go/commit/b71196743107e50e0ac945383abc5b2c3bf2e302動いた!8cc.go (Goへの移植)
8ccのコミット履歴を1個ずつ● テストの追加分を見る● そのテストをパスするC実装を自力でやってみる● 無理なら正解をチラ見する● 自力でCで再現する● 無理なら写経● Goに移植する延々と繰り返す8cc.go (Goへの移植)
● 5ヶ月半毎日継続● 95コミット移植完了● 基本的な文法はだいたい動く8cc.go (Goへの移植)
移植で見えてきた問題点● 8ccの字句読み取りの設計が複雑● 9ccはきれいに設計できてる● 自分でもゼロから設計してみたい!
Goコンパイラ開発● 試しにGoでGoのtokenizerを書いてみた● あっさり動いた!感動!→ その勢いでGoコンパイラ開発に突入
minigo (Goコンパイラ)● 1日目:足し算が動いた!● 2日目:引き算・掛け算・関数呼び出しが動いた!● 5日目: helloworld.go が動いた!
minigo (Goコンパイラ)● 1ヶ月後:FizzBuzzが動いた!
● 5ヶ月後:自分自身のコンパイルに成功!(時間があればデモ)minigo (Goコンパイラ)
野望● 夏〜秋ごろにセルフホスト達成予定● 来年のアメリカのGopherConに応募するぞ
コンパイラはいいぞ
ご清聴ありがとうございました