printfとCコンパイラ自作
Cコンパイラを自作しよう.そう思ったとき,例えばこんな手順で作るのではないでしょうか
- int型の四則演算ができて,シフト演算ができて,ビット演算&, |, ^ができた!
- 関数呼び出しと定義ができるようになって,ローカル変数も自由自在!,
- forやwhileもサポートできたし,かなりCっぽいコードがコンパイルできるようになった!
- char型と配列が生えて文字列が扱えるようになった!
徐々に自分のコンパイラがCに近いコードをコンパイルできるようになっていく喜びは,思いの外大きいものです.
でも...割と頑張っていろいろサポートできるようになっても,どうしても外部のCコンパイラに頼らなきゃいけないやつがあるんですよね.
int put_int(int n) {
printf("%d\n", n);
return 0;
}
こいつです.自分のコンパイラのコンパイル結果が正しいかどうかを確認する作業は,どうしてもついて回ります.そのときに,これが必要になるんです.printfはどうしても既存のコンパイラについてくる標準ライブラリに頼らざるをえません.
せっかく頑張っていろいろサポートしたんだし,自作コンパイラが吐いたアセンブリで完結させたいのに...
printfを自作すること,そのprintfを自作したコードををコンパイルできるようにすること,この2つが容易ではないことは,実際にやったことがなくても,なんとなく察しがつくものです.
これほどprintfが憎かったことがあるでしょうか.printf("Hello World!\n"); から世話になっているのに.
printf を卒業して put_int自作
なんとかして
自作コンパイラが吐いたアセンブリだけで完結させる
をできないものか....
そして思ったのです.printfはできなくても,put_int自体はできるのではないかと.
- writeシステムコール
- 自作itoa関数
- 自作strlen関数
これがあれば,いける!
int put_int(int n) {
char cbuf[20];
itoa(n, cbuf);
int len = strlen(cbuf);
cbuf[len++] = '\n';
cbuf[len] = '\0';
write(1, cbuf, len);
return 0;
}
こんな感じで!
write システムコール
さて,まずこいつを準備しようじゃないか.方法は簡単.アセンブラを直接書いてしまえばいいのです.なんてったってシステムコールを1回呼ぶだけなんですから!
著者が使っているのはMacOSなので,
- https://github.com/opensource-apple/xnu/blob/master/bsd/kern/syscalls.master
- https://stackoverflow.com/questions/48845697/macos-64-bit-system-call-table
この辺りを参考に,writeをアセンブリで書きました.
.global _write
_write:
push %rbp
mov %rsp, %rbp
mov $0x2000004, %rax
syscall
mov %rbp, %rsp
pop %rbp
ret
Cの関数群
いつものように普通に実装するだけです.(itoaをちょっとバグらせたのは内緒...)
Cファイルはこんな感じになりました.
int put_int(int n);
char* itoa(int n, char* cbuf);
int strlen(char* cbuf);
int write(int fd, char* cbuf, int nbytes);
int put_int(int n) {
char cbuf[20];
itoa(n, cbuf);
int len = strlen(cbuf);
cbuf[len++] = '\n';
cbuf[len] = '\0';
write(1, cbuf, len);
return 0;
}
char* itoa(int n, char* cbuf) {
char digit_char[11] = "0123456789";
int pos = 0;
if (n == 0) {
cbuf[pos++] = '0';
cbuf[pos] = '\0';
return cbuf;
}
if (n < 0) {
cbuf[pos++] = '-';
n = -n;
}
int n_memo = n;
while (n_memo > 0) {
n_memo /= 10;
pos++;
}
cbuf[pos--] = '\0';
while (n > 0) {
cbuf[pos--] = digit_char[n % 10];
n /= 10;
}
return cbuf;
}
int strlen(char* cbuf) {
int len = 0;
char* p = cbuf;
for (p = cbuf; *p != '\0'; p++) len++;
return len;
}
int main() {
put_int(0);
put_int(-131);
put_int(273);
return 0;
}
追記 (5/4)
上記コードは自作コンパイラでサポートしている範囲でできる限り動くようにと書いたものです.したがって,フルセットのC言語のコードとして見たときには,至らない箇所がいつくかあります.例えば,
- writeシステムコールのプロトタイプ宣言が本来と異なる(型size_tを提供していないため)
- 負数に2の補数表現を用いる処理系において,
n = -n
は,nがintの最小値であるときオーバフローを起こす(unsignedキーワードをサポートしていないため)
追記(5/7)
@fujitanozomu さんがintの最小値にも対応できるput_intを考えてくださいました.
char* myitoa(char* p, int n) {
if (n > 9 || n < -9) p = myitoa(p, n / 10);
int m = n - n / 10 * 10;
if (m < 0) m = -m;
*p++ = '0' + m;
return p;
}
int put_int(int n) {
char cbuf[20];
char* p = cbuf;
if (n < 0) *p++ = '-';
p = myitoa(p, n);
*p++ = '\n';
write(1, cbuf, p - cbuf);
return 0;
}
ウィニングラン
ここまでできれば,あとはコンパイルをするだけです!
自作コンパイラはminccという名前で,githubにおいてあります(宣伝)
https://github.com/kumachan-mis/mincc
put.cをminccでコンパイルし,アセンブラとリンカはgccのものを拝借します.
$ ./mincc.out put.c put.s
$ gcc-9 put.s syscall.s
$ ./a.out
0
-131
273
これで,gccにアセンブリファイルだけを渡せばput_intできるようになったので,晴れてprintfを卒業です!
もちろん,put_charとかputsとかも同じ感じで作れます!(itoaがいらないぶんそっちの方が簡単だったりする...)
みなさんもよいCコンパイラ自作ライフを!