rsp

C コンパイラをつくってみる (5)

低レイヤを知りたい人のためのCコンパイラ作成入門 ステップ9-10を参考にしています。

前回 (4)

次回 (6) 仮完結編

ローカル変数

変数を使うには、代入 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-gnuGDB のネイティブデバッグ機能はまだ無いようだし、 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" の部分