arm64でcコンパイラ作成中に詰まったところ(随時更新予定)

# はじめに
以下の資料を参考にcコンパイラを作成中です。x86-64で書かれていますが、arm64に書き換えながら進めています。詰まったところを随時まとめていきます。



https://www.sigbus.info/compilerbook

こちらが自分の作成中コンパイラです


https://github.com/poteto0/1pc

# レジスタの名称
x86-64では`rax`や`rdi`を用いるそうですが、arm64では`x0`、`x1`...`x30`または`w0`を扱います。


# 四則演算
```s
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の方が直感的？

# 比較演算
```s
cmp  x0, x1 # compare
cset x0, eq # ==
cset x0, ne # !=
cset x0, lt # <
cset x0, le # <=
```

# スタックポインタ
ここが特に違いが大きく苦労しました。参考にした本ではpushとpopからの紹介でしたが、ここでは`load`と`store`から紹介します。これが原因で手間取った。

## `load`と`store`
スタックレジスタ`[sp]`を用います。右のコードのイメージで読むと理解しやすかったです。ここで`[sp]`がベースレジスタを表すことに注意してください(オフセットを用いる際に重要になります)。
```s
# load
ldr x0, [sp] # x0 = *sp;
# store
str x0, [sp] # *sp = x0;
```

## `push`と`pop`
```s
# 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`でポストインデックスとなります。


![スクリーンショット 2023-02-25 19.00.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2742882/3a8e391d-669c-1ec6-e787-a7766242e622.png)


x86-64では`push rax`だけなので、理解に手間取りました。ちなみにx86-64では基本的に8ずつアドレスを動かすみたいですが、扱える値域も`2^16 -1`と広がるので、今回は16ずつずらす形で実装しています。

## プロローグ(2023-3-4追記)
プロローグの役割をあまり理解できていなかったので、記述していませんでした。プロローグではベースレジスタを定義して、変数で使う分のメモリを確保する必要があります。
```s
mov x29, sp # ベースレジスタx29を定義
sub sp, sp, 416 # 変数分のメモリを確保
```
## 変数の値をストアする(2023-3-4追記)
オフセットを利用して変数の値を読み出します。前回の内容ですとベースレジスタを定義できていませんでしたので、変数が2つ以上出てくると違うメモリを読み出していましたので、修正します。
ずっとspがベースレジスタだと思っていましたが、消したコードではspが動いてしまいます。動かないベースレジスタをプロローグで定義する必要があることが理解できました。

```diff
- str x0, [sp, -offset]!
+ mov x0, x29 # x29がベースレジスタ(プロローグ)
+ sub x0, x0, offset
+ str x0, [sp, -16]! 
```
変数を扱うために、ベースレジスタからの差オフセットを用いて変数のアドレスを参照して値をストアします。これによって変数の値をオフセットを用いてアドレスから管理することができます。
![スクリーンショット 2023-02-25 19.06.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2742882/4457ba8e-860e-c598-6160-b87ab94e6501.png)

# 制御構文
制御構文はjump命令を利用して表現します。
## if文
arm64では`b`命令で関数にjumpします。if文の方はcmpで比較して`b.eq`によってif文を表現します。`b.eq`は`cmp x0, 0`が正しい時にjumpするというように解釈されます。


https://www.mztn.org/dragon/arm6408cond.html

```s
cmp x0, 0
b.eq .LelseXXX
... ifの処理 ...
.LelseXXX
... elseの処理 ...
.LendXXX
```

# 関数呼び出し
関数を呼び出す際には、元のアドレスをスタックに格納して関数にjumpします。その後、関数からは元のアドレスに返すことによって関数呼び出しを実現します。x86-64では`call foo`で実現されますが、arm64では、`bl foo`とします。


## 引数なし関数の呼び出し
しかし、これが中々うまくいきませんでした。初めは`bl`命令で上記の処理を行なっていると解釈していました(`call`ではおそらくそう)。

```s
bl foo
str x0, [sp, -16]! # 戻り値x0をpush
```
そのため、このように書いていましたが、`Segmentation Fault`になってしまっていました。経験上これはメモリ外アクセスだと思いましたが、最初はどこでメモリ外アクセスが起こっているのか分からなかったので、
```c
void foo() { printf("OK\n");}
```
```sh
assert 1 'foo(); return 1;'
```
このように関数内では標準出力だけを行うように書き換えて実行した結果`OK`自体は出力されました。そのため、メモリ外アクセスが関数から戻ってくる際に生じていることが分かりました。


https://jhalfmoon.com/dbc/2023/01/18/ぐだぐだ低レベルプログラミング99arm64aarach64、所謂call、ret/

そして調べているうちにこちらに辿り着きました。こちらを読んでようやく理解しましたが、以下の処理を行なっていなかったということです。
>関数を呼び出す際には、元のアドレスをスタックに格納して関数にjumpします。


そして以下のように書き換えることによって関数呼び出しを実装することができました。
```diff
+ str lr, [sp, -16]! # 元のアドレスを格納する
  bl foo # 関数を呼び出し、lrに返ってくる
+ ldr lr, [sp], 16
  str x0, [sp, -16]!
```



# 参考
https://godbolt.org


http://nw.tsuda.ac.jp/lec/arm64/mac_m1/