こんにちは。自分のブログを書くのはものすごく久しぶりのid:rskyです。何年知的便秘だったんだって感じですね。
表題のとおりこれからCコンパイラを作っていこうかと思います。同僚のDQNEOさんが<8cc.go>というコンパイラを作られていて、それに触発されて「Rustでやってみよう」ということで、<低レイヤを知りたい人のためのCコンパイラ作成入門>をCでなくRustでやっていきます。 Rust自体も初めてなので、それも含めてどんなものになっていくのか、自分でも楽しみです。
なお、今後このシリーズでは<低レイヤを知りたい人のためのCコンパイラ作成入門>を二重鉤括弧つきの『入門』と表記します。
開発環境
自分の開発マシンはMacですが、macOSはターゲットとせずLinuxで動くものを作っていきます。『入門』にも
macOSはLinuxとアセンブリのソースレベルでかなり互換性がありますが、完全互換ではありません。この本の内容に従ってmacOS対応のCコンパイラを作成するのは不可能ではないものの、実際に試してみると、細かな点でいろいろな非互換性に悩まされることになるでしょう。Cコンパイラ作成のテクニックと、macOSとLinuxの差異を同時に学ぶというのは、あまりお勧めできることではありません。何かがうまく動かない場合、どちらの理解が間違っているのかよくわからなくなってしまうからです。したがって現時点では、本書ではmacOSは対象外とします。macOSではDockerなどを使ってLinux環境を用意するようにしてください。
とあるように、コンパイラ作成の本質に注力するためです。 予断ですがサブでWindowsも使っており、WSL2とWindows Terminalの出来如何ではいよいよメイン開発環境をWindowsにしてもいいかなと思っています。
コラム: macOSでの差異
Linuxとシンプルかつ最大の違いといえば、macOSでは公開シンボルにアンダースコアのprefixが必要なことでしょうか。main
関数はアセンブリでは_main
という名前で宣言しないといけません。
macOS環境においてシンボルにアンダースコアのprefixがいる件だけちょっと注意ですかね。マクロでやるよりはコンパイラでよしなにやるのがよいのかな? pic.twitter.com/ZbiDkWg8mK
— Ryusuke SEKIYAMA (@rsky) 2019年5月10日
また、これは教えていただいたことですが、レジスタにも違いがあるらしいです。このあたりの知識は今の自分に全く欠けていることもあり、素直にLinuxのみをターゲットにすることにしました。将来的にはクロスコンパイルにもチャレンジしてみたいですね。
レジスタも若干異なるのも鬼門ポイント https://t.co/DpIS64UIYU
— Kawakamiのおっさん (@kawakami_o3) 2019年5月10日
Repository
既に作成済みの9cc.rustをcloneして作業します。
$ git clone git@github.com:rsky/9cc.rust.git $ cd 9cc.rust
Dockerfile
Ubuntu 18.04 LTSベースのイメージを作ります。
FROM ubuntu:18.04 # Install build tools RUN apt-get update && apt-get install -y gcc make git curl binutils libc6-dev # Install Rust RUN curl https://sh.rustup.rs -o rustup-init.sh && chmod +x rustup-init.sh && ./rustup-init.sh -y WORKDIR /9cc.rust COPY . /9cc.rust
Getting Started
このシリーズで作るコンパイラのバイナリ名は r9cc
とします。それではさっそくプロジェクトを作成しましょう。今まさに読み始めたばかりの<プログラミングRust>に従ってパッケージを作成します。
$ cargo new --bin r9cc Created binary (application) `r9cc` project
$ tree r9cc r9cc ├── Cargo.toml └── src └── main.rs 1 directory, 2 files
しかし既に9cc.rustプロジェクト内で作業しているのでこれではちょっと都合が悪い、ということでリネームしました。
$ mv -v r9cc/* . r9cc/Cargo.toml -> ./Cargo.toml r9cc/src -> ./src $ rmdir r9cc
Hello, Rust!
まずは引数も取らず、固定のアセンブリコードを吐くだけのプログラムを書いてみます。僕自身初のRustコードです。
src/main.rs
fn main() { println!(".intel_syntax noprefix"); println!(".global main"); println!("main:"); println!(" mov rax, 1"); println!(" ret"); }
これをビルドします。※ここから先はコンテナ上での作業ですが、そこら辺の説明はざっくり省いています。
$ cargo run Compiling r9cc v0.1.0 (/9cc.rust) Finished dev [unoptimized + debuginfo] target(s) in 5.60s Running `target/debug/r9cc` .intel_syntax noprefix .global main main: mov rax, 1 ret
target/debug/r9cc
にバイナリが作られたようなので、これを実行、生成物をアセンブルして実行します。
$ ./target/debug/r9cc > tmp.s $ cat tmp.s .intel_syntax noprefix .global main main: mov rax, 1 ret $ gcc -o tmp tmp.s $ ./tmp $ echo $? 1
OKですね。引き続き引数として取った数値を返すようにします。
Take Arguments
コマンドライン引数の処理は<プログラミングRust>の第2章序盤に書かれており、そのまま使えました。
use std::io::Write; use std::str::FromStr; fn main() { let mut numbers = Vec::new(); for arg in std::env::args().skip(1) { numbers.push(u8::from_str(&arg).expect("error parsing argument")) } if numbers.len() != 1 { writeln!(std::io::stderr(), "Invalid number of arguments.").unwrap(); std::process::exit(1); } println!(".intel_syntax noprefix"); println!(".global main"); println!("main:"); println!(" mov rax, {}", numbers[0]); println!(" ret"); }
ビルドして実行してみます。引数のパースエラーも処理できていてありがたいですね。
$ cargo run Compiling r9cc v0.1.0 (/9cc.rust) Finished dev [unoptimized + debuginfo] target(s) in 0.41s Running `target/debug/r9cc` Invalid number of arguments. $ ./target/debug/r9cc Invalid number of arguments. $ ./target/debug/r9cc 42 .intel_syntax noprefix .global main main: mov rax, 42 ret $ ./target/debug/r9cc 65535 thread 'main' panicked at 'error parsing argument: ParseIntError { kind: Overflow }', src/libcore/result.rs:997:5 note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace. $ ./target/debug/r9cc foo thread 'main' panicked at 'error parsing argument: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:997:5 note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
というわけで、引数を指定して出力したコードをアセンブル、実行します。
$ ./target/debug/r9cc 42 > tmp.s $ cat tmp.s .intel_syntax noprefix .global main main: mov rax, 42 ret $ gcc -o tmp tmp.s $ ./tmp $ echo $? 42
OKですね。これでようやく『入門』のステップ1の途中、「コンパイラ本体の作成」まで完了しました。 次回は「ユニットテストの作成」です。