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
というオプションだ。
例えば、上記のサイトで紹介されている以下のようなプログラムをコンパイルしてみる。
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は関数呼び出しのレジスタ使用などの規約を意味しており、コンパイル時に関数の引数の受け渡しを統一するために必要であるが、
コードモデルの場合は異なるコードモデルの関数同士をリンクして一つの実行ファイルにすることができる。
関数同士のインタフェースとは無関係のため、コンパイル時に自由に決めることが可能となる。