わらばんし仄聞記

南の国で引きこもってるWeb屋さん

簡易brainf*ckコンパイラを作る part1

pythonでJIT(64bit版Linux環境) part3でも軽く触れていたが、ここまでで作成していたELFファイルや、JITでやっていた事をまとめると、brainf*ckのコードを読み込ませてELFの実行ファイルを作成させる事ができる。要はbrainf*ckコンパイラを作成できる。
ただ、その前にちょっとだけ、ここまででは不足している要素を補足する。

目標

「ELFファイルを作る」で作成していたELFでは外部ライブラリとのリンクを考慮していない。だが、brainf*ckの仕様として、文字の出力と入力が出来る必要がある。それ故、これらの実現の為に両者をシステムコールで直接実行出来るようにする。

コード

※先ほどまではnasmで書いていましたが、これらについてはgasの記法になっています

文字の出力

出力についてはwriteシステムコールを呼び出すことで実現できます。x86-64環境下でのシステムコールの呼び出し方は、raxレジスタへ呼び出したいシステムコールの番号を格納し、更に、その際に渡したい引数を特定のレジスタへ格納します。
システムコールの番号については、自分の環境下では /usr/include/asm-x86/unistd_64.h に記述があり、writeシステムコール

#define __NR_write              1

より、1であることが読み取れます。
また、このシステムコールが必要とする引数はmanコマンドでwriteシステムコールについて見ると

$ man 2 write
...
SYNOPSYS
       # include <unistd.h>
       ssize_t write(int fd, const void *buf, size_t count);
...

とあるので、順に、ファイルディスクリプタ指定、出力対象文字のアドレス、出力する文字数を渡すことになります。
コード中のコメントにもあるように、第一引数はrdi、第二引数はrsi、第三引数はrdxへ格納します。ediやedxを使っているのは単に、あとで生成するネイティブコードのバイト数を減らすためです。
ファイルディスクリプタの値については、シェルをいじっている時にもよくリダイレクト先として指定するあれらの値なので

stdin 0
stdout 1
stderr 2

となります。

さて、コードを見てみると、6行目でまずbss領域に確保されているメモリのアドレスをrbxへ格納し、次行でその先頭位置に0x41(=A)を格納しています。
それ以降はシステムコール呼び出しの為の処理となり、内容はコメントに記述してあります。そして、最後にsyscall命令を実行することで、これらの指定に応じたシステムコールが呼び出される事になります。尚、syscall命令はx86-64での命令で、32bit環境下では別の命令を使うことになります。

文字の入力

先ほどのwriteシステムコールでは文字を出力して終わりでしたが、今回は読み込みの為、正しく読み込めているかの確認のためにreadした値をwriteするといったコードになっています。
入力についてはreadシステムコールを使うことで実現できます。勿論、システムコールの実行手順等はwriteシステムコールの場合と同様なので、その辺りは省略。システムコール番号は

#define __NR_read               0

より、0であることがわかります。また、manコマンドより、コールに必要な引数を見ると

$ man 2 read
...
SYNOPSIS
       #include <unistd.h>
       ssize_t read(int fd, void *buf, size_t count);
...

とあるので、この辺りもほぼwriteと同様ですね。ファイルディスクリプタの指定がstdin(=0)になっているくらい。

実行

write.sの実行

$ gcc write.s
$ ./a.out
A

echo.sの実行

$ gcc echo.s
$ ./a.out
F
F

ということで、目的の結果を得られました。