Cコンパイラ入門①

スタンフォードの方がCコンパイラ入門の記事?を書かれていた。

www.sigbus.info

上のドキュメントを参考に、Cコンパイラを作成する。

ステップ1:整数1個をコンパイルする言語の作成

C言語の9cc.cを作成。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
  if (argc != 2) {
    fprintf(stderr, "引数の個数が正しくありません\n");
    return 1;
  }

  printf(".intel_syntax noprefix\n");
  printf(".global main\n");
  printf("main:\n");
  printf("  mov rax, %d\n", atoi(argv[1]));
  printf("  ret\n");
  return 0;
}

gccコマンドで実行ファイル生成。

[root@localhost 9cc]# gcc -o 9cc 9cc.c


9ccという実行ファイルが生成される。

[root@localhost 9cc]# ls
9cc  9cc.c


9ccを実行する。上のC言語のコードに従って、引数は1つ指定する。
アセンブリが生成されて標準出力に吐かれる。

[root@localhost 9cc]# ./9cc 1
.intel_syntax noprefix
.global main
main:
  mov rax, 1
  ret

これを、.s拡張子のファイルに保存。

[root@localhost 9cc]# ./9cc 1 > 9cc.s

中身を確認。

[root@localhost 9cc]# cat 9cc.s
.intel_syntax noprefix
.global main
main:
  mov rax, 1
  ret

ここまで、9cc.c → 9cc.sに変換した。
さらにこの.sアセンブリファイルを実行ファイル化する。
gcc(cc)は、与えられたファイルの拡張しで言語を判定してコンパイラアセンブラを起動する。

[root@localhost 9cc]# gcc -o 9cc 9cc.s

生成した実行ファイル9ccを実行して、リターンコードを確認。

[root@localhost 9cc]# ./9cc
[root@localhost 9cc]# echo $?
1
[root@localhost 9cc]#

ここまで、9cc.c → 9cc.s(tmp.sがファイル名としては正しかった。下で実行するテストスクリプトでtmp.sファイルを呼び出すため、ここで名前を変更しておくこと) → 9ccと変換し、実行した。


上述のtmp.sが存在する前提で、テストスクリプトを実行する。

[root@localhost 9cc]# ./test.sh
0 => 0
42 => 42
[ OK ]
[root@localhost 9cc]#


やっていることは、以下。

[root@localhost 9cc]# bash -x ./test.sh
+ try 0 0
+ expected=0
+ input=0
+ ./9cc 0
+ gcc -o tmp tmp.s
+ ./tmp
+ actual=0
+ '[' 0 = 0 ']'
+ echo '0 => 0'
0 => 0
+ try 42 42
+ expected=42
+ input=42
+ ./9cc 42
+ gcc -o tmp tmp.s
+ ./tmp
+ actual=42
+ '[' 42 = 42 ']'
+ echo '42 => 42'
42 => 42
+ echo '[' OK ']'
[ OK ]

なので、このテストをするには、gccでcファイルをアセンブリに変換しておく必要がある。


そこでmakeを利用。
今回利用するmakefileはこちら。Makefikeは9ccやtest.shファイルと同じディレクトリ内に用意する。


9cc: 9cc.c

test: 9cc
        ./test.sh

clean:
        rm -f 9cc *.o *~ tmp*

makeコマンド単品もしくはmake 9ccコマンドで自動的に実行ファイル9ccが生成される。
また、make testコマンドで./test.shが実行され、make cleanコマンドで中間ファイルが削除される。
Make testを実行前に、make (make 9cc)を実行する必要はない。実行対象の9ccファイルが、9cc.cよりも古い場合に限って、make (make 9cc)コマンドが実行される。ありがたい。


作成物をgithubで管理する。

[root@localhost 9cc]# git add ./*
[root@localhost 9cc]# git commit -m "new commit "
[master ef67cd4] new commit
 3 files changed, 45 insertions(+), 15 deletions(-)
 create mode 100644 sample.sh
 create mode 100644 test.c
[root@localhost 9cc]# git push origin master
Username for 'https://github.com':
Password for 'https://xxxx@github.com':
Counting objects: 7, done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 759 bytes | 0 bytes/s, done.
Total 5 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/xxxxx/compiler.git
   fe76126..ef67cd4  master -> master
[root@localhost 9cc]# ls
9cc.c  Makefile  old  sample.sh  test.c  test.sh
[root@localhost 9cc]#
[root@localhost 9cc]#
[root@localhost 9cc]#


そいえばクロスコンパイラという言葉を知らなかった。
そもそもコンパイラ動作マシンをホスト、ホストが出力したコードを実際に動かすマシンをターゲットと呼ぶらしい。今回はコンパイラ生成元と動作元は同一だが、これが異なるコンパイラのことをクロスコンパイラと呼ぶのだそう。
マイコンで動かす実行ファイルを生成するために、WinやMacで利用するコンパイラはクロスコンパイラ。


ステップ2:加減算のできるコンパイラの作成

ステップ1のコンパイラを拡張して、加減算ができるようにする。
最初のステップはリターンコードだけだった。

コンパイルする
ガイドに従って、アセンブラをベタ書きして、実行してみよう。

[root@localhost 9cc]# vi tmp.s
[root@localhost 9cc]# cat tmp.s
.intel_syntax noprefix
.global main

main:
	mov rax, 5
	add rax, 20
	sub rax, 4
	ret
[root@localhost 9cc]# gcc -o tmp tmp.s
[root@localhost 9cc]# ./tmp
[root@localhost 9cc]# echo $?
21
[root@localhost 9cc]#

5+20-4が実行され、21がリターンコードとして表示された。

C言語に落とす前に、今回実装する加減算について定義している。
===========
さて、このアセンブリファイルはどのように作成すればいいのでしょうか? この加減算のある式を「言語」として考えてみると、この言語は次のように定義することができます。
\* 最初に数字が1つある
\* そのあとに0個以上の「項」が続いている
\* 項というのは、+の後に数字が来ているものか、-の後に数字が来ているものである
===========


Strol関数がよくわからず。こちらが参考になった。
http://www9.plala.or.jp/sgwr-t/lib/strtol.html

以下テスト実装。

[root@localhost 9cc]# cat test.c
# include <stdio.h>
# include <stdlib.h>

int main(int argc, char **argv) {
	char *p = argv[1];
	printf("arg is ... %s\n",p);
	printf("FIRST NUM => %ld\n", strtol(p, &p, 10));
	while (*p) {
		if ( *p == '+' ) {
			p++;
			printf("PLUS .... %ld\n", strtol(p, &p, 10));
			printf("%s\n",p);
			continue;
		}

	}

}

Strol関数は第一引数を対象に、第三引数を基数としてlong型の4byteの符号付整数に変換する。
第二引数は変換不可能な文字列のポインタの格納先。
以下の結果の通り、strol関数の第二引数で指定したポインタ&pに変換不可能な文字列を格納しており、+に接触する都度、+以降が格納されていることがわかる。

[root@localhost 9cc]# ./a.out 1+2+3+4+5+6+7+8+9
arg is ... 1+2+3+4+5+6+7+8+9
FIRST NUM => 1
PLUS .... 2
+3+4+5+6+7+8+9
PLUS .... 3
+4+5+6+7+8+9
PLUS .... 4
+5+6+7+8+9
PLUS .... 5
+6+7+8+9
PLUS .... 6
+7+8+9
PLUS .... 7
+8+9
PLUS .... 8
+9
PLUS .... 9

コード中のp++;はなんだろうか。
以下は、BEFOREとAFTERの間でp++;を実装した例。

BEFORE: +8+9
AFTER: 8+9

なるほど、最初の文字が取り除かれていた。



続いて実行ファイルを生成する。

[root@localhost 9cc]# make
cc     9cc.c   -o 9cc

加減算のアセンブリを生成する。

[root@localhost 9cc]# ./9cc 1+5-2
.intel_syntax noprefix
.global main
main:
	mov rax, 1
	add rax, 5
	sub rax, 2
	ret
[root@localhost 9cc]#

うまく生成されていそうだ。
以下をtest.shに追記。

try 21 5+20-4

テスト。

[root@localhost 9cc]# make
cc     9cc.c   -o 9cc
[root@localhost 9cc]# make test
./test.sh
5+20-4 => 21
0 => 0
42 => 42
[ OK ]
[root@localhost 9cc]#

成功した。
これもgithubにプッシュ。

一旦ここまで。