FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V におけるメモリモデルについて

f:id:msyksphinz:20180421185728p:plain

RISC-Vのコンパイル時に登場する謎のメモリモデルについて調査したのでまとめておく。

以下の資料を参考にした。

  • All Aboard, Part 4: The RISC-V Code Models

www.sifive.com

RISC-Vはコード内をジャンプするための手法としては複数の手段があるのだが、他のプロセッサアーキテクチャと比較すると決しては多いわけではない。

RISC-Vの場合は以下の3種類に限定される。

  • PC相対 (auipc / jal / br*命令)
  • レジスタ相対 (jalr / addi命令)
  • 絶対 (lui命令)

gccによりプログラムがコンパイルされる際には、ジャンプ先やメモリアクセス先はどこに配置されるのかは分からない。 これは、実際にリンクしてみるまで分からないので、オブジェクトを作成する段階では、とりあえずアクセス先の情報を空に設定しておき、リンカによる再設定を行う。

gccでは、オブジェクトのコンパイルされてからリンクされるまでのコードの変遷を見るための便利なオプションが用意されている。--save-tempsというオプションだ。

例えば、上記のサイトで紹介されている以下のようなプログラムをコンパイルしてみる。

  • cmodel.c
long global_symbol[2];

int main() {
  return global_symbol[0] != 0;
}

以下のようにしてコンパイルをしてみる。

$ riscv64-unknown-elf-gcc cmodel.c -o cmodel -O3 --save-temps

結果、以下のようなファイルが生成された。

$ ls -1 | grep cmodel
cmodel
cmodel.c
cmodel.i
cmodel.o
cmodel.s

cmodel.iファイルはプリプロセッサを通過させただけなので面白くない。次にcmodel.sはどのようになっているだろうか。

    .file   "cmodel.c"
    .option nopic
    .section    .text.startup,"ax",@progbits
    .align  1
    .globl  main
    .type   main, @function
main:
    lui     a5,%hi(global_symbol)
    ld      a0,%lo(global_symbol)(a5)
    snez    a0,a0
    ret
    .size   main, .-main
    .comm   global_symbol,16,8
    .ident  "GCC: (GNU) 7.2.0"

オブジェクト形式として保存されたcmodel.oをダンプしてみると、以下のようになっていることが分かった。

$ riscv64-unknown-elf-objdump -dtr cmodel.o

cmodel.o:     file format elf64-littleriscv
...
0000000000000010       O *COM*  0000000000000008 global_symbol
...
0000000000000000 <main>:
   0:   000007b7                lui     a5,0x0
                           0: R_RISCV_HI20 global_symbol
                           0: R_RISCV_RELAX        *ABS*
   4:   0007b503                ld      a0,0(a5) # 0 <main>
                           4: R_RISCV_LO12_I       global_symbol
                           4: R_RISCV_RELAX        *ABS*
   8:   00a03533                snez    a0,a0
   c:   8082

実際にglobal_symbolのアドレスは決まっていないため、アセンブラはこの変数の場所をまだ決めることが出来ず、 その代わりに上記のような目印を配置している。 そしてオブジェクトにリロケーションテーブルを作成し、最終的にリンカがアドレスを決める際にどの命令のどの場所の 変数のアドレス情報を置き換えれば良いかが分かるようになっている。

最終的に生成された実行ファイルを同様にobjdumpすると、以下のようにアドレスが確定されていることが見て取れる。

$ riscv64-unknown-elf-objdump -dtr cmodel
Disassembly of section .text:

0000000000010330 <main>:
   10330:       67c9                    lui     a5,0x12
   10332:       0387b503                ld      a0,56(a5) # 12038 <global_symbol>
   10336:       00a03533                snez    a0,a0
   1033a:       8082                    ret

ここまでが、RISC-Vにおけるデフォルトのコード生成モデルであるmedlowの試行結果である。 一方で、RISC-Vにはもう一つのコード生成モデルであるmedanyというモデルがある。

これも同様にコンパイルして、その結果をダンプしてみよう。

$ riscv64-unknown-elf-gcc -mcmodel=medany cmodel.c -o cmodel -O3 --save-temps
$ riscv64-unknown-elf-objdump -dtr cmodel.o

...

Disassembly of section .text.startup:

0000000000000000 <main>:
   0:   00000797                auipc   a5,0x0
                           0: R_RISCV_PCREL_HI20   global_symbol
                           0: R_RISCV_RELAX        *ABS*
   4:   00078793                mv      a5,a5
                           4: R_RISCV_PCREL_LO12_I .L0
                           4: R_RISCV_RELAX        *ABS*
   8:   6388                    ld      a0,0(a5)
   a:   00a03533                snez    a0,a0
   e:   8082                    ret

先程の結果と少し変わっている。 medlowのコンパイルではR_RISCV_HI20とされていたものが、medanyではR_RISCV_PCREL_HI20と変わっているし、 R_RISCV_LO12_I とされていたものが R_RISCV_PCREL_LO12_I と変わっている。

まとめると、以下のようになる。

  • コードモデルmedlowの場合 lui / ld が生成される
  • コードモデルmedanyの場合 auipc / ld が生成される

-mcmodel=medlowの場合

medium-lowコードモデルであることを示す。アドレス参照の範囲は、絶対アドレスとして-2GBから+2GBまでの間である。 命令生成時には、lui/addi 命令を用いてアドレスが生成される。

-mcmodel=medanyの場合

medium-anyコードモデルであることを示す。アドレス参照の範囲は現在のPCの位置から2GBの範囲に限定される。 命令生成時には、 auipc / ld命令が使用される。

ここで注意しなければならないのは、コードモデルはABIとは異なるということだ。 ABIは関数呼び出しのレジスタ使用などの規約を意味しており、コンパイル時に関数の引数の受け渡しを統一するために必要であるが、 コードモデルの場合は異なるコードモデルの関数同士をリンクして一つの実行ファイルにすることができる。 関数同士のインタフェースとは無関係のため、コンパイル時に自由に決めることが可能となる。