-
Notifications
You must be signed in to change notification settings - Fork 5
OpeLa Compiler MultiArch
OpeLa コンパイラ(opelac)は当初 x86-64 専用のコンパイラとして開発されましたが、2021/09/03 現在は AArch64 にも対応しています。 このページは OpeLa コンパイラのマルチアーキテクチャ対応状況を説明します。
opelac は -target-arch <arch>
オプションで出力アーキテクチャを変更できます。
現在対応しているのは x86_64
(デフォルト)と aarch64
です。
AArch64 は Arm プロセッサの動作モードの名前です。64 ビットレジスタが使えるモードです。
opelac が対応しているのは M1 Mac です。 AArch64 が使えるコンピュータは他にも Raspberry Pi 等がありますが、opelac が出力するアセンブラは今のところ Mach-O 形式専用なので、M1 Mac でしか動作しません。 他のアーキテクチャに移植してくれる方を随時募集しています。
複数のアーキテクチャに対応する際、考慮すべきポイントは次の通りです。
- 命令セット:x86-64 で実行できる命令セットか、AArch64 で実行できる命令セットか
- 実行可能ファイル形式:ELF か Mach-O か
- OS システムコール:どのように OS のシステムコールを呼ぶか
-
Introduction - OS X Assembler Reference: Mac OS X のアセンブラリファレンス。Mach-O バイナリを作るときの参考になる。
-
Assembler Directives: Mach-O のセクションの説明。
__TEXT,__text,regular,pure_instructions
の意味などが載っている。
-
Assembler Directives: Mach-O のセクションの説明。
- A64 Instruction Set Reference
- ARM64 ABI 規則の概要: Microsoft による一般的な ARM64 ABI の説明
- herumi さんの AArch64 呼び出し規約の記事
- Writing ARM64 Code for Apple Platforms: Mac 特有の ARM64 ABI の説明
- Arm の ABI に関する公式ドキュメントリンク集
int printf(const char* format, ...);
の ...
は引数省略の記号で、可変長引数を表すのに使われる。
...
には 0 個以上の引数を渡せる。
OpeLa で上記の関数の型は func (format *byte, ...) int
のように書く。
SystemV AMD64 ABI では可変長引数かどうかに関わらず、先頭から 6 個まではレジスタ渡し。
AArch64 でよく使われる EABI でも同様らしいが、M1 Mac が採用する ABI はそれとは異なっており、可変長引数は全てスタックで渡す。
printf
の例では format
が x0 レジスタで渡され、それ以降の引数はスタックで渡す事になる。
AArch64 の乗算命令(MUL)は即値を指定できないため、x = a * 即値
のような演算をしたい場合、即値をレジスタに格納してからレジスタ間乗算を行う。
一時的にレジスタが必要となるが、どのレジスタが空いているかは free_calc_regs
で管理しており、今のところこの変数を asmgen
のメソッドに渡さない実装になっているため、asmgen
側から空きレジスタを把握することができない。
いずれかのレジスタをスタックに保存して一時的に使うというのが一つの方法。
実際、OpeLa コンパイラ Ver.2 ではこの方法を使っている時期があった。参照:Mul64
の中で push/pop するコード
しかし AArch64 は x86-64 に比べて多くの汎用レジスタがあるのだから、それらを使わない手は無い。
OpeLa Ver.2 は x86-64 と AArch64 でコード生成を共通化するために、AArch64 のレジスタの多く(r6, r7, r10~r18、r24~r28)を使わない設計になっている。
裏を返せば、OpeLa のコード生成プログラム(GenerateAsm 関数)はそれらのレジスタを利用しないということだから、asmgen
の中で勝手にそれらのレジスタを使っても競合しない。では、使われないレジスタのうち、どのレジスタを使うのが最適だろうか。
herumi さんの AArch64 呼び出し規約の記事 によれば、x16 と x17 はプロシージャ内呼び出しスクラッチレジスタであり、「リンカが挿入する小さいコード(veneer)や共有ライブラリのシンボル解決に利用するPLT(procedure linkage table)コードなどで利用」されるとのことだ。Arm 公式のドキュメント には、x16 と x17 について「intra-procedure-call scratch register (can be used by call veneers and PLT code); at other times may be used as a temporary register.」とある。関数呼び出し時にちょこっと処理を追加するような目的で使うが、その他の場面では一時レジスタとして使って良い、ということだ。したがって、今回の目的に最適だと思う。
スタックを使う方法とスクラッチレジスタを活用する方法で、生成される機械語はどう変わるだろう。func main() *int { var a *int; return a + 2; }
というコードをコンパイルして機械語の変化を見た。
ldr x0, [x29, #-8] # ポインタ a の読み出し
mov x1, #2 # a に加算する値(2)をレジスタに設定
str x2, [sp, #-16]! # x2 をスタックに push
mov x2, #8 # sizeof(int) = 8
mul x1, x1, x2 # 2×sizeof(int) を計算
ldr x2, [sp], #16 # x2 をスタックから復帰
add x0, x0, x1 # a に 2×sizeof(int) を加算
asmgen
側では x2 が使用中なのか空いているのかを判断できないため、安全側に倒してスタックに保存する。これが、スクラッチレジスタを使うようにしたら次のようになった。
ldr x0, [x29, #-8] # ポインタ a の読み出し
mov x1, #2 # a に加算する値(2)をレジスタに設定
mov x16, #8 # sizeof(int) = 8
mul x1, x1, x16 # 2×sizeof(int) を計算
add x0, x0, x1 # a に 2×sizeof(int) を加算
x16 は GenerateAsm のコード生成処理で使われないため、スタックに保存せず利用でき、コードがシンプルになった。この最適化を導入したコミットは 0cbd656ab6eb80d60927e22e52450c96942afce1