3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-25

はじめに

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

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

レジスタの名称

x86-64ではraxrdiを用いるそうですが、arm64ではx0x1...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からの紹介でしたが、ここではloadstoreから紹介します。これが原因で手間取った。

loadstore

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

# load
ldr x0, [sp] # x0 = *sp;
# store
str x0, [sp] # *sp = x0;

pushpop

# 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

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]! 

変数を扱うために、ベースレジスタからの差オフセットを用いて変数のアドレスを参照して値をストアします。これによって変数の値をオフセットを用いてアドレスから管理することができます。
スクリーンショット 2023-02-25 19.06.23.png

制御構文

制御構文はjump命令を利用して表現します。

if文

arm64ではb命令で関数にjumpします。if文の方はcmpで比較してb.eqによってif文を表現します。b.eqcmp 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]!

参考

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?