LoginSignup
3
3

More than 3 years have passed since last update.

自作Cコンパイラを使ってprintfを卒業した話

Last updated at Posted at 2020-05-03

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なので,

この辺りを参考に,writeをアセンブリで書きました.

syscall.s
    .global _write
_write:
    push %rbp
    mov %rsp, %rbp
    mov $0x2000004, %rax
    syscall
    mov %rbp, %rsp
    pop %rbp
    ret

Cの関数群

いつものように普通に実装するだけです.(itoaをちょっとバグらせたのは内緒...)
Cファイルはこんな感じになりました.

put.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コンパイラ自作ライフを!

3
3
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3