はじめに
以下の資料を参考にcコンパイラを作成中です。x86-64で書かれていますが、arm64に書き換えながら進めています。詰まったところを随時まとめていきます。
こちらが自分の作成中コンパイラです
レジスタの名称
x86-64ではrax
やrdi
を用いるそうですが、arm64ではx0
、x1
...x30
またはw0
を扱います。
四則演算
add x0, x0, x1 # x0 = x0 + x1
sub x0, x0, x1 # x0 = x0 - x1
mul x0, x0, x1 # x0 = x0 * x1
sdiv x0, x0, x1 # x0 = x0 / x1
x86-64の場合は引数2つ(add rax, rdi
)なので、ここが違いですね。arm64の方が直感的?
比較演算
cmp x0, x1 # compare
cset x0, eq # ==
cset x0, ne # !=
cset x0, lt # <
cset x0, le # <=
スタックポインタ
ここが特に違いが大きく苦労しました。参考にした本ではpushとpopからの紹介でしたが、ここではload
とstore
から紹介します。これが原因で手間取った。
load
とstore
スタックレジスタ[sp]
を用います。右のコードのイメージで読むと理解しやすかったです。ここで[sp]
がベースレジスタを表すことに注意してください(オフセットを用いる際に重要になります)。
# load
ldr x0, [sp] # x0 = *sp;
# store
str x0, [sp] # *sp = x0;
push
とpop
# push
str x0, [sp, -16]! # sp -= 16; *sp = x0;
# pop
ldr x1, [sp], 16 # x0 = *sp; x0 += 16;
push
ではアドレスを-16してから値をstore
します。スタックが下に伸びるように設計されているため、こういう挙動になります。[sp, #num]!
でプレインデックスとなります。
pop
では値をload
してからアドレスを+16します。これで上に上がるので、ポップしている形になります。[sp], #num
でポストインデックスとなります。
x86-64ではpush rax
だけなので、理解に手間取りました。ちなみにx86-64では基本的に8ずつアドレスを動かすみたいですが、扱える値域も2^16 -1
と広がるので、今回は16ずつずらす形で実装しています。
プロローグ(2023-3-4追記)
プロローグの役割をあまり理解できていなかったので、記述していませんでした。プロローグではベースレジスタを定義して、変数で使う分のメモリを確保する必要があります。
mov x29, sp # ベースレジスタx29を定義
sub sp, sp, 416 # 変数分のメモリを確保
変数の値をストアする(2023-3-4追記)
オフセットを利用して変数の値を読み出します。前回の内容ですとベースレジスタを定義できていませんでしたので、変数が2つ以上出てくると違うメモリを読み出していましたので、修正します。
ずっとspがベースレジスタだと思っていましたが、消したコードではspが動いてしまいます。動かないベースレジスタをプロローグで定義する必要があることが理解できました。
- str x0, [sp, -offset]!
+ mov x0, x29 # x29がベースレジスタ(プロローグ)
+ sub x0, x0, offset
+ str x0, [sp, -16]!
変数を扱うために、ベースレジスタからの差オフセットを用いて変数のアドレスを参照して値をストアします。これによって変数の値をオフセットを用いてアドレスから管理することができます。
制御構文
制御構文はjump命令を利用して表現します。
if文
arm64ではb
命令で関数にjumpします。if文の方はcmpで比較してb.eq
によってif文を表現します。b.eq
はcmp x0, 0
が正しい時にjumpするというように解釈されます。
cmp x0, 0
b.eq .LelseXXX
... ifの処理 ...
.LelseXXX
... elseの処理 ...
.LendXXX
関数呼び出し
関数を呼び出す際には、元のアドレスをスタックに格納して関数にjumpします。その後、関数からは元のアドレスに返すことによって関数呼び出しを実現します。x86-64ではcall foo
で実現されますが、arm64では、bl foo
とします。
引数なし関数の呼び出し
しかし、これが中々うまくいきませんでした。初めはbl
命令で上記の処理を行なっていると解釈していました(call
ではおそらくそう)。
bl foo
str x0, [sp, -16]! # 戻り値x0をpush
そのため、このように書いていましたが、Segmentation Fault
になってしまっていました。経験上これはメモリ外アクセスだと思いましたが、最初はどこでメモリ外アクセスが起こっているのか分からなかったので、
void foo() { printf("OK\n");}
assert 1 'foo(); return 1;'
このように関数内では標準出力だけを行うように書き換えて実行した結果OK
自体は出力されました。そのため、メモリ外アクセスが関数から戻ってくる際に生じていることが分かりました。
そして調べているうちにこちらに辿り着きました。こちらを読んでようやく理解しましたが、以下の処理を行なっていなかったということです。
関数を呼び出す際には、元のアドレスをスタックに格納して関数にjumpします。
そして以下のように書き換えることによって関数呼び出しを実装することができました。
+ str lr, [sp, -16]! # 元のアドレスを格納する
bl foo # 関数を呼び出し、lrに返ってくる
+ ldr lr, [sp], 16
str x0, [sp, -16]!
参考