ELF を吐く C コンパイラを Rui 本の方式で作る

編纂者:hsjoihs

リポジトリへのリンク

Google Presentation へのバックリンク

会話ログ本体

2022年10月3日(Discord にて)

tkr
tkr
そういや C コンパイラやってないな。アセンブリ言語を機械語にするのに binutils のを使うのもどうなんだろと思ってたらそのまま数ヶ月経ってた
hsjoihs
hsjoihs
いいですね。だったら、Rui 本の原則に則って、『step 1 からもう ELF バイナリへとコンパイルしちゃう』『常に実行形式をコンパイラが生成し続けられるようにする』というのでやってみませんか?
tkr
tkr
なるほど
hsjoihs
hsjoihs
C → アセンブラのコンパイラをセルフホストしたあとにアセンブラとリンカと libc と書くのは既に ushitora_anqou がやってるけど、最初から ELF バイナリへとコンパイルしつづける形式で Rui 本を走ってる人を私は知らないので、やってみるとおもしろそう
教材書けとは言わんので、作業ログでもいいんで書いてみません?言語はどれでやるんです?
tkr
tkr
もちろん Rust
hsjoihs
hsjoihs
じゃあ、step 1 は『return 3; するやつと return 42; するやつを gcc でコンパイルして ELF バイナリを吐いて、その差分を比較してどのバイトを置き換えるべきかを調べ、それを `include_bytes!()` する』という方針にするのがよさそう
なので、step 1 ではアセンブラもリンカもない。まあ、拡張していくと、機械語を直にいじるのがじきにきつくなるので、だんだんアセンブリ言語が育っていって、それは GNU のアセンブリ言語とそれなりに互換性があることが期待されるが、別にそれを目標にするわけではない。
という感じでやっていくとよさそう。今日はもう寝ます?
tkr
tkr
いま布団の中です
hsjoihs
hsjoihs
ではでは、おやすみなさい〜

2022年10月3日(一人で作業)

hsjoihs
hsjoihs
じゃあ私が勝手に step 1 を実装するか。なんなら会話ログももうレンダリングできるようにしておこう
とりあえず docs フォルダを立てて、docs/dialog.txt に会話を書いて docs/ 内で node index.js したらログがレンダリングできるようにしておいた
このリポジトリに招待を飛ばしておいて、
Ubuntu の方にリポジトリをクローンし、cargo init し、議事録を書く。おや、npm が通らない。えっと https://stackoverflow.com/questions/67938486/after-installing-npm-on-wsl-ubuntu-20-04-i-get-the-message-usr-bin-env-bash に従ったら直った。
ちゃんと README.md も書いておこう。まあ日本語と英語で書いておくか。
もう step 1 は私がやってしまおう。experiment フォルダにファイルを用意して、
とりあえず Makefile はこんな感じでいいか
CFLAGS=-std=c17 -Wall -s

3: 3.c
42: 42.c

clean: 
	rm 3
	rm 42

.PHONY: clean
gcc が出力したファイルのサイズを数えると、
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ wc -c 3
13296 3
うーむ、デカい!
まあ、考えてみると、一応 gcc の出力をそのまま使う必要もないんだよな
https://www.muppetlabs.com/~breadbox/software/tiny/teensy.html に頼って、もっと小さい実行形式を出力できるようなお膳立てをしてからじゃないと step 1 にふさわしくない気がする
というか、reproducible なビルドになるようにしないといけないんだよな
……よし、こうするか。
  1. nasm かなんかで小さな .asm をコンパイルすることで、我々が目指すべき「動く小さい ELF バイナリ」を得る
  2. それはそれとして、我々が作るのは C コンパイラなので、常に C 言語(のようなもの)を入力として取る

2022年10月3日(yukata_yu との会話)

ゆかたゆ
ゆかたゆ
-O1 を使わないのですか? (エイリアスなんでしたっけ)
あー, -Osがあるんだ…
hsjoihs
hsjoihs
-O1 を導入せず、gcc を消し飛ばして nasm で作るようにします
ゆかたゆ
ゆかたゆ
なるほ
hsjoihs
hsjoihs
-Os を使うと、3 + 4 - 2 とかが畳み込まれちゃう気がするので
まあ一応 -Os でどれくらい減るのかも実験しておくか。そもそも nasm にする予定だけど。CFLAGS=-std=c17 -Wall -s -Os で試すと、
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ make 3
cc -std=c17 -Wall -s -Os    3.c   -o 3
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ make 42
cc -std=c17 -Wall -s -Os    42.c   -o 42
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ wc -c 3
14328 3
はい、ということで nasm 使います

2022年10月3日(一人で作業)

hsjoihs
hsjoihs
とりあえず、原典にこうあるけど、
  ; tiny.asm
  BITS 32
  GLOBAL _start
  SECTION .text
  _start:
                mov     eax, 1
                mov     ebx, 42  
                int     0x80
64 ビット環境だし、この BITS 32 を削って実行してみるか。Makefile はこんな感じ
tiny42: tiny42.asm
	nasm -f elf tiny42.asm
	gcc -Wall -s -nostdlib tiny42.o -o tiny42
make tiny42 をすると~?
nasm -f elf tiny42.asm
gcc -Wall -s -nostdlib tiny42.o -o tiny42
/usr/bin/ld: i386 architecture of input file `tiny42.o' is incompatible with i386:x86-64 output
collect2: error: ld returned 1 exit status
make: *** [Makefile:12: tiny42] Error 1
はい。正直そんな気はしてた
普通に gcc 付属のアセンブラを使ってみるか
.globl _start
_start:
	movl	$1, %eax
	movl	$42, %ebx  
	int		$0x80
に対して
tiny42: tiny42.s
	gcc -Wall -s -nostdlib tiny42.s -o tiny42
をかませてやると、まあちゃんと動く。しかし wc -c tiny42 すると 13024 tiny42 なので、結局問題が解決してないんだよな
食事から戻った。やっていき
nasm -felf64 hello.asm && ld hello.o && ./a.out
でよいとのこと。なるほど、やってみるか
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ make tiny42
nasm -felf64 tiny42.asm
ld tiny42.o -o tiny42
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ ./tiny42
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ echo $?
42
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ wc -c tiny42
4672 tiny42
まあこんなもんか。これ以上削るのはやりすぎな気もする
一応マニュアル https://www.nasm.us/doc/ を見る。あ、--reproducible があるのか、都合がいい、つけておこう
おや、動かない。このバージョンの nasm にはないのかな
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ nasm --version
NASM version 2.14.02
マニュアルは version 2.15.05 となっている。この --reproducible、なんと version 2.15.05 で加わった最新の機能だそうだ。すばらしい
手元の WSL 2 上の Ubuntu でも sudo apt-get update からの sudo apt-get -y install nasm をすれば nasm が最新になってくれたりしないかな
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ sudo apt-get -y install nasm
Reading package lists... Done
Building dependency tree       
Reading state information... Done
nasm is already the newest version (2.14.02-1).
The following package was automatically installed and is no longer required:
  libfwupdplugin1
Use 'sudo apt autoremove' to remove it.
0 upgraded, 0 newly installed, 0 to remove and 44 not upgraded.
残念。じゃあ自分で入れるしかないか
とりあえず https://www.linuxfromscratch.org/blfs/view/svn/general/nasm.html に従って入れて……
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ nasm --version
NASM version 2.15.05 compiled on Oct  3 2022
よし。ではいよいよ
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ make tiny42
nasm -felf64  --reproducible tiny42.asm
ld tiny42.o -o tiny42
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ make tiny3
nasm -felf64  --reproducible tiny3.asm
ld tiny3.o -o tiny3
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ ./tiny42; echo $?
42
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ ./tiny3; echo $?
3
これは、やったか?
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ cmp -l ./tiny42 ./tiny3
4103  52   3
4185  21  20
4209  14  13
4233  30  27
4257  37  36
4286  64  63
4287  62  56
4288  56 141
4289 141 163
4290 163 155
4291 155   0
4292   0 137
4294 137 142
4295 142 163
4297 163 137
4298 137 163
4299 163 164
4300 164 141
4301 141 162
4302 162 164
4303 164   0
4304   0 137
4305 137 145
4306 145 144
4307 144 141
4308 141 164
4309 164 141
4310 141   0
4311   0 137
4312 137 145
4313 145 156
4314 156 144
4315 144   0
4317   0  56
4318  56 163
4319 163 171
4320 171 155
4321 155 164
4322 164 141
4323 141 142
4324 142   0
4325   0  56
4326  56 163
4327 163 164
4328 164 162
4329 162 164
4330 164 141
4331 141 142
4332 142   0
4333   0  56
4334  56 163
4335 163 150
4336 150 163
4337 163 164
4338 164 162
4339 162 164
4340 164 141
4341 141 142
4342 142   0
4343   0  56
4344  56 164
4345 164 145
4346 145 170
4347 170 164
4348 164   0
4577  44  43
4633 334 333
んー。ああそうか、ld 側にも reproducible にしてくれと頼まないと
……軽く調べたが、頼み方がよくわからん。リンカを gold にしたらバージョン名を埋め込んできたし。んー、1 バイトのズレはファイル名『tiny3.asm』『tiny42.asm』の差に起因してるっぽいし、そこを strip かなんかで処理すれば解決するかも?
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ make tiny42
nasm -felf64  --reproducible tiny42.asm
ld tiny42.o -o tiny42
strip tiny42
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ make tiny3
nasm -felf64  --reproducible tiny3.asm
ld tiny3.o -o tiny3
strip tiny3
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ ./tiny42; echo $?; ./tiny3; echo $?
42
3
hsjoihs@LAPTOP-BKHPSENK:~/c_to_elf_compiler/experiment$ cmp -l ./tiny42 ./tiny3
4103  52   3
よし、差分が 1 バイトになった!!!!!
ここで 52 って出てるのは cmp コマンドがバイトを 8 進数で出力する仕様になっているからであって、これは 10 進数だと 42 なのでこれで正解です
よーし、やっとスタート地点に立つことができた
fn main() -> std::io::Result<()> {
    let input = std::env::args().nth(1).expect("入力が与えられていません");
    let input: u8 = input.parse().expect("入力をパースできません");
    let tiny_3 = include_bytes!("../experiment/tiny3");
    let tiny_42 = include_bytes!("../experiment/tiny42");
    assert_eq!(tiny_3.len(), tiny_42.len());

    let file = std::fs::File::create("a.out")?;

    {
        use std::io::Write;
        let mut writer = std::io::BufWriter::new(file);
        for (index, byte) in tiny_3.iter().enumerate() {
            if *byte == tiny_42[index] {
                writer.write_all(&[*byte])?;
            } else if *byte == 3 && tiny_42[index] == 42 {
                writer.write_all(&[input])?;
            } else {
                panic!("`../experiment/tiny3` と `../experiment/tiny42` の間に非自明な差分が見つかったので、なにを出力すべきか分かりません")
            }
        }
    }
    Ok(())
}
これで、step 1 達成!

2022年10月15日

hsjoihs
hsjoihs
そういえば、あの実行形式まで作る c-to-elf-compiler
あれステップの偶奇で交互にやりません?
tkr
tkr
あそこから先ってどうやるつもりなんです?
hsjoihs
hsjoihs
いまは写経させてるわけですけど、ステップが進むごとに、その写経してる実行ファイルの内容を少しずつ学ばないといけない
逆に、学ばなくていいところは未来に先送りする
tkr
tkr
そこらへんの仕様って……ネットで調べれば落ちてるか
hsjoihs
hsjoihs
それを調べてログに書くのもステップの一部とする、という方針で考えてます
yuchiki
yuchiki
教育的授業だ
tkr
tkr
自分でバイナリを吐き出したい、と
hsjoihs
hsjoihs
つまり、要は作る側が頑張らないことで、読む側も頑張らなくて済む。少しずつ、ELF の中身を謎解きしていく追体験ができる
tkr
tkr
まあたしかにたしかに
hsjoihs
hsjoihs
というのを考えています。ということで、ステップ 2 『足し算と引き算』やってみません?
tkr
tkr
足し算と引き算をやって、終了コードで返す
hsjoihs
hsjoihs
そうそう
tkr
tkr
結局 main 関数からリターンするだけか
hsjoihs
hsjoihs
実はですね、今の実装は(ログに書いてありますが)_start なので、リンクせずに ebx レジスタに値をセットして、残りを OS に任せてる
これにより、リンクが要らない
tkr
tkr
musl と crt とリンクさせる方針じゃなくても直接吐けばいいのか
hsjoihs
hsjoihs
いずれは C コンパイラになることが期待されているので、いずれは関数 main や関数 foo が宣言できるようになる予定だけど、ステップ 1 を実現するのにはどう考えてもいらないので、したがって省いている
そう、要はなにかというと、この方法を取ることによって、まず謎解き感覚になって面白い。次に、『調べすぎちゃいけない』という縛りにしておくことで、我々ともに下手するとコンパイラにかまけて授業とか研究の手を抜きそうな民なので、それをある程度抑止できる
tkr
tkr
なるべくラクしてバイナリを吐こう、ってことか
hsjoihs
hsjoihs
そうそう。要は compilerbook ってかなりよく書かれた教材なので、逆に言うとあの本をそのまま再走してもあんま面白くない。私に至っては 4 年前にやったし
となると、こういう企画にするのが、『我々にとっての面白さ』『我々にとっての勉強になる度』『学校・趣味バランス』の 3 点において優れている、と主張します
tkr
tkr
前は gcc のコンパイル結果を比較してた?
hsjoihs
hsjoihs
実はリポジトリに会話ログを全部取ってあって、リポジトリが https://github.com/sozysozbot/c_to_elf_compiler 、ログが https://sozysozbot.github.io/c_to_elf_compiler/log.html
tkr
tkr
なんか招待来てたな
hsjoihs
hsjoihs
じゃあ再送しときます
tkr
tkr
受け取った気がする
足し算ステップってなにやるんでしたっけ
hsjoihs
hsjoihs
3+5-2とかをコンパイルする
tkr
tkr
スタックは使わないやつだった気がする
hsjoihs
hsjoihs
そう
構成としては、最初はマジでわけが分からずバイナリをただ写経してただけだったのが、ステップを進めていくうちに『これを実装するには、自分の理解を増やすことを強いられるな……』が積み重なって、最終的にアセンブラとリンカが勝手に生えてくる
tkr
tkr
ELF の数値の表現ってどうでしたっけ。127 以下だとどうこう、だっけ
hsjoihs
hsjoihs
最初は小さい数だけが動けばよし!
というのが、そもそも Rui 本のスピリット
tkr
tkr
たしかに
なんて名前でしたっけ、可変長の表現の名前。そうだleb128だ
hsjoihs
hsjoihs
そういう話も、調べて、メモって、『ここ対応できるようにしたいなぁ』となったときに初めて書く、という方針になるわけですね
あと言うべきこととしては、とりあえずリポジトリのドキュメンテーションはそれなりには書いたので、読めば分かる
tkr
tkr
開発環境は?
hsjoihs
hsjoihs
Ubuntu on WSL
tkr
tkr
WSL 上じゃないと開発できないようになってそう
hsjoihs
hsjoihs
正解のバイナリを吐くのに使ってるアセンブラ nasm が、比較的最近 --reproducible ってオプションを実装してくれたので、LTS の Ubuntu で apt で入るやつにはまだこのオプションがなく、自分で configure して make する必要がある
tkr
tkr
nasm でしたっけ
hsjoihs
hsjoihs
実をいうと、nasm が動かなくても、nasm が生成してくれたバイナリは『コミットしてある』ので、なくても我々の c-to-elf コンパイラは走る
あと、ログのレンダリングは私が雑に実装したレンダラでやってて、CI のセットアップしてないので、
tkr
tkr
勝手にはされない、と
hsjoihs
hsjoihs
node index.js をコミット前にする必要がある
tkr
tkr
nasm の 2.15 だと動きます?
hsjoihs
hsjoihs
自信ないけど 2.16 以降が必要だったかな

2022年10月16日

tkr
tkr
加減算の実装
`experiment/tiny3` のサイズが4kを超えていて機械語どころかアセンブリも詳しくない人には厳しい。
以下プログラムで127バイトのelfを生成して実行すると終了コード0を出力することを確認。https://github.com/tchajed/minimal-elf
`31 ff (xor %edi,%edi`) を `6a 01 (push $0x01) 5f (pop %edi)` にバイナリエディタで書き換えると終了コードが1になることを確認。アドレス `0x78` 以降を書き換えれば加減算の実装は簡単にできそうだ。
objdumpコマンドでmovやaddやsubのバイナリ表現を調べて実装終わり

2022年10月17日

hsjoihs
hsjoihs
えーと次にやるべきは……『ステップ3:トークナイザを導入』か。すぐ終わりそう
check 2 "5 - 3" を走らせ、テストが無事落ちることを確認。
トークナイザをサクッと書いて、挿げ替えて、テストが通ることを確認。

2022年10月27日

tkr
tkr
エラーメッセージを改善するだけ
エラー型を定義して関数分割して実装。分岐を少なくするためにtokenにeofを追加

2022年10月29日(一人で作業)

hsjoihs
hsjoihs
tkr からプルリクが来ておる。コードを読んだ。なるほどね。マージ

2022年10月29日(Discord にて)

hsjoihs
hsjoihs
とまあこういうことをやってるんですよ
hiromi_mi
hiromi_mi
おもしろかったです、ushitora_anqouと違ってELFファイルを出力できるような状態をたもちつつ拡張するなど、アイデアに一本取られました
hsjoihs
hsjoihs
ふむふむ。わりと自然な発想のエンコーディングですね

2022年10月29日(一人で作業)

hsjoihs
hsjoihs
さてやっていこう。compilerbook のステップ 5 には
説明の都合上、一気に*、/、()を実装しているような話の流れになっていますが、実際には一気に実装することは避ける方がよいでしょう。 元々、加減算ができる機能があったわけですから、まずはその機能を壊さずに、抽象構文木とそれを使ったコードジェネレータを導入するようにしてみてください。 そのときには新たな機能を足すわけではないので、新しいテストは必要ありません。その後に、*、/、()を、テスト込みで実装していってください。
と書いてあるので、まずは抽象構文木を導入すべきなんだな
まずは clippy の pedantic でも足して、
あとそろそろ tokenize を別ファイルに分離するか
apperror.rs と tokenize.rs を分離

2022年10月30日(一人で作業)

hsjoihs
hsjoihs
そして抽象構文木を導入
さて、コードジェネレーターでこれを使わなきゃいけないんだよな

2022年10月30日(Discord にて)

hsjoihs
hsjoihs
とまあこういうことをやっています
ikubaku
ikubaku
実行形式の中でも ELF にしてるのってどういう理由です?ELF はそれなりに複雑であり、難易度が上がる気がしていますけど
hsjoihs
hsjoihs
一番ポピュラーであり、私の WSL 環境で動くからです。あと、その複雑さを上手く回避する小技を導入しています(step 1 をどう実装したかを見せる)
hiromi_mi
hiromi_mi
最近の Linux では a.out フォーマットのサポートが消えたので、ELF を選択するという判断は妥当そうに思えます。Windows の com も今はダメですし

2022年10月30日(コードジェネレーター)

hsjoihs
hsjoihs
抽象構文木を導入したことにより、許容される式の自由度が真に上がったので、コードジェネレーターを書き換えねばならない。
……よな?ここをちゃんと確認しないと『必要最低限のみを調べる』という縛りに抵触してしまう。
うむ、一方向にのみ伸びているとは限らない木を処理できるようにするには、スタックマシンの仕組みを導入しないと無理ですな
えーと tkr の書いたバイナリ表現を読まないとリファクタリングできない。よって調べる
https://defuse.ca/online-x86-assembler.htm に投げたところ、bf ** 00 00 00 は edi レジスタに ** をセットするらしい。じゃあ 83 c7 ** は edi を増やして 83 ef ** は edi を減らすのでしょう
とはいえ、我々は「なるべくアセンブリ言語について知らないようにしている」という設定なので、これに mov とか add とか名前を付けるのでは面白くない
せっかく Rust が識別子に日本語を許すようになったのだし、『edi増加』『edi減少』とかの関数名にしてしまえ
えーと、スタックマシンにする必要がある。そのためにはどのような機械語が必要だろうか。レジスタとメモリの間で色々移動させる必要がありそうだな
『即値をスタックにpush』『レジスタをスタックにpush』『スタックからレジスタへとpop』『レジスタの中身を、別のレジスタに足し合わせる』があればいい
tkr が既に「6a 01 (push $0x01)」「5f (pop %edi)」というのを調べてくれている。えーと、
☑ 即値をプッシュ ☒ ediをプッシュ ☑ ediへとポップ ☒ eaxへとポップ ☒ ediにeaxを足し合わせる ☒ ediからeaxを減じる
ということで、あとは残り 4 つを調べればよい
fn 即値をプッシュ(n: u8) -> [u8; 2] {
    [0x6a, n]
}

fn ediをプッシュ() -> [u8; 1] {
    [0x57]
}

fn ediへとポップ() -> [u8; 1] {
    [0x5f]
}

fn eaxへとポップ() -> [u8; 1] {
    [0x58]
}

fn ediにeaxを足し合わせる() -> [u8; 2] {
    [0x01, 0xc7]
}

fn ediからeaxを減じる() -> [u8; 2] {
    [0x29, 0xc7]
}
あとはこれらを組み合わせれば動くはず。これでいいかな
fn exprを評価してediレジスタへ(writer: &mut impl Write, expr: &Expr) {
    match expr {
        Expr::BinaryExpr {
            op: BinaryOp::Add,
            op_pos: _,
            左辺,
            右辺,
        } => {
            exprを評価してediレジスタへ(writer, 左辺);
            writer.write_all(&ediをプッシュ()).unwrap();
            exprを評価してediレジスタへ(writer, 右辺);
            writer.write_all(&ediをプッシュ()).unwrap();
            writer.write_all(&eaxへとポップ()).unwrap();
            writer.write_all(&ediへとポップ()).unwrap();
            writer.write_all(&ediにeaxを足し合わせる()).unwrap();
        }
        Expr::BinaryExpr {
            op: BinaryOp::Sub,
            op_pos: _,
            左辺,
            右辺,
        } => {
            exprを評価してediレジスタへ(writer, 左辺);
            writer.write_all(&ediをプッシュ()).unwrap();
            exprを評価してediレジスタへ(writer, 右辺);
            writer.write_all(&ediをプッシュ()).unwrap();
            writer.write_all(&eaxへとポップ()).unwrap();
            writer.write_all(&ediへとポップ()).unwrap();
            writer.write_all(&ediからeaxを減じる()).unwrap();
        }
        Expr::Primary { val, pos: _ } => {
            writer.write_all(&ediに代入(*val)).unwrap();
        }
    }
}

fn parse_and_codegen(
    mut writer: &mut impl Write,
    tokens: &[Token],
    input: &str,
) -> Result<(), AppError> {
    let expr = parse(tokens, input)?;

    let tiny = include_bytes!("../experiment/tiny");
    writer.write_all(&tiny[0..0x78]).unwrap();
    writer.write_all(&[0xb8, 0x3c, 0x00, 0x00, 0x00]).unwrap();

    exprを評価してediレジスタへ(&mut writer, &expr);

    writer.write_all(&[0x0f, 0x05]).unwrap();
    Ok(())
}
走らせてみよう
[FAIL] 1+2 => 3 expected, but got 139
なるほど落ちる。あー、多分 eax レジスタをなんか別ので使ってるんだろうな。0xb8, 0x3c, 0x00, 0x00, 0x00 辺りかな
じゃあそれを『exprを評価してediレジスタへ』より後ろに回すと……動いた。

2022年10月30日(乗算)

hsjoihs
hsjoihs
次は乗算を組んでいこう。まず構文木に BinaryOp::Mul を加える。対応するバイナリ表現も調べる
fn ediをeax倍にする() -> [u8; 3] {
    [0x0f, 0xaf, 0xf8]
}
次に、再帰下降パーサにする。前に自分で書いた Rust での再帰下降を眺めよう
再帰下降パーサというのは、『読めるところまで読み、それ以外は読み残す』という挙動をする部品を組み合わせることで上手くいく
ということで、とりあえずイテレータを取るような関数へと変更。peekable もほしいかな
そして parse_multiplicative 関数を実装し、parse の中ではそれを呼ぶようにする
さて 3*4-5 が動くかな?おっと Mac ではそもそもどのテストケースも通らない。WSL で試すか
ああ普通に 1+2 が落ちてる。それはそうで、せっかく peekable にしたのに peek していない
peekするようにすると……おっと『演算子かeofが期待されていますが、数か * が来ました』と言われる。ああここも peek にしないとね。あと第一式も parse_multiplicative にしないと
修正したら通った。"9 *8 - 7* 6 + 5* 4*1" とかも通りますね
次はカッコを実装しよう。そのためには、
  • 数値リテラルを読むところを parse_primary 関数に切り出す
  • 『全部読めていない場合に落とす』の部分を parse 関数から切り出す
  • カッコで括った式を primary と見なす
という変更を加えることになる。とりあえず最初の二つをやり、テストが通ることを確認
次はトークナイザへのカッコの追加だ。正直 paren, sqbracket, brace とかより『開き丸括弧』の方が読みやすくない?バンバン日本語識別子を使っていこう
そうしたら parse_primary の中で parse_additive を呼んでやって、テストを書く
えーと "5*(6-3)*7" が「演算子の次に来ているものが数値ではありません」で落ちる
ああ、parse_multicative の右辺の方を parse_primary にしていなかったな。直した。テストが通る
残るは除算の実装だけ。これ面倒なんだよな。compilerbook にはこう書いてある
特にパースやコード生成において重要なポイントではないのですが、トリッキーな仕様のidiv命令が上のコードでは使われているので、それについて説明しておきましょう。 idivは符号あり除算を行う命令です。x86-64のidivが素直な仕様になっていれば、上のコードでは本来idiv rax, rdiのように書きたかったところですが、そのような2つのレジスタをとる除算命令はx86-64には存在しません。その代わりに、idivは暗黙のうちにRDXとRAXを取って、それを合わせたものを128ビット整数とみなして、それを引数のレジスタの64ビットの値で割り、商をRAXに、余りをRDXにセットする、という仕様になっています。cqo命令を使うと、RAXに入っている64ビットの値を128ビットに伸ばしてRDXとRAXにセットすることができるので、上記のコードではidivを呼ぶ前にcqoを呼んでいます。
ということで、足すべきは以下の通り
fn eaxの符号ビットをedxへ拡張() -> [u8; 1] {
    [0x99]
}

fn edx_eaxをediで割る_商はeaxに_余りはedxに() -> [u8; 2] {
    [0xf7, 0xff]
}

fn eaxをプッシュ() -> [u8; 1] {
    [0x50]
}
テストケースも足したし、これで『ステップ5:四則演算のできる言語の作成』が完了

2022年10月31日(Discordにて)

hsjoihs
hsjoihs
コードレビューお願いします
tkr
tkr
diff がでかくて見るのが大変で放置してた
hsjoihs
hsjoihs
まあ再帰下降にする過程で全部書き直す羽目になりますからねぇ
tkr
tkr
わかるなぁ
そういえば、ログのページを生成するのも CI にしたい。えーと今は master ブランチの上の GitHub Pages を直に公開してるのか
hsjoihs
hsjoihs
じゃあ step 8 の代わりに CI の整備とかお願いしていいですかね。本来の step 8 はコンパイラのソースコードをファイル分割して Makefile をセットアップするプロセスなんですけど、Rust で書いている我々にとっては既にそれは解決されているので

2022年10月31日(GitHubにて)

tkr
tkr
これposどこにするか迷う 🤔
エラーメッセージ的にはここが正しそうだけど「unexpected token」的なエラーメッセージとどっちが分かりやすいんだろう
                _ => Err(AppError {
                    message: "この開き丸括弧に対応する閉じ丸括弧がありません".to_string(),
                    input: input.to_string(),
                    pos: *pos,
というのが気になったけど、大体よさそう
hsjoihs
hsjoihs
まあ理想的には Rust みたいに「範囲」でエラーを持つ方がきれいですよね。まあ Rui 本はあんまりエラーに凝らない設計なので、あんま深く考えなくていいでしょう
tkr
tkr
あとは Peekable 使うかスライス使うかとか迷うなぁ
パーサのファイル分割とかはstep8?でやるかー

2022年11月7日

tkr
tkr
そろそろやらなければ
単項演算子の実装はパーサー少し変更するだけっぽいので一瞬で終わった
色々テスト書いて終わり
今後 `-"x"` みたいな型エラー発生するコードを書いたときにエラーメッセージが微妙に分かりにくくなるのが気になるが

2022年11月9日

hsjoihs
hsjoihs
やるぞ
比較演算子を実装。ここら辺は本当にやるだけだな
関数が長くなりすぎて clippy が文句を言ってきているな。でもこれ過剰に分割しても読みやすくならないだろうし、#[allow(clippy::too_many_lines)] でお茶を濁そう

2022年11月10日

tkr
tkr
CIの整備をします
github pagesの設定を修正してactionsからのアップロードみたいなのに変更する必要があるけど権限ないのでお願いします
やったことはコミットメッセージに

2022年11月11日(CIの修正)

hsjoihs
hsjoihs
tkr からプルリクが来ておるな
CIの整備と、ファイル分割がされている。ありがたい。まずマージして、
設定を変更して、えっと
あーあれか、マージしてから設定を変更したせいで、この版まではデフォルトのgithub-pages アクションに基づいて生成されているのか
じゃあ、もう一回コミットしたら tkr の書いた GitHub Action が走ってくれる?
んー、Workflow details will appear here once your site has been deployed. と言われる
Your site was last deployed to the github-pages environment by the pages build and deployment workflow. とのことだから、設定できてないな
If you already have a workflow to publish your site, you can skip this step. GitHub Pages does not associate a specific workflow to the GitHub Pages settings. However, the GitHub Pages settings will link to the workflow run that most recently deployed your site.
とのこと。つまり、Source: GitHub Actions とだけ出てそれ以上設定する画面がないという現状で問題ないっぽい
The general flow of a workflow is to: 1. Trigger whenever there is a push to the default branch of the repository or whenever the workflow is run manually from the Actions tab. 2. Use the actions/checkout action to check out the repository contents. 3. If required by your site, build any static site files. 4. Use the actions/upload-pages-artifact action to upload the static files as an artifact. 5. If the workflow was triggered by a push to the default branch, use the actions/deploy-pages action to deploy the artifact. This step is skipped if the workflow was triggered by a pull request.
と書いてある。tkr の書いてくれた workflows/docs.yaml には actions/checkout と actions/upload-pages-artifact が言及されている
ふむ、The starter workflows use a deployment environment called github-pages. If your repository does not already include an environment called github-pages, the environment will be created automatically. と書いてある。名前を変えてみるか
うまくいかない。 actions/deploy-pages action を足してみるか
Error: Error message: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable at Function.<anonymous> (/home/runner/work/_actions/actions/deploy-pages/v1/webpack:/deploy-pages/node_modules/@actions/core/lib/oidc-utils.js:71:1) at Generator.next (<anonymous>) at /home/runner/work/_actions/actions/deploy-pages/v1/webpack:/deploy-pages/node_modules/@actions/core/lib/oidc-utils.js:8:1 at new Promise (<anonymous>) at __webpack_modules__.8041.__awaiter (/home/runner/work/_actions/actions/deploy-pages/v1/webpack:/deploy-pages/node_modules/@actions/core/lib/oidc-utils.js:4:1) at Function.getIDToken (/home/runner/work/_actions/actions/deploy-pages/v1/webpack:/deploy-pages/node_modules/@actions/core/lib/oidc-utils.js:57:1) at Object.<anonymous> (/home/runner/work/_actions/actions/deploy-pages/v1/webpack:/deploy-pages/node_modules/@actions/core/lib/core.js:315:1) at Generator.next (<anonymous>) at /home/runner/work/_actions/actions/deploy-pages/v1/webpack:/deploy-pages/node_modules/@actions/core/lib/core.js:27:1 at new Promise (<anonymous>) Error: Ensure GITHUB_TOKEN has permission "idToken: write".
うーむ。とりあえず上手くいっていないので、せっかくバージョン管理されていることだし、上手くいっていたところまで強引に世界を差し戻すか?
いや、https://github.com/jurliyuuri/cerke_online_alpha/blob/master/.github/workflows/deploy.yml のを転用して、gh-pages ブランチからページを生やすようにしよう
The workflow is not valid. .github/workflows/docs.yaml (Line: 35, Col: 14): Unexpected value '' .github/workflows/docs.yaml (Line: 36, Col: 9): Unexpected value 'github_token'
それはそう。え、でも cerke_online_alpha には secret が設定されていないと書いてあるけど
For newbies of GitHub Actions: Note that the GITHUB_TOKEN is NOT a personal access token. A GitHub Actions runner automatically creates a GITHUB_TOKEN secret to authenticate in your workflow. So, you can start to deploy immediately without any configuration.
あ、そうなの。じゃあなんで落ちてるんだろうか
https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-first-deployment-with-github_token を読む。なるほど、初回は失敗することが想定されているのか。へー
とりあえず git checkout --orphan gh-pages をして push することで gh-pages ブランチを作るか
えーとインデントが壊れている。直そう
ジョブは走っているが、ページが出力されない。.nojekyll ってファイルだけが生えておる
ああ、publish_dir がミスってるのか
直したら直った!URL も今までと変わらず提供できている。勝利ですね。リポジトリの README も直しておくか

2022年11月11日(一文字変数)

hsjoihs
hsjoihs
さて、次は複文と一文字変数
そういえば、compilerbook では『一番最後の式の結果をプログラム全体の計算結果とすることにします』として、構文定義がこうなってるけど、
program = stmt* stmt = expr ";" expr = assign assign = equality ("=" assign)? equality = relational ("==" relational | "!=" relational)* relational = add ("<" add | "<=" add | ">" add | ">=" add)* add = mul ("+" mul | "-" mul)* mul = unary ("*" unary | "/" unary)* unary = ("+" | "-")? primary primary = num | ident | "(" expr ")"
このプロジェクトは Rust で書いてるんだし、Rust と同様に『文の列の最後に式を書くと、それが戻り値になる』で組もうかな
こうすることの利点としては、
  • 今までのテストケースに手を入れなくて済む
  • スタック操作をミスってバグが出たときに、より早期に気づきやすい(Cコンパイラ班の受講生で、ここの周りで混乱してバグらせた人を見た)
  • 実はセミコロンを中置演算子として見ることができ、既存の再帰下降のコピペで実装が終わる
などが挙げられそう
ところで、関係ないけど、このログを生成する自作ツール PseudoRoku に箇条書き機能欲しくなってきたな。$HTML コマンドに <ul> を食わせて1行ずつで書かなきゃいけないのが面倒になってきた
さて、とりあえずトークナイザに一文字変数を足さねば。あと Assign 演算子も足そう。右結合だから、ループにせずとも再帰で十分
いや、まずは最小限の変更で動かしたいから、最初はセミコロンだけだな。AndThenという二項演算子として処理して、
        Expr::BinaryExpr {
            op: BinaryOp::AndThen,
            op_pos: _,
            左辺,
            右辺,
        } => {
            exprを評価してediレジスタへ(writer, 左辺); // 左辺は push せずに捨てる
            exprを評価してediレジスタへ(writer, 右辺);
        }
これだけで動くはず。便利
次に、一文字変数と代入が必要。'a' から 'z' および '=' をトークナイズし、パーサーを増やす。parse_primary で識別子を読むコードも必要
あとはコード生成を書くだけ。今回の実装では return しないので、エピローグは不要で、プロローグで変数 26 個分の領域を確保すれば十分
おや、パースが通らない。なるほど、再帰下降のコピペミスか。直した。しかし Mac だとテストケースごとに docker run するから遅いな。しょうがないけど
直したらパースが通るようになったが、`a = 3; b = 4; a + b` で `7 expected, but got 139` が出ている。なんかミスっておるな
`a = 7; a` でも同じ。ところで、この手のミスをすると毎回 139 が来る気がするけど、これなんなんだろう
あーわかった。コメントアウトしてた『ediから即値を引く』関数をそのまま使ってたけど、これは『edi から即値を引く』から、rdi の上位 32 ビットがクリアされちゃってるんだな
ということで、
fn rdiから即値を引く(n: u8) -> [u8; 4] {
    [0x48, 0x83, 0xef, n]
}
にしたら……動いた!
今までは意図的に 32 ビットレジスタと 64 ビットレジスタを混同するような書き方をしてきたけど、そろそろちゃんと分ける必要が出てきますわね
あ、CI で cargo fmt が要求されてる。掛けておくか
そういえば、せっかく `a = b = 7` みたいなやつに対応してあるんだから、テストケースに足しておこう

2022年11月21日

hsjoihs
hsjoihs
そういやこの 139 ってなんなんですかね。SIGSEGV のシグナル自体は 139 ではなかった気がするんですけど
あー、シグナルが 11 で exit code が 139 か
hiromi_mi
hiromi_mi
『なんらかの異常値である』必要はそりゃあると思いますけど、具体的になんで 139 なんですかね
hsjoihs
hsjoihs
あ、128 + 11 ってことなのかな

2022年11月24日

tkr
tkr
複数文字のローカル変数
Rustなので `HashMap` 使ってまあ適当に
変数にアンダースコアとか数値も使えるように
ローカル変数情報のバケツリレーつらいからそろそろcodegenをstructにしてもいいかもな
compilerbookだとこのステップ終わっても26個しか変数使えなくない?この制限解除しようとすると `rspから即値を引く` の段階では変数の数決まらないからきついな。とりあえず `Write + Seek` にするか
hsjoihs
hsjoihs
tkr からのプルリクを読みます
読んだ。マージした。
さて、今回は return 文なわけで、これによって既存のテストケースを変更する必要がある
-   a = b = 7; a 
+   a = b = 7; return a;
みたいな変更を全部に入れる必要があり、とりあえずそれのコンパイルを通す必要がある
まあ、とりあえずは return キーワードをトークナイザに足すところからですな
一瞬で足せた。payload をすり替えるだけだな。で、次にパーサーに手を加える必要がある。今まではセミコロンを 2 項演算子として扱っていたので、それを修正する必要がある
まず、Expr と Program を分離していく。定義を複製してすり替えることで、とりあえず動くものを作る
次に、Statement 型を立てていく。parse_statement を立て、それの繰り返しにより parse_program を定義する。Program は Vec<Statement> とする
えーっと、今回は return してるわけじゃないんだよな。とはいえ syscall したら帰ってこないから、普通にエピローグを出力すればいいのか
これはテストケースにもう return を足して、return 文も実装してしまおう
えーと通らない。『数値リテラルでも開き丸括弧でもないものが来ました』というメッセージ。多分再帰下降のどこかで「エラーを出さずにスルー」を忘れている
いや違うな。eof トークンがあるから while tokens.peek().is_some() ではダメなんだ。
直したら動いた
よしプルリクエストを作成。あ、テストケースを増やした方がいいな

2022年12月3日

tkr
tkr
とりあえずトークナイザ実装
jmp/jeのバイナリ表現を調べたら0xEB バイト数/0x74 バイト数であることが分かった。前方ジャンプは負の数かつジャンプ命令自体のサイズを含むので-2となることに注意。

2022年12月4日

tkr
tkr
コード生成のインターフェイスをもう少し整備したい。jmp命令という中間の命令列のバイト数情報が必要なコードの生成を考えるとWrite + Seekを持ち回すのは限界を感じる。RopeっぽいBufを実装する
parser実装。forの初期化部はint i = 0のような変数定義が許されるので厳密には式ではないけど今の仕様だと定義なしで代入すると変数が作られる仕様なのでとりあえず式でいいか
前回書いたSeekを使う汚いコードをBufを使って書き直した

2022年12月5日

tkr
tkr
ifとwhileのコード生成は前回の調査結果から実装。forはwhileのsyntax sugarとして実装
hsjoihs
hsjoihs
見ていくか
statement が statment になってる誤字があるのと、そもそも関数「statementを評価してediレジスタへ」って値を「ediレジスタへ」確実に書いてくれるんでしたっけ?
  • まずediレジスタへ確実に書いてくれるかわからん
  • 書いてくれていたとしても、「edi レジスタに書きこんでいる」を前提としたコードは文のレベルでは不存在っぽそう?
ということで、関数名だけ変更してマージしておきますね
あ、clippy が Buf に Default とか is_empty を足せと言ってくるので一応足しておく
マージしたので、step 13 をやっていく。ブロックか。簡単だ
波括弧をトークナイザに追加し、パーサで読み、ああコードジェネレータにはもう既にあるのか(for を while に書き換える際に必要であったため)。
ということで、かなり少ない作業量でブロックの実装が完了。dangling else のテストケースも追加しておこう。よし通った

2022年12月7日

tkr
tkr
step14のテストどうしましょう。本だとオブジェクトファイルを生成して自作関数とリンクすることになっていますが、、
hsjoihs
hsjoihs
リンカ実装は明らかに荷が重いので、なんらかの syscall を呼ぶ関数を最初の魔法の 78 バイト付近に埋め込んで、__builtin() でその関数をコールできるようにする、でどうでしょう?
tkr
tkr
たーしかに…とりあえずビルドイン関数でなんとかしますかー。グローバル変数はまだまだ先だったしそれが楽そう
hsjoihs
hsjoihs
まず __builtin_three()(3 を rax に入れて ret するだけ)を実装し、次に __builtin_putchar(int char) を実装、といった感じかなぁ

2022年12月30日

tkr
tkr
トークナイザは変更しなくていい。パーサーは
hsjoihs
hsjoihs
primary_expression の定義を変えるんだっけ
tkr
tkr
compilerbook はそうしてますね。まずは引数のないやつだけ組むのか
hsjoihs
hsjoihs
__builtin_three がちょうどいいですね
tkr
tkr
パーサーの実装の入れ子が深くなっちゃうな
hsjoihs
hsjoihs
まあ後で浅くなるので気にしなくてよいのでは
tkr
tkr
パーサーはこれでいい。codegen 面倒だなぁ
hsjoihs
hsjoihs
まあね
関数名が __builtin_three じゃなかったらエラー吐いて落ちればいいでしょ
tkr
tkr
codegen ではエラーを吐かない方がいいのでは?
hsjoihs
hsjoihs
関数の不存在はリンクエラーの領分だし、codegen が落ちてもいいでしょ
tkr
tkr
たしかにそうか
えーと jmp が必要で
hsjoihs
hsjoihs
いえ、call です
tkr
tkr
名前では呼べませんよね
hsjoihs
hsjoihs
そりゃあね
tkr
tkr
とりあえずアセンブリを書いてビルドしてみて実践するしかないか
GLOBAL _start
SECTION .text
_test:
        ;mov rax, 3
        ret
_start:
            mov     eax, 1
            call    _test
            mov     ebx, eax
            int     0x80
なんで 0 を返して正常終了するんだろう。3 が返ってきてほしいのに
hsjoihs
hsjoihs
./tiny ではなく ./tiny3 を走らせる必要がありませんか?
tkr
tkr
ほんとだ。そうすると……セグフォ?なぜ
rsp の操作が要りそうだな
hsjoihs
hsjoihs
ん、call はそこら辺ちゃんとやってくれるから要らないのでは
tkr
tkr
pop が要るんじゃね?
hsjoihs
hsjoihs
いや、要らないと思うけどなぁ。gcc で return 3; をコンパイルしたアセンブリはどうなります?
セグフォなんですが、ありそうだと思ってるのが、実はビルドが完成していなくて、本来はリンカが call の対象を直して実行ファイルにするんですけど、その直すのがされていないままの、リンカを通っていないやつを実行してセグフォしているのでは?という気もします
tkr
tkr
Makefile の中で ld 通ってますよ
hsjoihs
hsjoihs
たしかに〜
tkr
tkr
call を jmp にすり替えたらセグフォせずに無限ループになっているので、そこの問題はないと思われますが
hsjoihs
hsjoihs
もう gdb で攻める以外の方法思いつかない
tkr
tkr
使ったことない〜。でも使わないと今後きつそう。まあとりあえず gcc でコンパイルしてみよう
ベースポインタの退避とかしなくていいんですかね。call 命令はやってくれる?
hsjoihs
hsjoihs
call 命令は退避はしてくれないので、呼ぶ側はプロローグ書かないとですね。呼ばれる方の builtin_three は要らない
tkr
tkr
あーじゃあそれのせいだな
hsjoihs
hsjoihs
プロローグは compilerbook にあるのでそれを使えばいいでしょう
tkr
tkr
でも compilerbook はプロローグ無しで call できてそうだけど
hsjoihs
hsjoihs
具体的にどこの?
tkr
tkr
ここですね
このCコードに対応するアセンブリは次のようになります。 .intel_syntax noprefix .globl plus, main plus: add rsi, rdi mov rax, rsi ret main: mov rdi, 3 mov rsi, 4 call plus ret
hsjoihs
hsjoihs
たしかにね。このコードを標準的な gcc でビルドすることによってまず動かして、どこを nasm にすり替えるとこれがこわれるのかを調べるのが近道かな?
tkr
tkr
call 終わった時点ではセグフォ起きてない。これ↓がセグフォせず無限ループしてるので
GLOBAL _start
SECTION .text
_test:
        mov eax, 3
        ret
_start:
            call    _test
a:
            jmp a
            mov     ebx, eax
            int     0x80
なんでこれがセグフォしないんだ?
あ、もしかして int 0x80 が eax に 1 を必要とする?
hsjoihs
hsjoihs
可能性がある。int 0x80 の直前に mov eax, 1 書いたらどうなります?
tkr
tkr
いけた。call のせいじゃなくて syscall の使い方間違えてるだけだったわ
あーそうか、syscall 番号が eax か。0x80 がシステムコール番号なのではなくて
hsjoihs
hsjoihs
私も一時期その勘違いしてた気がするな
tkr
tkr
syscall 命令みたいなのなかったっけ
hsjoihs
hsjoihs
とりあえず動いたのでヨシ
で、結局バイナリを見て call がどう翻訳されてるかを確認したかったんですよね?
GLOBAL _start
SECTION .text
_test:
        mov eax, 3
        ret
_start:
        call    _test
        mov     ebx, eax
        mov     eax, 1
        int     0x80
とりあえずその前に、動いたアセンブリとバイナリをコミットし、
私が書いてるログも入れるとよさそう
tkr
tkr
Makefile いじろう
既存のファイルが .out に変わるけどとりあえず今のところは影響ないか
てかコミットしてるなら clean 自体が要らない気がするな
あ、Makefile のルールをそもそも拡張子なしにできるからそれでいいのか

2023年1月1日

tkr
tkr
Macだとasm->elfの実験ができなかったりテスト遅かったりして開発しにくかったので改善
call命令も相対アドレスなのかなりめんどくさい。jmpは文を先に生成してサイズ見てでよかったけどcallは今まで生成したコードサイズを記録しないといけないからかなり大変
call レジスタ名 と指定すれば絶対アドレスでいけるらしいのでとりあえずこれでいいかな。多分ダイナミックディスパッチと同じで遅いけど
またセグフォー、多分これ__builtin_threeがエントリポイントになってるなうげ〜。出力バイナリの0x18から2バイト(?)をエントリポイントのアドレスに書き換えたらいけるかも
そろそろLEB128の実装が必要そう
mov rax, 120; call rax; (120は__builtin_threeのアドレス)しても必ずraxに120が入ってる。そもそも__builtin_threeが実行されていなさそう。謎
eaxに即値をセットする命令は4byte指定しないといけなかったのと、call命令は0x00400000をelf内のアドレスに足さないといけなかったらしい。gdbの使い方少し覚えた。0x00400000がベースアドレスだったのか

2023年1月2日

hsjoihs
hsjoihs
お、できておる。すごい。えーと clippy が通っていない
なるほど、i32 じゃなくて i8 の to_le_bytes してるところがあるのか。でも呼ばれてないから今の所問題が露呈していない、と
それを直していただいたら、とりあえずこれでマージしちゃって、__builtin_putchar は step14-2 ってブランチでやっちゃいましょう
しかしなんとか突破できて本当によかった。ステップ 14 はだいぶ苦しくなるだろうと思ってたし、クリアがなされたのはめちゃめちゃえらいんだよな
tkr
tkr
そもそもアセンブリもよく知らない状況だったから大変だった
どこらへんでぶっ壊れてるかもわからんから gdb の使い方から調べて。思ったよりハードコアだった。x64 のバイナリフォーマット相当複雑じゃないですか?
hsjoihs
hsjoihs
実はね
tkr
tkr
wasm は本当にラクだったけど、x64 やばすぎる
hsjoihs
hsjoihs
ところで、この追加されてる .nix とか .lock とかについて知るには何を読めばいいですか?
tkr
tkr
Mac での開発用に足しただけなので、Linux でやってるなら気にしなくていいです
hsjoihs
hsjoihs
Mac で使えるのはうれしいので、これがなんであってどううれしいのか、みたいな話をお教えいただけると
tkr
tkr
なにしてたっけな。えっと、bintools とかを使えるようにした。Makefile で、Mac だと x64-linux 向けのリンカや strip コマンドが入っていないので、それを自動で入れて、Mac だったら自動でそれを使ってくれるように。experiment の中の Makefiles で BINTOOLS_PREFIX って書いてあるじゃないですか。ここに Mac だった場合に適切な prefix が入って、これがどこで定義されているかというと、break.nix の下の方に、x64-Linux だったら空、そうじゃない場合に適切な文字列を入れることで、Linux の ld とかが使えるようになって、それらをどこで開発環境にインストールしているかというと、24 行目を見ていただくと、開発用のシェルみたいなのを定義できるので、ここでインストールされている。この Linux パッケージというのは、ARM のだと用意されていなかったので、ARM の場合は x64 用のツールチェーンを使うように 18 行目で設定していて、そこら辺によって Linux 向けのリンカとかが簡単に使えるようになった
hsjoihs
hsjoihs
つまりこれは experiments 用のみであって、コンパイラをビルドしたりテストしたりするのには使ってない?
tkr
tkr
今のところは使ってない
hsjoihs
hsjoihs
分かりました
tkr
tkr
で、これを使う場合には nix とか direnv ってのを入れなきゃいけなくて、入れると use flake ってのでさっき定義してた develop 用の環境が勝手にロードされるという仕組みになっている。使う場合はいろいろインストールが必要
hsjoihs
hsjoihs
なるほどなるほど
しかしまあ、なんとか突破できてよかった。いやまあギリギリ突破できそうで勉強になりそうだからこのテーマ設定にしたというのもあるけど。元の『低レイヤを知りたい人のためのCコンパイラ作成入門』は親切であるがゆえに『書いてあることをこなすとできる』って作業になってしまうのよね
tkr
tkr
実は自分ひとりで元のもやってるんだけど、
hsjoihs
hsjoihs
あ、そうなの
tkr
tkr
やっぱり元のほうが圧倒的にラク
hsjoihs
hsjoihs
そりゃそうよね。かなり多くをアセンブラとリンカに押し付けることができるし
tkr
tkr
リンカはともかく、アセンブラなんて大したことやってないっしょって思ってたら、そんなことなかった。アセンブラって思った以上に複雑なことしてたんだな、こんな複雑だったんだ、っていう気持ちになった

2023年1月3日

tkr
tkr
__builtin_putchar を書いたが動かない
hsjoihs
hsjoihs
int 0x80 は 32 bit 用の ABI であって、64 bit のポインタを渡しても上位 32 bit は無視されるそうです
exit システムコールはポインタを用いなかったのでこの問題が露呈しなかったが、write システムコールはポインタを要求するので int 0x80 ではダメで syscall を使う必要があるとのこと
tkr
tkr
そもそもこれって32bitバイナリを吐き出すプロジェクトですか?64bitバイナリを吐き出すプロジェクトですか?
今のところコンパイラは32bitバイナリを吐き出しているのにexperimentのtiny以外は64bitバイナリ(ただし64bitでは使えなさそうな int 0x80 を使っている)を吐き出しているように見えます
qemuでセグフォが起きるのでどっちかに統一したいです
と思ったけどバイナリ自体は64bitなのでintをsyscallに書き換えればよさそう?
hsjoihs
hsjoihs
64 bit でも int 0x80 は使えるんですよね。exit は引数にポインタがないので int 0x80 をそのまま使って問題ないけど、write は引数にポインタがあるせいで困る
tkr
tkr
qemuの制限なんですかね?(qemuでエミュレーションしているm1 mac上のdockerだと死にました)
hsjoihs
hsjoihs
qemu はいろんなものが動かなかったりするという噂を聞いています
tkr
tkr
コンパイラが出力するバイナリはsyscallを使っているのでexperimentの方も書き換えておきます
qemuだと int 0x80 が動かないので syscall に書き換えた
putcharをアセンブリで書けた。writeに渡すbufはポインタじゃないといけないからスタックに引数をpushしてrspを渡せばいいかな
スタック領域を16の倍数にアライメントとかを考えるとpush/popでネストした式の途中結果を保存するより全部ローカル変数に入れた方が楽なのでは 🤔
れもん
れもん
その概念を拡張したのがregister allocation
tkr
tkr
実装したことがない😿

2023年1月4日

tkr
tkr
アセンブリ→バイナリの変換規則眺めてもなんもわからんって思ってたけど、hexじゃなくて2進数列にしたらなんとなく分かったわ。そういうこと???
れもん
れもん
x86は3bitでレジスタを表していたりする。64bit拡張命令(0x4_)とかも調べてみると面白いよ
tkr
tkr
やっぱ認識あってた。むずい
そういえば、標準出力のテストなんてものはいま無いので、追加するかどうか迷っています
hsjoihs
hsjoihs
まあほしいですよねテスト
tkr
tkr
わかる〜。作らなくては

2023年1月7日

hsjoihs
hsjoihs
標準出力のテストどうするかね
tkr
tkr
あーそういやこれドラフトのままだった。完全に終わってた気になってた。今のインターフェース的に標準出力のテストめんどいよな
hsjoihs
hsjoihs
リダイレクトするのではいかんの?
tkr
tkr
現状のシェルスクリプトが check って関数の形じゃないですか。あと、複数行コードが出てきたときに今のままだとつらい
hsjoihs
hsjoihs
もっとも簡単な形で解決すればいいので、check_stdout を新造するという形でいいのでは
tkr
tkr
それか環境変数で
hsjoihs
hsjoihs
関数分ければよくな〜い?
tkr
tkr
とりあえず、check の第三引数にオプショナルで与えるということで
hsjoihs
hsjoihs
よさそう

2023年1月8日

hsjoihs
hsjoihs
さて step 15 は『関数の定義に対応する』だった。関数を用意する方法自体は前のステップとかで整備されているはずなので、それに乗っかればまあ実装できるでしょう
とはいえ、ここからは main 関数が出てくるようになるという話もある。これをどうするか。
まあ、普通に、main 関数を定義して、それを暗黙にスタートアップルーチンから call するようにすればいいか
main を call するコードを出力するには、main(); というソースコードをそのまま解釈させればよかろう
とりあえず、現状のテストケースを全て暗黙に main 関数の中に突っ込み、エントリーポイントの中では暗黙に main(); を呼び出す実装へと変更しよう
先にコード生成部分をそう変更してから、その後にテストケース自体に main() { ... } を追加して、それをパースするようにと変更すればよさそうだな
ちょっとリファクタリング。u16 とか u8 幅を超えていたら明示的にエラーメッセージを出して落とすようにし、現状の codegen は関数テーブルを変更しないので mutable で渡しているところを immutable にし、generate_startup 関数として切り出す
ところで、エントリーポイントの実装は main(); じゃなくて return main(); にすべきだな。というか、現状の return は実は return ではなく exit を呼ぶ偽物なので、__builtin_exit みたいな名前に変更しちゃおうかな
カッコを要求してないし、JavaScript に倣って __throw でいいや
で、この __throw の実装を複製して return を作っていく
非自明なのは codegen だけで、このときには edi の結果をコピーして leave; ret すればいいのかな
よし、これで return が復活したはず。まだ試せないけど
そうしたら、次に目指すべきは、テストケースの __throw を return に戻して、
__start() { __throw main(); }
main() { ここにテストケースを貼る }
といった感じへと改めていくことだな
えーっと、__throw main(); をトークナイズして、さっき切り出した関数に渡せば、これでよさそう?やってみよう
前述のとおり、テストケースの __throw を return に戻して、
よし動いた!
コード部分の変更ができたので、今度はテストケース自体を main() {} で囲うようにしよう
これにより、toplevel の概念が新たに生まれるようになるはず
えーと、とりあえず Program 型を FunctionContent とかに改名するか
変数名とかも適宜改名した。さて、残るは parse_program 関数だけであって、これが FunctionContent を返すのではなくて関数定義のリストを返すように変更すればよい
対応すべきは `foo(x, y) { ... }` という構文なので、とりあえず関数呼び出しの構文を定義している parse_primary をコピペして、実引数ではなく仮引数なので identifier だけを受理して、
よし、parse_toplevel_function_definition が実装できた
あとは、そうして読み込まれた関数定義ごとに、コード生成をしてやれば完成かな。まだ仮引数をレジスタから読みだすところを作ってないけど、とりあえずテストケースを main() { ... } で包んで実験しよう
よし、単に main() で包んだだけのやつは動いた。じゃあ関数定義を複数にすると?
one() { return 1; } two() { return one() + 1; } main() { return one() + two() + __builtin_three(); }
動きますね。やったぁ
あとは仮引数をレジスタから読み出す処理を実装すれば、再帰でフィボナッチが呼べるはず
その前に、ちょっとコードをきれいにして、codegen に住んでいてもよさそうな感じのコードにしておこう
あー、std::mem::take ってこういうときに便利なのね。なるほどなぁ
xやyといったローカル変数が存在するものとしてコンパイルして、関数のプロローグの中で、レジスタの値をそのローカル変数のためのスタック上の領域に書き出してください。
とあるのだから、その通りにすればよい
えーと、引数自体が rdi に入っているから、計算をするのに rdi を使うことができなくて
もう直に mov [rbp-12], r9d とかの命令を使っちゃおう。さて実験だ
とりあえずフィボナッチ
はい~動かない
あ、Buf::new() に対して fold しておるな。『初期値代入』コードが入るべき場所はここだろ
んー動かない
……あっ、なるほどエラーメッセージが出てるけど他の成功メッセージで流れてしまってるのか
どれどれ
thread 'main' panicked at '関数 fib が見つかりません', src/codegen.rs:579:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
あ~~なるほど、関数 fib の中ではまだ関数 fib が定義されていないから、再帰関数を呼び出すことができないのか
これはしょうがないので、テストケースを変えよう
[FAIL] add(x, y, z) { return x + y + z; } main() { return add(1, 2, __builtin_three()); } => 6 expected, but got 139
はいはいセグフォセグフォ
えーと失敗したときにも rm -rf してしまってるから調査対象物がない。成功時にのみ消すようにしよう
[FAIL] id(x) { return x; } main() { return id(6); } => 6 expected, but got 139
これでダメということは、まあなんかが根本的にダメなんだろうな。もうちょいソースを眺めるか
変数はスタック上にあるのではなくてベースポインタからの差分なのだから、stack_size には影響を与えないはず。だから stack_size は 8 のままにしなきゃいけないな
ところで、変数を読むところで落ちてる?変数を書き込むところで落ちてる?確認しよう
[FAIL] three(x) { return 3; } main() { return three(6); } => 3 expected, but got 139
まあそうよね
tkr の書いていたコードと重複があったことがわかり、リファクタリングをしていたところ、多分オフセットが -4 とかじゃなくて 0 になってるのがよくなさそうという気持ちになった。
しかし、謎がある。なぜ既存のコードではオフセットが 0 になることがなかったのだろうか
なってたけどバグってなかったのかもしれない。まあいいや、とりあえずオフセットは一律で idents.len() * 4 + 4、というか idx * 4 + 4、になるということに決めてしまおうか
というか idents じゃなくて lvar_table みたいな名前にしたいな。functions も function_table にしたいわね
はい無事動いた。とはいえ、再帰呼び出しができていないので、
このステップが終わるとフィボナッチ数列を再帰で計算しつつ表示したりできるようになるのでグッと面白くなるはずです。
が達成できていないな。じゃあ、せっかくなので、今回の機能を使ってフィボナッチをループで計算しつつ表示するテストケースを追加するか
check 0 "if_non0(n) { __builtin_putchar(n ? n + 48 : 32); return 0; } print(n) { if_non0(n / 100); if_non0((n / 10) % 10); __builtin_putchar((n % 10) + 48); return 0; } main() { a = 0; b = 1; for(;a<255;) { print(a); __builtin_putchar(44); c = a+b; a = b; b = c; } return 0; }" "  0,  1,  1,  2,  3,  5,  8, 13, 21, 34, 55, 89,144,233,"
えーと % 演算子が実装されておらず、かつ % が bash の printf と衝突する。了解
とりあえず bash の printf と衝突するというのを直して、ループへと展開して、
はいはい、 % だけじゃなくて ? と ++ と -= もない。それもほぐしてやる。
255 が書けない。じゃあ、フィボナッチ数列ではなくリュカ数列にすればよい。すると
check 0 "if_non0(n) { if (n) { __builtin_putchar(n + 48); } else { __builtin_putchar(32); } return 0; } print(n) { for(h=0;n>=100;h=h+1){n=n-100;} if_non0(h); for(t=0;n>=10;t=t+1){n=n-10;} if_non0(t); __builtin_putchar(n + 48); return 0; } main() { a = 2; b = 1; for(;a<127;) { print(a); __builtin_putchar(44); c = a+b; a = b; b = c; } return 0; }" "  2,  1,  3,  4,  7, 11, 18, 29, 47, 76,123,"

thread 'main' panicked at 'while 文の中でジャンプするためのバッファの長さが i8 に収まりません: TryFromIntError(())', src/codegen.rs:326:65
なるほどね
バッファの中身を読んでみるか
thread 'main' panicked at 'while 文の中でジャンプするためのバッファの長さが i8 に収まりません。バッファの長さは 155、中身は 0x[55 5f 48 83 ef 04 48 8b 3f 57 bf 7f 00 00 00 57 5f 58 39 f8 0f 9c c0 0f b6 f8 83 ff 00 74 7e 48 83 ec 08 55 5f 48 83 ef 04 48 8b 3f 57 5f b8 0c 01 40 00 ff d0 89 c7 48 83 c4 08 48 83 ec 08 bf 2c 00 00 00 57 5f b8 87 00 40 00 ff d0 89 c7 48 83 c4 08 55 5f 48 83 ef 0c 57 55 5f 48 83 ef 04 48 8b 3f 57 55 5f 48 83 ef 08 48 8b 3f 57 58 5f 01 c7 58 89 38 55 5f 48 83 ef 04 57 55 5f 48 83 ef 08 48 8b 3f 58 89 38 55 5f 48 83 ef 08 57 55 5f 48 83 ef 0c 48 8b 3f 58 89 38] です', src/codegen.rs:327:21
えーと disass して
0:  55                      push   rbp
1:  5f                      pop    rdi
2:  48 83 ef 04             sub    rdi,0x4
6:  48 8b 3f                mov    rdi,QWORD PTR [rdi]
9:  57                      push   rdi
a:  bf 7f 00 00 00          mov    edi,0x7f
f:  57                      push   rdi
10: 5f                      pop    rdi
11: 58                      pop    rax
12: 39 f8                   cmp    eax,edi
14: 0f 9c c0                setl   al
17: 0f b6 f8                movzx  edi,al
1a: 83 ff 00                cmp    edi,0x0
1d: 74 7e                   je     0x9d
1f: 48 83 ec 08             sub    rsp,0x8
23: 55                      push   rbp
24: 5f                      pop    rdi
25: 48 83 ef 04             sub    rdi,0x4
29: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
2c: 57                      push   rdi
2d: 5f                      pop    rdi
2e: b8 0c 01 40 00          mov    eax,0x40010c
33: ff d0                   call   rax
35: 89 c7                   mov    edi,eax
37: 48 83 c4 08             add    rsp,0x8
3b: 48 83 ec 08             sub    rsp,0x8
3f: bf 2c 00 00 00          mov    edi,0x2c
44: 57                      push   rdi
45: 5f                      pop    rdi
46: b8 87 00 40 00          mov    eax,0x400087
4b: ff d0                   call   rax
4d: 89 c7                   mov    edi,eax
4f: 48 83 c4 08             add    rsp,0x8
53: 55                      push   rbp
54: 5f                      pop    rdi
55: 48 83 ef 0c             sub    rdi,0xc
59: 57                      push   rdi
5a: 55                      push   rbp
5b: 5f                      pop    rdi
5c: 48 83 ef 04             sub    rdi,0x4
60: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
63: 57                      push   rdi
64: 55                      push   rbp
65: 5f                      pop    rdi
66: 48 83 ef 08             sub    rdi,0x8
6a: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
6d: 57                      push   rdi
6e: 58                      pop    rax
6f: 5f                      pop    rdi
70: 01 c7                   add    edi,eax
72: 58                      pop    rax
73: 89 38                   mov    DWORD PTR [rax],edi
75: 55                      push   rbp
76: 5f                      pop    rdi
77: 48 83 ef 04             sub    rdi,0x4
7b: 57                      push   rdi
7c: 55                      push   rbp
7d: 5f                      pop    rdi
7e: 48 83 ef 08             sub    rdi,0x8
82: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
85: 58                      pop    rax
86: 89 38                   mov    DWORD PTR [rax],edi
88: 55                      push   rbp
89: 5f                      pop    rdi
8a: 48 83 ef 08             sub    rdi,0x8
8e: 57                      push   rdi
8f: 55                      push   rbp
90: 5f                      pop    rdi
91: 48 83 ef 0c             sub    rdi,0xc
95: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
98: 58                      pop    rax
99: 89 38                   mov    DWORD PTR [rax],edi
はい、とりあえず for(;a<127;) { print(a); __builtin_putchar(44); c = a+b; a = b; b = c; } が書けないのね
まあもうちょい削ったらいけるでしょ。削ります
よし!!!!いけた!!!!!
最終的なソースコードは以下の通り
if_non0(n)
{
	if (n) {
		__builtin_putchar(n + 48);
	}
	else {
		__builtin_putchar(32);
	}
	return 0;
}

print(n)
{
	for (h = 0; n >= 100; h = h + 1) {
		n = n - 100;
	}
	if_non0(h);
	for (t = 0; n >= 10; t = t + 1) {
		n = n - 10;
	}
	if_non0(t);
	__builtin_putchar(n + 48);
	return 0;
}

printcomma(n)
{
	print(n);
	__builtin_putchar(44);
	return n;
}
main()
{
	a = 2;
	b = 1;
	while (a < 127) {
		c = printcomma(a) + b;
		a = b;
		b = c;
	}
	return 0;
}
リュカ数列は知名度がないし、フィボナッチの例も足しておくか。上限を表すのに 255 とは書けないが、20*20などと書くことはできるので
thread 'main' panicked at 'while 文の中でジャンプするためのバッファの長さが i8 に収まりません。バッファの長さは 134、中身は 0x[55 5f 48 83 ef 04 48 8b 3f 57 bf 14 00 00 00 57 bf 14 00 00 00 57 58 5f 0f af f8 57 5f 58 39 f8 0f 9c c0 0f b6 f8 83 ff 00 74 5d 55 5f 48 83 ef 0c 57 48 83 ec 0c 55 5f 48 83 ef 04 48 8b 3f 57 5f b8 57 02 40 00 ff d0 89 c7 48 83 c4 0c 57 55 5f 48 83 ef 08 48 8b 3f 57 58 5f 01 c7 58 89 38 55 5f 48 83 ef 04 57 55 5f 48 83 ef 08 48 8b 3f 58 89 38 55 5f 48 83 ef 08 57 55 5f 48 83 ef 0c 48 8b 3f 58 89 38] です', src/codegen.rs:327:21
そうでした~
まあ 89 まででいいか。そうなるとコードももうちょいシンプルにできて、
print(n) { 
    for (t = 0; n >= 10; t = t + 1) { 
        n = n - 10; 
    } 
    if (t) { 
        __builtin_putchar(t + 48); 
    } 
    else { 
        __builtin_putchar(32); 
    } 
    __builtin_putchar(n + 48); 
    return 0;
}
printcomma(n) {
    print(n);
    __builtin_putchar(44); 
    return n; 
} 
main() { 
    a = 0; b = 1; 
    while (a < 100) { c = printcomma(a) + b; a = b; b = c; } 
    return 0; 
}
とすればよい。
終わらせたので、その旨をツイート
tkr
tkr
はや
今再帰使えないっけ
あー関数のオフセットがいるのか
hsjoihs
hsjoihs
再帰できるようにしてからマージします?
そういえば、手元の非 M1 な Mac で試したら mktemp が illegal option で落ちましたね

2023年1月9日

tkr
tkr
再帰大変そうーー
-dだめですっけ?
なんかオプション違ったようなー…
hsjoihs
hsjoihs
再帰わりとできそうな気がしたのでやってみるか
関数のオフセット自体は関数をコンパイルし始める直前に定まるので、その時点で関数テーブルに登録しておけば問題なく動くはずなんだよな
'else でジャンプするためのバッファの長さが i8 に収まりません。バッファの長さは 130、中身は 0x[55 5f 48 83 ef 04 48 8b 3f 57 bf 01 00 00 00 57 5f 58 39 f8 0f 94 c0 0f b6 f8 83 ff 00 74 0b bf 01 00 00 00 89 f8 c9 c3 eb 58 48 83 ec 08 55 5f 48 83 ef 04 48 8b 3f 57 bf 01 00 00 00 57 58 5f 29 c7 57 5f b8 a9 00 40 00 ff d0 89 c7 48 83 c4 08 57 48 83 ec 0c 55 5f 48 83 ef 04 48 8b 3f 57 bf 02 00 00 00 57 58 5f 29 c7 57 5f b8 a9 00 40 00 ff d0 89 c7 48 83 c4 0c 57 58 5f 01 c7 89 f8 c9 c3] です', src/codegen.rs:303:41
ギリ足らんか。とはいえ else { return fib(n-1) + fib(n-2); } がコンパイルできないのは厳しいな。
まあ else の外に出してやればよく、はい普通に動いた
tkr
tkr
すご
定義時点のバイナリサイズをHashMapに突っ込めばいいのか
hsjoihs
hsjoihs
そういうことです
tkr
tkr
確かに、宣言+後ろで定義してある関数に飛ぶのはめんどくさそう…
少なくともワンパスじゃ多分無理😢
hsjoihs
hsjoihs
現状の Buf を改造して、「名前付き穴」みたいなものを書けるようにし、.to_vec() で最終的に書き込むときにその「名前付き穴」を解決する、といった実装が一番シンプルなのかな
tkr
tkr
そうなんよなあ
ジャンプ先って可変長整数だっけ、そうだとしんでしまう
hsjoihs
hsjoihs
実はね~(ログを見てもらうと分かる通り、ジャンプ先が現在 8 bit 固定なのでかなりのショートコーディングを強いられています)
tkr
tkr
わかる…
多分jmp命令って7bitで収まるなら1byte、14bitで収まるなら2byteみたいな感じな気がする😇😇
hsjoihs
hsjoihs
まあとりあえず、最後に「フィボナッチ数列を再帰で計算しつつ表示」を達成してからマージしてもらう、としようかな
ジャンプ先を 127 以内に収めながらのコーディングをもう一回やってみるか
tkr
tkr
コードゴルフ
hsjoihs
hsjoihs
ブロックの中をなるべく短くすることだけが必要なので、全体のコード長自体はむしろ増やす方向ですわね
できた
if_non0(n) { if (n) { __builtin_putchar(n + 48); } else { __builtin_putchar(32); } return 0; } print(n) { for(h=0;n>=100;h=h+1){n=n-100;} if_non0(h); for(t=0;n>=10;t=t+1){n=n-10;} if_non0(t); __builtin_putchar(n + 48); return 0; } printcomma(n) { print(n); __builtin_putchar(44); return n; } fib(n) { if(n == 0){ return 0; } else if(n == 1) { return 1; } return fib(n-1) + fib(n-2); } main() { for(n=0;n<17;n=n+1) {printcomma(fib(n));} return 0; }
れもん
れもん
別に最適化しないなら2byteだけ使っていればよいと思います
tkr
tkr
あ、それできるのか
れもん
れもん
あてかどうせならオペランドに4byte入る0xe9をどうぞ
tkr
tkr
それでよさそう

2023年1月14日

hsjoihs
hsjoihs
さて次は単項&と単項*だからわりと簡単そう
と思いきや、今まで 4 バイトと 8 バイトを雑に扱ってたのを直さないといけないからちょっとめんどいかもね
tkr
tkr
忘れてた。来週の予定までにそこまでは終わらせておこう

2023年1月15日

tkr
tkr
とりあえず雑に実装
*がセグフォする
とりあえずcodegen系関数に引き回すstate多くなってきたので構造体にまとめた
word sizeを64bitにしたけどセグフォがなおらん〜〜〜
*&x はいけるけど y=&x の時 *y はセグフォになる、変数の値の読み書き死んでるなこれ
mov [rax], edi -> mov [rax], rdi にしたらなおった
add命令を64bit化したらテスト2も通るようになった。ついでにsub/mulも64bit化

2023年1月16日

hsjoihs
hsjoihs
レビューをしていくか
mulにだけ0x48がついてませんね
tkr
tkr
あれ、ミスってた
hsjoihs
hsjoihs
この U32_WORD_SIZE って変数名、u32 そのものの size っぽく読めてしまって紛らわしいので、WORD_SIZE_AS_U32 みたいな変数名にしたほうがよいと思います
あと、トークンの方は Mul じゃなくて Asterisk にリネームしてもいいのかもですね。意味が 2 種類現れるようになったし。まあこのブランチをマージしたあとのリファクタリングとしてやってもいいか
tkr
tkr
ですよねー

2023年1月17日

hsjoihs
hsjoihs
step 17 さっさとやっちゃうか
とりあえず、「関数はいままでfoo(x, y)といった形で書いていましたが、これをint foo(int x, int y)といった形になるように改造します。」が一番ラクなので、これをまずやる
えーと FunctionDefinition を生成しているところをいじればいいんだから
FunctionDefinition 型の定義を変更
pub struct FunctionDefinition {
    pub func_name: String,
    pub params: Vec<(Type, ParameterIdentifier)>,
    pub pos: usize,
    pub content: FunctionContent,
    pub return_type: Type
}
として型の情報を足せばよく、
トークナイザに予約語 int を足す必要もある
関数定義のパースに int をねじ込んだので、今度はテストケースに int を入れていく必要がある
足したはずなのに「トップレベルが型名でないもので始まっています」で落ちる。なぜだろう
あー理解。`Identifier("__start")` で始まってると言われている。つまり、__start を C で書いてるからそこも直さなきゃいけない
直した。テストが全部通ったのでコミット
あとは、「変数定義文」「未定義変数をエラー」の二つだな
こうなると、idents って HashMap を local_var_table に、functions って HashMap を function_table にしたくなる。しておこう
次に、パーサーに変数定義文を足す。「真にいにしえの C」と同様に、関数の頭でだけ変数定義ができるようにしておこう
after_param_list で関数の中身を読んでるので、そこの冒頭で変数宣言を読めるようにしよう
とりあえず local_var_definitions という HashMap に読み込んでおくか
で、それを FunctionDefinition に仕込んでおいて、
えーと未定義変数は Codegen で怒るか。えーっと local_var_table が Codegen にあるのは間違いで
あー、ちゃんと tkr もその問題に気づいていて「TODO: FunctionGenみたいなのに分けたい」というコメントが書いてあるな
まあとりあえず、先に変数宣言をちゃんと読めているかをテストしよう
読めているのでコミット
さてリファクタリングしたい。とりあえず、CodeGen 自体を FunctionGen へと改名し、必要に応じて FunctionGen から追い出していこう
リファクタリングした。『関数をコード生成しメインバッファとグローバル関数テーブルに挿入』だけを FunctionGen から追い出すだけで済んだ
そうしたら、今度は FunctionGen に『関数先頭で宣言されている変数の一覧』を渡して、それと照合してエラーを出せばよい
走らせてみると……関数の仮引数についても『知らん』と言われる。それはそう。なのでそいつらも合わせてやろう
ちなみに、関数の先頭で宣言された変数は、関数の仮引数と同じスコープを持ち、名前がかぶってはいけない、というのが C の仕様なので、本当に等価に扱えばよい
よし、実装できた
おや~未定義動作を含む x = 3; y = 5; z = &y + 8; return *z; が CI でだけ落ちる。もういいや、未定義動作だしこのテストケースはコメントアウトしよう

2023年1月18日

tkr
tkr
FunctionGen の外側の Codegen は今のところ複雑じゃないしまだ分けなくていいか(グローバル変数の定義とか出てきたら考えましょう)
hsjoihs
hsjoihs
そうね
tkr 担当の step 18 は簡単で、その次の私がやる step 19 は alloc4(&p, 1, 2, 4, 8) ができるようにしなきゃいけないんだよな
tkr
tkr
とりあえず来週火曜まではお休みで…(発表あるので)
hsjoihs
hsjoihs
了解です。私も授業が第二週に入ったのでそろそろ学業を真面目にやらんとあかん
tkr
tkr
あ、1.5年修士だからまだ忙しいのか
hsjoihs
hsjoihs
そういうことです(てか今学期末で卒業予定なので)
とりあえず brk() の使い方を雑に理解したので組めそうだな
この二つのリンクを残しておけば 2 週間後のわたしがサクッと組んでくれるでしょう
tkr
tkr
ポインタ+1確かに+1じゃないのかこれ
hsjoihs
hsjoihs
そうなのよね
とりあえず小さなリファクタリングだけ入れといたので一応 review を request しておく
パーサを式のとトップレベルのとに分ける いちいち std::mem::take するのが面倒なので Buf::append を実装する
tkr
tkr
approve

2023年1月30日

tkr
tkr
パーサを修正してcodegenを少しいじるだけでよさそう
ポインタ型ではないものをderefしたらエラーとかは今のところは不要みたい

2023年1月31日

hsjoihs
hsjoihs
うわこの Addr と Deref の処理が対比的になってなくてかなり読みづらいな。バグあるんじゃないかと疑ってしまった。でも合ってはいるのでマージします
tkr
tkr
左辺値処理との一貫性を取ったらこっちが不一貫になっちゃった
新幹線の中でやったから酔ったけど、大した分量なかった
hsjoihs
hsjoihs
ステップ 18 はね
tkr
tkr
ステップ 19 大変そう
hsjoihs
hsjoihs
そうそう。型をつけて回んなきゃいけない
tkr
tkr
型をつけてそれに応じて分岐とか面倒そう
hsjoihs
hsjoihs
UntypedExpr からなる構文木を元に、TypedExpr からなる構文木を再構築する手と、構文木に漸次的に型をつけていく方法がある
私はどちらかというと前者が好み
tkr
tkr
そうよね。とはいえ面倒そう
hsjoihs
hsjoihs
まあ、まず恒等変換を書いて、その後に TypedExpr に型フィールドを要求してエラーが出たところをどんどん直すだけ。そんなに難しくはない
ということで、ステップ 19
Expr を UntypedExpr に改名しようかと思ったが、Rust だしジェネリックパラメータに空タプル or 型情報でやればいいか
空タプルを使うのもどうかと思うので、Any という名のゼロサイズ型を定義し、そいつをつけてまわることで、とりあえず UntypedExpr 化に成功
で、次に local_var_declarations を Context という名前の中に突っ込み、そいつを parse_statement につけてまわる
えーと関数定義から関数宣言を取り出してそいつも引き回さないといけない
んー、C だし『構文木に漸次的に型をつけていく方法』で十分だな。もうそっちにしよう
よし、一応一時間でできたな。スタートアップには main の定義を教えてやる必要があり、よしコンパイルが通った。実行じゃ
えーーとはい、『ビルトイン関数の宣言がない』『仮引数の宣言を知らされていない』というエラーが。至極ごもっとも
その二つの情報を教えると…『関数 fib は宣言されておらず、戻り値の型が分かりません』。あーはい。再帰のためには自身の戻り値の型をちゃんと教えないといけない
教えてやると……よ〜しできたできた
で、現状だと +933 -625 とあまりに diff が大きすぎる。しかしこれの大部分は式パーサーを impl Context にしたことによるインデントの変更なんだよな
これは本質ではないので、ちょっと先に diff を小さくする作業をしていくか
impl Context ではなく明示的に第一引数として引き回すようにする
なんかパソコンが重い
うおー Docker が変なエラーメッセージを吐いた。パソコン再起動
よし、diff が +410 -95 にまで減った
あとは、結局 Any を使わなかったので、それも削ろう
そしてジェネリクスにする必要ももはやないので、削る
これで +384 -70 で済んだ
あと、定義が
pub enum Expr {
    BinaryExpr {
        op: BinaryOp,
        op_pos: usize,
        左辺: Box<Expr>,
        右辺: Box<Expr>,
        typ: Type,
    },
    Numeric {
        val: u8,
        pos: usize,
        typ: Type,
    },
    Identifier {
        ident: String,
        pos: usize,
        typ: Type,
    },
    Call {
        ident: String,
        pos: usize,
        args: Vec<Expr>,
        typ: Type,
    },
    UnaryExpr {
        op: UnaryOp,
        op_pos: usize,
        expr: Box<Expr>,
        typ: Type,
    },
}
になってて、全部に typ を書いているのもあんまり賢くないんだよな。とはいえこれを変えると diff が増えるので今は放置しよう(本末転倒)
さて、じゃあ本題の『ポインタの加減算で sizeof を見る』を実装していきますか
足し算のときの変換を実装
引き算のときの変換を実装
さて、問題はこれをどうテストするかで、compilerbook には
この段階ではまだ連続してメモリをアロケートする方法がないので(我々のコンパイラにはまだ配列がない)、テストを書くのはちょっと大変です。ここは単に外部のコンパイラの助けを借りて、そちらのほうでmallocすることにして、自分のコンパイラの出力ではそのヘルパー関数を使ってテストを書くようにしてみてください。
とある。しかし我々は外部のコンパイラの生成したオブジェクトファイルとリンクできないので、これまたコンパイラディレクティブを新造しなければならない
さて、ここでログを遡ると、『この二つのリンクを残しておけば 2 週間後のわたしがサクッと組んでくれるでしょう』と 2 週間前の私が以下の二つのリンクを書き残している。
これが伏線回収ってやつですね
さて、これらのリンクを読んでいく限り、
#include <unistd.h>

int brk(void* end_data_segment);
void *sbrk(intptr_t increment);

int alloc4(int **pp, int a, int b, int c, int d) {
    int *p = sbrk(0);
    brk(p + 4);
    p[0] = a;
    p[1] = b;
    p[2] = c;
    p[3] = d;
    *pp = p;
    return 0;
}
として alloc4 は実装できそうだ。wandbox でも試してみよう
#include <unistd.h>
#include <stdio.h>

int brk(void* end_data_segment);
void *sbrk(intptr_t increment);

int alloc4(int **pp, int a, int b, int c, int d) {
    int *p = sbrk(0);
    brk(p + 4);
    p[0] = a;
    p[1] = b;
    p[2] = c;
    p[3] = d;
    *pp = p;
    return 0;
}

int main() {
    int *p;
    alloc4(&p, 1, 2, 4, 8);
    int *q;
    q = p + 2;
    printf("%d, ", *q); // 4
    q = p + 3;
    printf("%d\n", *q); // 8
    return 0;
}
ちゃんと動いてますね。もちろんたまたま動いているだけかもしれんけど
ん? ushitora-anqou 実装では最初にゼロ渡すのもその後に拡張するのも両方 syscall(12, addr) で実装しているけど、これでいいの?
なるほど、 https://man7.org/linux/man-pages/man2/brk.2.html に書いてある。
However, the actual Linux system call returns the new program break on success. On failure, the system call returns the current break.
理解。なので、syscall(12, NULL) を最初に走らせて現在の break 位置を知り、その後にその現在の break 位置から必要量だけ増やした位置を syscall(12, new_break) すれば動くのか
さて、こいつは 5 引数関数だから……あ、第一引数ポインタだから『rbpにoffsetを足した位置にediを代入』で事故りそう
まあいいや、とりあえず戻り値にポインタ入れちゃおう
あとはアセンブリプログラミングを頑張り、えーっとこうすればスタックを使わないからアラインメントも気にしなくてよくなって
よし、実装できた。あとはこれをコンパイラに教え、呼ぶだけだ
運命の時!
[FAIL] int main() { int *p; p = __builtin_alloc4(1, 2, 4, 8); return *p + *(p+1) + *(p+2) + *(p+3); } => 15 expected, but got 1
おや~?
えーと testwork フォルダ内に当該バイナリがあるはず。一旦 rm -rf testwork/* してから ./test.sh することにより ./a.out が手に入る
とりあえず failing.out とかいう名前を付けて一旦コミットし、この検体を分析しよう
hsjoihs@LAPTOP-BKHPSENK:~/c-compilers/c_to_elf_compiler$ gdb failing.out
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from failing.out...
(No debugging symbols found in failing.out)
(gdb) run
Starting program: /home/hsjoihs/c-compilers/c_to_elf_compiler/failing.out 
[Inferior 1 (process 10911) exited with code 01]
(gdb) start
No symbol table is loaded.  Use the "file" command.
Make breakpoint pending on future shared library load? (y or [n]) y
Temporary breakpoint 1 (-qualified main) pending.
Starting program: /home/hsjoihs/c-compilers/c_to_elf_compiler/failing.out 
[Inferior 1 (process 10945) exited with code 01]
(gdb) file
No executable file now.
No symbol file now.
(gdb) 
え~っと run は動くが start も file も効かない。どうしようかな。あ、file failing.out なら動いた。そうするのか
いや stepi できないな。もういい、objdump する
hsjoihs@LAPTOP-BKHPSENK:~/c-compilers/c_to_elf_compiler$ objdump -d failing.out

failing.out:     file format elf64-x86-64
うお~ section が存在しないから disass されない
もういいや、バイナリエディタを開いて、中身を https://defuse.ca/online-x86-assembler.htm に突っ込んで、
えーとこれが __builtin_putchar よね。あれ?__builtin_three はどこ行った?
0:  55                      push   rbp
1:  48 89 e5                mov    rbp,rsp
4:  48 83 ec 08             sub    rsp,0x8
8:  89 7d f8                mov    DWORD PTR [rbp-0x8],edi
b:  b8 01 00 00 00          mov    eax,0x1
10: bf 01 00 00 00          mov    edi,0x1
15: 48 8d 75 f8             lea    rsi,[rbp-0x8]
19: ba 01 00 00 00          mov    edx,0x1
1e: 0f 05                   syscall
20: c9                      leave
21: c3                      ret

次にこれが __builtin_alloc4 だけど……あっプロローグでローカル変数の分の引き算をしていない!!これだ
22: 55                      push   rbp
23: 48 89 e5                mov    rbp,rsp
26: 48 83 ec 08             sub    rsp,0x8
2a: 89 7d f8                mov    DWORD PTR [rbp-0x8],edi
2d: 89 75 f0                mov    DWORD PTR [rbp-0x10],esi
30: 89 55 e8                mov    DWORD PTR [rbp-0x18],edx
33: 89 4d e0                mov    DWORD PTR [rbp-0x20],ecx
36: b8 0c 00 00 00          mov    eax,0xc
3b: bf 00 00 00 00          mov    edi,0x0
40: 0f 05                   syscall
42: 50                      push   rax
43: 5f                      pop    rdi
44: b8 10 00 00 00          mov    eax,0x10
49: 48 01 c7                add    rdi,rax
4c: b8 0c 00 00 00          mov    eax,0xc
51: 0f 05                   syscall
53: 48 83 e8 04             sub    rax,0x4
57: 48 8d 7d e0             lea    rdi,[rbp-0x20]
5b: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
5e: 48 89 38                mov    QWORD PTR [rax],rdi
61: 48 83 e8 04             sub    rax,0x4
65: 48 8d 7d e8             lea    rdi,[rbp-0x18]
69: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
6c: 48 89 38                mov    QWORD PTR [rax],rdi
6f: 48 83 e8 04             sub    rax,0x4
73: 48 8d 7d f0             lea    rdi,[rbp-0x10]
77: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
7a: 48 89 38                mov    QWORD PTR [rax],rdi
7d: 48 83 e8 04             sub    rax,0x4
81: 48 8d 7d f8             lea    rdi,[rbp-0x8]
85: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
88: 48 89 38                mov    QWORD PTR [rax],rdi
8b: c9                      leave
8c: c3                      ret
はい~単純な凡ミス。これを直して………あれ???まだ通らない。なにゆえ~
とりあえず、もう一回検体を生成しよう
ああ普通に __builtin_three あるね。さっきはコピペ範囲をミスっただけか
0:  55                      push   rbp
1:  48 89 e5                mov    rbp,rsp
4:  48 83 ec 00             sub    rsp,0x0
8:  b8 03 00 00 00          mov    eax,0x3
d:  c9                      leave
e:  c3                      ret

次にこれが __builtin_putchar で、
f:  55                      push   rbp
10: 48 89 e5                mov    rbp,rsp
13: 48 83 ec 08             sub    rsp,0x8
17: 89 7d f8                mov    DWORD PTR [rbp-0x8],edi
1a: b8 01 00 00 00          mov    eax,0x1
1f: bf 01 00 00 00          mov    edi,0x1
24: 48 8d 75 f8             lea    rsi,[rbp-0x8]
28: ba 01 00 00 00          mov    edx,0x1
2d: 0f 05                   syscall
2f: c9                      leave
30: c3                      ret

お次のこれが __builtin_alloc4 のはず。今度はちゃんと 0x20 を引いてるんだけどなぁ
31: 55                      push   rbp
32: 48 89 e5                mov    rbp,rsp
35: 48 83 ec 20             sub    rsp,0x20
39: 89 7d f8                mov    DWORD PTR [rbp-0x8],edi
3c: 89 75 f0                mov    DWORD PTR [rbp-0x10],esi
3f: 89 55 e8                mov    DWORD PTR [rbp-0x18],edx
42: 89 4d e0                mov    DWORD PTR [rbp-0x20],ecx
45: b8 0c 00 00 00          mov    eax,0xc
4a: bf 00 00 00 00          mov    edi,0x0
4f: 0f 05                   syscall
51: 50                      push   rax
52: 5f                      pop    rdi
53: b8 10 00 00 00          mov    eax,0x10
58: 48 01 c7                add    rdi,rax
5b: b8 0c 00 00 00          mov    eax,0xc
60: 0f 05                   syscall
62: 48 83 e8 04             sub    rax,0x4
66: 48 8d 7d e0             lea    rdi,[rbp-0x20]
6a: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
6d: 48 89 38                mov    QWORD PTR [rax],rdi
70: 48 83 e8 04             sub    rax,0x4
74: 48 8d 7d e8             lea    rdi,[rbp-0x18]
78: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
7b: 48 89 38                mov    QWORD PTR [rax],rdi
7e: 48 83 e8 04             sub    rax,0x4
82: 48 8d 7d f0             lea    rdi,[rbp-0x10]
86: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
89: 48 89 38                mov    QWORD PTR [rax],rdi
8c: 48 83 e8 04             sub    rax,0x4
90: 48 8d 7d f8             lea    rdi,[rbp-0x8]
94: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
97: 48 89 38                mov    QWORD PTR [rax],rdi
9a: c9                      leave
9b: c3                      ret

で、こいつが今回試されるべき int main() { int *p; p = __builtin_alloc4(1, 2, 4, 8); return *p + *(p+1) + *(p+2) + *(p+3); }
9c: 55                      push   rbp
9d: 48 89 e5                mov    rbp,rsp
a0: 48 83 ec 08             sub    rsp,0x8
a4: 55                      push   rbp
a5: 5f                      pop    rdi
a6: 48 83 ef 08             sub    rdi,0x8
aa: 57                      push   rdi
ab: 48 83 ec 10             sub    rsp,0x10
af: bf 08 00 00 00          mov    edi,0x8
b4: 57                      push   rdi
b5: bf 04 00 00 00          mov    edi,0x4
ba: 57                      push   rdi
bb: bf 02 00 00 00          mov    edi,0x2
c0: 57                      push   rdi
c1: bf 01 00 00 00          mov    edi,0x1
c6: 57                      push   rdi
c7: 5f                      pop    rdi
c8: 5e                      pop    rsi
c9: 5a                      pop    rdx
ca: 59                      pop    rcx
cb: b8 a9 00 40 00          mov    eax,0x4000a9
d0: ff d0                   call   rax
d2: 89 c7                   mov    edi,eax
d4: 48 83 c4 10             add    rsp,0x10
d8: 58                      pop    rax
d9: 48 89 38                mov    QWORD PTR [rax],rdi
dc: 55                      push   rbp
dd: 5f                      pop    rdi
de: 48 83 ef 08             sub    rdi,0x8
e2: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
e5: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
e8: 57                      push   rdi
e9: 55                      push   rbp
ea: 5f                      pop    rdi
eb: 48 83 ef 08             sub    rdi,0x8
ef: 48 8b 3f                mov    rdi,QWORD PTR [rdi]
f2: 57                      push   rdi
f3: bf 04 00 00 00          mov    edi,0x4
f8: 57                      push   rdi
f9: bf 01 00 00 00          mov    edi,0x1
fe: 57                      push   rdi
ff: 58                      pop    rax
100:    5f                      pop    rdi
101:    48 0f af f8             imul   rdi,rax
105:    57                      push   rdi
106:    58                      pop    rax
107:    5f                      pop    rdi
108:    48 01 c7                add    rdi,rax
10b:    48 8b 3f                mov    rdi,QWORD PTR [rdi]
10e:    57                      push   rdi
10f:    58                      pop    rax
110:    5f                      pop    rdi
111:    48 01 c7                add    rdi,rax
114:    57                      push   rdi
115:    55                      push   rbp
116:    5f                      pop    rdi
117:    48 83 ef 08             sub    rdi,0x8
11b:    48 8b 3f                mov    rdi,QWORD PTR [rdi]
11e:    57                      push   rdi
11f:    bf 04 00 00 00          mov    edi,0x4
124:    57                      push   rdi
125:    bf 02 00 00 00          mov    edi,0x2
12a:    57                      push   rdi
12b:    58                      pop    rax
12c:    5f                      pop    rdi
12d:    48 0f af f8             imul   rdi,rax
131:    57                      push   rdi
132:    58                      pop    rax
133:    5f                      pop    rdi
134:    48 01 c7                add    rdi,rax
137:    48 8b 3f                mov    rdi,QWORD PTR [rdi]
13a:    57                      push   rdi
13b:    58                      pop    rax
13c:    5f                      pop    rdi
13d:    48 01 c7                add    rdi,rax
140:    57                      push   rdi
141:    55                      push   rbp
142:    5f                      pop    rdi
143:    48 83 ef 08             sub    rdi,0x8
147:    48 8b 3f                mov    rdi,QWORD PTR [rdi]
14a:    57                      push   rdi
14b:    bf 04 00 00 00          mov    edi,0x4
150:    57                      push   rdi
151:    bf 03 00 00 00          mov    edi,0x3
156:    57                      push   rdi
157:    58                      pop    rax
158:    5f                      pop    rdi
159:    48 0f af f8             imul   rdi,rax
15d:    57                      push   rdi
15e:    58                      pop    rax
15f:    5f                      pop    rdi
160:    48 01 c7                add    rdi,rax
163:    48 8b 3f                mov    rdi,QWORD PTR [rdi]
166:    57                      push   rdi
167:    58                      pop    rax
168:    5f                      pop    rdi
169:    48 01 c7                add    rdi,rax
16c:    89 f8                   mov    eax,edi
16e:    c9                      leave
16f:    c3                      ret
170:    55                      push   rbp
171:    48 89 e5                mov    rbp,rsp
174:    48 83 ec 00             sub    rsp,0x0
178:    48 83 ec 08             sub    rsp,0x8
17c:    b8 14 01 40 00          mov    eax,0x400114
181:    ff d0                   call   rax
183:    89 c7                   mov    edi,eax
185:    48 83 c4 08             add    rsp,0x8
189:    b8 3c 00 00 00          mov    eax,0x3c
18e:    0f 05                   syscall
よし、わからん!
とりあえず、困難を分割しよう。p[0] から p[3] のうち、格納に成功しているのはどこまでだろうか
check 1 "int main() { int *p; p = __builtin_alloc4(1, 2, 4, 8); return *p; }"
check 7 "int main() { int *p; p = __builtin_alloc4(7, 2, 4, 8); return *p; }"
check 11 "int main() { int *p; p = __builtin_alloc4(11, 2, 4, 8); return *p; }"
通る。p[0]は格納できているようだ
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 1, 4, 8); return *(p+1); } => 1 expected, but got 0
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 7, 4, 8); return *(p+1); } => 7 expected, but got 0
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 11, 4, 8); return *(p+1); } => 11 expected, but got 0
はいはい
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 4, 1, 8); return *(p+2); } => 1 expected, but got 0
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 4, 7, 8); return *(p+2); } => 7 expected, but got 0
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 4, 11, 8); return *(p+2); } => 11 expected, but got 0
パターン見えてきたよ
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 4, 8, 1); return *(p+3); } => 1 expected, but got 0
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 4, 8, 7); return *(p+3); } => 7 expected, but got 0
[FAIL] int main() { int *p; p = __builtin_alloc4(2, 4, 8, 11); return *(p+3); } => 11 expected, but got 0
でしょうね
ということで、先頭要素の格納にだけは成功しているが、残りの 3 つが格納できておらず、必ず 0 で埋まっている
……あっ、分かったような気がするぞ
これ 32 bit 配列に 64 bit 書き込みをしてるのが原因だわ
なのでこれ直せば動くはず。まずは alloc4 の実装を直して、
よ~し動いた。しかし現状だと『このビルトインで確保した配列に、自分で値を書き込む』をやると 64 bit 書き込みになってバグるはず。試そう
[FAIL] int main() { int *p; p = __builtin_alloc4(3, 3, 3, 3); *(p+3) = 8; *(p+2) = 4; *(p+1) = 2; *p = 1; return *p + *(p+1) + *(p+2) + *(p+3); } => 15 expected, but got 1
よし期待通り。なので、ここの書き込みも sizeof を見るようにしなければならない
match typ.sizeof() {
    8 => buf.append(raxが指す位置にrdiを代入()),
    4 => buf.append(raxが指す位置にediを代入()),
    _ => panic!("size が {} な型への代入はできません", typ.sizeof()),
};
よし通った!!!
しかし、アセンブリの書きやすさのためにp[3] → p[2] → p[1] → p[0] の順で書き込んでいっていて幸いだったな。おかげでバグに気づけた
tkr
tkr
sizeof演算子って型だけじゃなくて式もかけたのまじか

2023年2月1日

tkr
tkr
次ってなんでしたっけ
hsjoihs
hsjoihs
sizeof なのでめちゃめちゃラクです
tkr
tkr
もう既に hsjoihs さんが実装終わらせてるようなもんだからな
hsjoihs
hsjoihs
ここ数回は奇数ステップの方が圧倒的に実装重いですね
現実逃避のパワーで、自分で別個でやってた C コンパイラ https://github.com/sozysozbot/2kmcc が compilerbook 基本走りきっちゃった
ということで、課題をやります。レビューと実装をとても素早く終わらせることによって、tkr は私の課題の進捗を邪魔することができます
tkr
tkr
圧を掛けられている
hsjoihs「で、現状だと +933 -625 とあまりに diff が大きすぎる。しかしこれの大部分は式パーサーを impl Context にしたことによるインデントの変更なんだよな」
これってgitの設定でなんとかできなかったっけ
hsjoihs
hsjoihs
できるんだけど(てか VSCode では勝手になんとかなる)、レビューは GitHub 上で発生するので、そっちでの負担が少なくなる方がいいだろうとの判断です
tkr
tkr
あ〜〜
さうす
さうす
これ使えない?
hsjoihs
hsjoihs
へ〜こんなのが
tkr
tkr
そんなのあったの
便利だ
hsjoihs
hsjoihs
まあ今回は &mut Context じゃなくて &Context なので、第一引数で引き回してもそんなに読みづらくならないだろうとの判断もありました
それはともかくさうすくんありがとうございます

2023年2月2日

tkr
tkr
sizeofの実装やるぞ
トークナイザとパーサ変更して終わり。はい
sizeof 値 は一瞬で実装できたので、sizeof (型) を実装してからプルリク投げます
hsjoihs
hsjoihs
まだ配列ないから sizeof(int **[5][2]) とか考えなくていいし、よさそう
sizeof は型取るときはカッコ必要ですよ
tkr
tkr
え、そうなのか。あぶなかった。カッコがなかったら絶対式だけど、カッコがあったら両方ありうる?めちゃくちゃ面倒じゃない?
hsjoihs
hsjoihs
今の c_to_elf コンパイラの現状では型名はすべて int で始まるので、困りません
ところで、型がカッコで括られているといえばキャストもありますが、こいつと sizeof の兼ね合いでこういう話がありますね
int foo() {
    int *p;
    return sizeof((int)*p);
}

int bar() {
    int a = 3;
    return sizeof(int)*a;
}
tkr
tkr
sizeof 実装できた
hsjoihs
hsjoihs
ということで、『ステップ 21: 配列を実装する』をやっていきます
トークナイザに角括弧を追加
enum Type にも配列の存在を教え、Type::sizeof も実装
直前のステップ 20 で sizeof 演算子が実装してあることによりテストケースを書きやすくなっているの、なかなか上手い工夫なんだよな
さて、問題はパーサー。『型をパース』と『識別子名をパース』してる関数を先に一つに固め、そこに変更を加えて配列を差し込んでいくのがラクです
てか既に parse_parameter_type_and_identifier 関数がある。じゃあこれを改造しよう
ローカル変数も関数引数もともに parse_type_and_identifier 関数を呼ぶように変更
配列をポインタに decay させるのは、Box_ という型を間に挟んで
pub fn decay_if_arr(expr: Expr) -> Box_<Expr> {
    match expr.typ() {
        Type::Arr(t, _) => Box_(Box::new(Expr::DecayedArr {
            expr: Box_(Box::new(expr)),
            typ: Type::Ptr(t),
        })),
        _ => Box_(Box::new(expr)),
    }
}

pub fn no_decay_even_if_arr(expr: Expr) -> Box_<Expr> {
    Box_(Box::new(expr))
}
としてやることで解決。コード生成もした。さてあとは配列のための領域をスタックにアロケートするだけだな
えーと現状では local_var_table に『何番目の変数か』を入れていて idx と呼び、それに WORD_SIZE を掛け算してオフセットが計算されている
これを、オフセットを直に入れるように変更する必要があるな。これはもうこのテーブルを struct として切り出したりしたほうがいいんだろうか
てか未定義変数をエラーにするようにした時点で .or_insert は一個削られる運命だったんだな。まずそれを処理しよう
あ、.or_insert 全部外せる
で、今度はオフセットを直に HashMap に入れるようにして、
普通に更新を関数に切り出すべきだな
HashMap に入れて max_offset を増やす LocalVarTable::allocate() も実装
配列のサイズに基づいてアロケートするようにした。これでよかろう

2023年2月6日

tkr
tkr
テスト実行するとMacの調子が悪くなる問題、多分サブプロセスの起動しすぎだったのでなおした
配列のインデックス構文はパーサ書き直すだけ。関数呼び出しも後置演算子の仲間にしたかったが意味解析も同時に行ってる現状では厳しそうなので諦め

2023年2月15日

hsjoihs
hsjoihs
パーサーのリファクタリングもちょっとやってもらったし、そろそろステップ23(グローバル変数)の実装をやる必要がある
さてこれどうしたもんかね。流石にこれは ELF の中身を見ないことには回らないのかな。どうなんだろ
いやまあ、『グローバル変数はスタートアップ時にヒープに確保され、ゼロクリアされる』って処理系にしても規格には準拠するんだが
ん〜〜〜とはいえ、実際の処理系では『ゼロクリアされるような場所にグローバル変数を確保』するのはローダの仕事のはず
つまり!これは『いまローダがないから、それの代用として Linux システムコールに頼ってヒープに確保しているだけ』と主張することができる(ふむ〜?)
Q.『外部リンケージどうするの?』
A.『いままだリンカないから。リンカできたらそのときに初めて考えよう』
まあなにはともあれパースできないと始まらない
いまは parse_toplevel_function_definition しかないので、これを『関数定義かグローバル変数定義』へとすり替える必要がある
とりあえずハリボテして……
パーサーを書いた。動く。
さて、さっき実行時確保の案を出したが、グローバル変数のアドレスってコンパイル時に定数である必要があったりしなかったっけ
なんかそこらへんを考えると、規格に想定されていない曲芸をやるのはやめて、普通に策を練ったほうがいい気がしてくるな
よし、決めた、これは後回しにしよう。
予定変更! @すーぱーてけらには引き続き 24 と 26 もやってもらいますが、私は 23 と 25 の実装をサボります 理由:グローバル変数は外部リンケージを持つ存在なので、「幕間」のあとにやるのが自然 「幕間」と呼んでいるのは、「今回の縛り特有」でどうしても必要になってくる要素をいくつか導入するフェーズ 幕間 1: プロトタイプ宣言と相互再帰 - 現状の Buf を改造して、「名前付き穴」みたいなものを書けるようにし、.to_vec() で最終的に書き込むときにその「名前付き穴」を解決するようにする 幕間 2: 初めての依存ライブラリ、初めての中間ファイル - cargo add serde して、「名前付き穴」のある Buf を JSON として出力してしまう - その出力ファイルを読み、「名前付き穴」を解決し、ELF として吐く仕組みを作る 幕間 3: Yet Another Linkable Format.json - 複数の出力ファイルを読み、「名前付き穴」を解決し、ELF として吐く仕組みを作る - 分割コンパイルのテストケースを書く ということで、私の step 23 は「グローバル変数が定義できるが、sizeof しか取ることができず、読み書きしようとするとコンパイルエラー」で一旦完成とします。同様に step 25 の文字列リテラルも「sizeof しか取ることができない定数」にします
sizeof のオペランドとしては正しく動くようにした。ということでこれで一旦完成とする

2023年2月16日

hsjoihs
hsjoihs
あー、グローバル変数と関数が同じ名前空間に入るようにするか
名前空間をくっつけた

2023年3月2日

tkr
tkr
char型の実装
型を追加する仕組みはすでにあるので少し修正加えるだけ。char+charがintになるの知らなくてバグらせてたけど直した

2023年3月6日

hsjoihs
hsjoihs
文字列リテラルをやる。ただし、sizeof のオペランドとしてのみ動くようにする
まずはエスケープシーケンスなしで組む
このステップではバックスラッシュによるエスケープなどは実装する必要はありません。ステップバイステップで行くのが重要なので、簡単に実装できそうに思えても、しないようにしてみてください。
じゃあもうこれで終わりでいいか。まあ一応『バックスラッシュを見たらエラーを吐く』ぐらいはやってもいい

2023年4月3日

tkr
tkr
さすがにそろそろ再開します
複数行入力、雑に実装してテストケース修正するだけかな
テストケースはプロセス置換で修正、トークナイザで改行などを無視するようにした
エラー位置の表示は filename:line:col の方がVSCodeのサポートとか考えると便利なのでこっちでいきます
filenameのバケツリレー大変だ
ちゃんと動いていそう
int main() {
    b = 1;
}
test.c:2:5
    b = 1;
    ^ 識別子 b は定義されておらず、型が分かりません

2023年4月10日

hsjoihs
hsjoihs
はいレビュー
はい複数行コメント
よしこれで any% クリアですね
Powered by PseudoRoku, which is designed by hsjoihs. Feel free to report any issues.