osdev-jpでは、OS開発に有用な情報を収集し公開しています

View My GitHub Profile

OpeLa トップページ

OpeLa コンパイラ(opelac)は当初 x86-64 専用のコンパイラとして開発されましたが、2021/09/03 現在は AArch64 にも対応しています。 このページは OpeLa コンパイラのマルチアーキテクチャ対応状況を説明します。

コンパイラオプション

opelac は -target-arch <arch> オプションで出力アーキテクチャを変更できます。 現在対応しているのは x86_64(デフォルト)と aarch64 です。

AArch64 対応について

AArch64 は Arm プロセッサの動作モードの名前です。64 ビットレジスタが使えるモードです。

opelac が対応しているのは M1 Mac です。 AArch64 が使えるコンピュータは他にも Raspberry Pi 等がありますが、opelac が出力するアセンブラは今のところ Mach-O 形式専用なので、M1 Mac でしか動作しません。 他のアーキテクチャに移植してくれる方を随時募集しています。

マルチアーキテクチャ対応に関する雑記

複数のアーキテクチャに対応する際、考慮すべきポイントは次の通りです。

Mach-O の資料

AArch64 の資料

可変長引数

int printf(const char* format, ...);... は引数省略の記号で、可変長引数を表すのに使われる。 ... には 0 個以上の引数を渡せる。

OpeLa で上記の関数の型は func (format *byte, ...) int のように書く。

SystemV AMD64 ABI では可変長引数かどうかに関わらず、先頭から 6 個まではレジスタ渡し。

AArch64 でよく使われる EABI でも同様らしいが、M1 Mac が採用する ABI はそれとは異なっており、可変長引数は全てスタックで渡す。 printf の例では format が x0 レジスタで渡され、それ以降の引数はスタックで渡す事になる。

AArch64 でのスクラッチレジスタの使用

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