Edited at

Cコンパイラ8ccの1コミット目を読んでみよう


8cc とは?

rui314 さんが作られたCコンパイラです。

次のような特徴があります。


  • C言語で書かれている

  • スクラッチから作られた。字句解析・構文解析も手書き。

  • インクリメンタルな開発

詳しくはご本人の記事と rebuildfm第153回をご参照ください。

Cコンパイラをスクラッチから開発してみた(日記)

Rebuild: 153: Connecting The Dots (rui314)


1コミット目

https://github.com/rui314/8cc/commit/3764b2071b9601067b81976d80175a0851d0f209

git cloneしてcheckoutするとこんな感じになります。

/tmp $ git clone https://github.com/rui314/8cc

/tmp $ cd 8cc
/tmp/8cc $ git checkout 3764b2071b9601067b81976d80175a0851d0f209
/tmp/8cc $ ls
8cc.c Makefile driver.c test.sh*


動かし方

8ccはアセンブリを出力しますが、これはLinux上でのみ動きます。

MacやWindowsを使ってる人はLinux環境を用意する必要があります。

私の方でDocker Imageを用意したのでよかったらお使いください。

docker pull dqneo/ubuntu-build-essential


dockerコンテナを起動して中に入る

/tmp/8cc $ docker run  --rm -it -w /mnt -v $PWD:/mnt dqneo/ubuntu-build-essential bash


コンパイル

root@0b67a3589f2a:/mnt# make

cc -c -o 8cc.o 8cc.c
cc 8cc.o -o 8cc


使い方

標準入力から数値を渡すと、それをC言語のソースコードとみなしてアセンブリに変換してくれます。

root@0b67a3589f2a:/mnt# echo 7 | ./8cc

.text
.global mymain
mymain:
mov $7, %eax
ret

このアセンブリコードをファイルとして保存します。

root@0b67a3589f2a:/mnt# echo 7 | ./8cc > tmp.s

次に、アセンブリファイルと driver.c を一緒にコンパイルします。

root@0b67a3589f2a:/mnt# gcc -o tmp.out driver.c tmp.s

そうすると tmp.out という実行可能ファイルが作られます。

これを実行すると、最初に標準入力から 8ccに渡した数値が画面に表示されます。

root@0b67a3589f2a:/mnt# ./tmp.out

7

ここまでできたら成功です!

以上の手順は、 test.sh を実行することで自動でテストすることができます。

root@0b67a3589f2a:/mnt# ./test.sh

All tests passed


ソースコード解説


8cc.c

コンパイラ本体です。

#include <stdio.h>

#include <stdlib.h>

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;
}

標準入力から数値を1つ受け取って、それをアセンブリに埋め込みつつ出力します。

吐き出されたアセンブリはこのようになります。 (7はダミー値)

    text

.global mymain
mymain:
mov 7, %eax
ret

これは下記のようなC言語と同じ働きをします。

int mymain() {

return 7;
}

8cc.c は、このように数値をreturnするだけの関数(=プログラム部品)を出力します。


driver.c

#include <stdio.h>


extern int mymain(void);

int main(int argc, char **argv) {
int val = mymain();
printf("%d\n", val);
return 0;
}

コンパイラ本体8cc.c にはまだ関数呼び出しの機能がないため、画面に文字を表示するということができません。

よって、動作確認するためには別の仕組みが必要になります。

driver.c によりmain関数という外側をつけてあげることでこれが可能になります。

この driver.c は後のコミットで消滅する (https://github.com/rui314/8cc/commit/dcc0795288ad8c65f0e677a214c9991251c3640d) ので、よくわからなければ気にしなくて大丈夫です。

興味がある人のために、もう少し詳しく説明してみます。

上の手順だと裏側が見えにくいので、ステップを細分化してみましょう。

root@0b67a3589f2a:/mnt# echo 7 | ./8cc > tmp.s

root@0b67a3589f2a:/mnt# gcc -S -o driver.s driver.c
root@0b67a3589f2a:/mnt# as -o tmp.o tmp.s
root@0b67a3589f2a:/mnt# as -o driver.o driver.s
root@0b67a3589f2a:/mnt# gcc -o tmp.out tmp.o driver.o
root@0b67a3589f2a:/mnt# ./tmp.out
7

driver.c はいったんアセンブリ driver.s に変換されます。

アセンブリ driver.stmp.s はアセンブラ(as)によってオブジェクトファイル(driver.o, tmp.o)に変換されます。

最後に、オブジェクトファイルがリンカによってひとつの実行ファイルに結合されます。

gcc を使っているとリンカの動きが見えにくいですね。

そこで gcc -v とすると内部の動きを見ることができます。

root@0b67a3589f2a:/mnt# gcc -v -o tmp.out tmp.o driver.o

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.3.0-16ubuntu3' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --with-as=/usr/bin/x86_64-linux-gnu-as --with-ld=/usr/bin/x86_64-linux-gnu-ld --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)
COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'tmp.out' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper -plugin-opt=-fresolution=/tmp/cc6xlhn6.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o tmp.out /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. tmp.o driver.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-v' '-o' 'tmp.out' '-mtune=generic' '-march=x86-64'

最後の方に現れる /usr/lib/gcc/x86_64-linux-gnu/7/collect2 がリンカです。

root@0b67a3589f2a:/mnt# /usr/lib/gcc/x86_64-linux-gnu/7/collect2 --version

collect2 version 7.3.0
/usr/bin/x86_64-linux-gnu-ld --version
GNU ld (GNU Binutils for Ubuntu) 2.30
Copyright (C) 2018 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.

余談ですがruiさんはこれとは別のリンカ(lld)の作者でもあります。


これでCコンパイラと呼べるのか?

C言語の仕様を極限まで小さくしたCっぽい言語のコンパイラであると言えるとは思います。

そして発想を変えると、ここから出発してC以外の言語のコンパイラに成長させることもできます。

実際に私はここから出発してGo言語コンパイラを作っていますが、今のところうまく動いています。