C コンパイラをつくってみる (5)
低レイヤを知りたい人のためのCコンパイラ作成入門 ステップ9-10を参考にしています。
ローカル変数
変数を使うには、代入 a = 1
とそれを使った計算 a + 1
などができないと話にならないので、先に文法に 文はセミコロンで終端される 規則を入れた*1。
stmt := assignment ";"
アルファベット小文字1文字トークンからなるローカル変数を実現するために、スタックフレームの大きさを(a
から z
までの分で)固定。 s0
レジスタ(x86でいうところのebp
)*2 からのオフセットでアクセスする。
スタックフレームは、main の開始時に確保(sp
からフレームサイズを引く)し、ret
する直前に解放(sp
を戻す)する。
(抽象構文木中の)変数ノードは、「そのアドレスを計算してスタックにプッシュする」アセンブリを生成する。
size_t const offset = ('z' - id_name + 1) * sizeof_variable; std::cout << // ローカル変数のアドレスを s0 からのオフセットで計算 " mv a0, s0\n" " addi a0, a0, -" << offset << "\n" // スタックにプッシュ " addi sp, sp, -" << sizeof_variable << "\n" " sd a0, (sp)\n";
変数の値がほしい場面ではデリファレンスする。
std::cout << // スタックからアドレスをポップ " ld a0, (sp)\n" // そのアドレスから値をロード " ld a0, (a0)\n" // スタックの同じ箇所に上書き " sd a0, (sp)\n";
(抽象構文木中の)代入演算子ノードは、上記のアドレス計算と、右辺の値の計算を終わらせ、スタックからオペランドを2つとってストアする。
std::cout << // 2つポップ (a0: ストア先アドレス, a1: ストアされる値) " ld a1, (sp)\n" " ld a0, " << sizeof_variable << "(sp)\n" " addi sp, sp, " << sizeof_variable*2 << "\n" // ストアする " sd a1, (a0)\n" // おなじ値をスタックにプッシュ(多重代入 e.g. a=b=1 のため) " addi sp, sp, -" << sizeof_variable << "\n" " sd a1, (sp)\n";
以下のような計算ができるようになった。
a = 2; c = 3; z = 4; a + c + z + 1; => 10
return
文
文の定義を
stmt := assignment ";"
から
stmt := "return" assignment ";" | assignment ";"
に変更し、
字句解析側で return[^a-zA-Z0-9]
*3 をみつけて、ひとつのトークンとして扱うことで、return
文を実装した。
コード生成としては、 return なにか;
が来たらすぐさま、main の終わり方と同じく ret
命令を含む コードを出力する。
以下のように、一度目の return
以後は評価されないことを確かめられた。
return 1; return 2; => 1
余談
デバッグ環境が欲しくなったけれど、 riscv64-linux-gnu
で GDB のネイティブデバッグ機能はまだ無いようだし、 gdbserver
もビルド中に Not Supported と言われてしまう。
basic Linux application support (中略) will be in the next (8.3) release. https://riscv.org/software-status/#debugging
gdb-8.3 には入るのかな? どう発表されたら native デバッグができることになるのか、よく分からない。
参考文献
*1:if 文 や for 文 で破綻しそう
*2:Calling Convention には s0/fp と表記があるが、fp ではアセンブルできなかった。fp のほうが役割を表していてわかりやすいのだが…
*3:の、"return" の部分