RISC-Vのコードモデルについていろいろ調べる機会があったのでまとめておく。
参考にしたのは、
- RISC-V Large Code Model Software Workaround
さて、ここまでGCCのオプションによって生成される命令が異なることが分かったが、どのように使い分ければいいのか。重要なのはこの2つのアドレッシングモードが、アドレスとして指定することのできる範囲が異なるということである。それぞれのコードモデルにおいてアクセス可能な最小のアドレスと最大のアドレスを計算してみる。
medlowコードモデル- 最小アドレス:
LUI x1, 0x80000により0x8000_0000となる。つまり、現在のPCから-2GBまでの距離。 - 最大アドレス:
LUI x1, 0x7ffffによりx1に0x8000_0000を格納しLW -1(x1)にアクセスすることで+2GB-1Bの場所までアクセスすることができる。
- 最小アドレス:
medanyコードモデル- 最小アドレス:
AUIPC x1, 0x80000によりPC+0x8000_0000となる。つまり、現在のPCから-2GBまでの距離。 - 最大アドレス:
AUIPC x1, 0x7ffffによりPC+0x7ffff_f000を格納し、LW -1(x1)にアクセスすることで現在のPCから最大で+2GB-1Bの距離までアクセスできる。
- 最小アドレス:
このように、コードモデルに応じてアクセスできる距離が異なる。medanyでは、現在のコードからの距離に応じてアクセスできる範囲が異なる、というのがミソだ。
関数呼び出しにおけるリロケーション情報の挿入
ABIに関する仕様書を再び読み解いていく前に、関数呼び出しについてもチェックしておく。以下のようなプログラムをコンパイルして動作をチェックしておこう。
func_call_main.c
extern void func_call(); int main() { func_call(); }
func_call.c
int global_v; void func_call() { global_v = global_v + 1; }
$ riscv64-unknown-elf-gcc -c -o func_call_main.o func_call_main.c $ riscv64-unknown-elf-gcc -c -o func_call.o func_call.c $ riscv64-unknown-elf-ld -o func_call.riscv func_call_main.o func_call.o
func_call_main.cでは外部で定義されている関数としてfunc_call()を呼び出すが、func_call_main.cをコンパイルする時点でこの関数のアドレスは分かっていない。ではどのようにしてリロケーション情報を挿入しているのか。
$ riscv64-unknown-elf-objdump -D -r func_call_main.o
0000000000000000 <main>:
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
8: 00000097 auipc ra,0x0
8: R_RISCV_CALL func_call
8: R_RISCV_RELAX *ABS*
c: 000080e7 jalr ra
10: 4781 li a5,0
12: 853e mv a0,a5
14: 60a2 ld ra,8(sp)
16: 6402 ld s0,0(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret
R_RISCV_CALLとR_RISCV_RELAXというリロケーション情報が挿入されることが分かった。これはPC相対ジャンプを行うときに挿入される。ABIのマニュアルを読むと、staticモードの場合はR_RISCV_CALLが挿入され、picモードの場合はR_RISCV_CALL_PLTが挿入されることが分かる。
| Enum | ELF Reloc Type | Description | Details |
|---|---|---|---|
| 18 | R_RISCV_CALL | PC-relative call | MACRO call,tail (auipc+jalr pair) |
| 19 | R_RISCV_CALL_PLT | PC-relative call (PLT) | MACRO call,tail (auipc+jalr pair) PIC |
The pseudoinstruction:
call symbol
expands to the following assembly and relocation:
auipc ra, 0 # R_RISCV_CALL (symbol), R_RISCV_RELAX (symbol)
jalr ra, ra, 0
and when -fpic is enabled it expands to:
auipc ra, 0 # R_RISCV_CALL_PLT (symbol), R_RISCV_RELAX (symbol)
jalr ra, ra, 0
では同じファイル内に存在している関数に対して、リロケーション情報を挿入する必要があるか?
extern void func_call(); extern void func_call2(); extern int global_v2; int main() { func_call(); func_call2(); } void func_call2() { global_v2 = global_v2 + 1; }
結果、同様に挿入された。同じファイルの内部であろうがなかろうが、関数ジャンプに対してはリロケーション情報は必ず挿入される。
0000000000000000 <main>:
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
8: 00000097 auipc ra,0x0
8: R_RISCV_CALL func_call
8: R_RISCV_RELAX *ABS*
c: 000080e7 jalr ra
10: 00000097 auipc ra,0x0
10: R_RISCV_CALL func_call2
10: R_RISCV_RELAX *ABS*
14: 000080e7 jalr ra
18: 4781 li a5,0
1a: 853e mv a0,a5
1c: 60a2 ld ra,8(sp)
1e: 6402 ld s0,0(sp)
20: 0141 addi sp,sp,16
22: 8082 ret
グローバル変数のアクセスに関するリロケーション情報の挿入
次はグローバル変数のアクセスについて纏めておく。前回紹介したコードモデルと、ロード命令、ストア命令についてリロケーション情報をまとめておく。
medlowコードモデルの場合、絶対アドレスを用いてアドレスが計算される。アドレスの計算:
LUI+ADDIを使ってアドレスの計算が行われる。リロケーションシンボル:
R_RISCV_HI20+R_RISCV_LO12_Iasm 0: 00000537 lui a0,0x0 0: R_RISCV_HI20 global_v2 0: R_RISCV_RELAX *ABS* 4: 00050513 mv a0,a0 4: R_RISCV_LO12_I global_v2 4: R_RISCV_RELAX *ABS*
メモリロード:
LUI+LW命令を使ってメモリロードが実行される。リロケーションシンボル:
R_RISCV_HI20+R_RISCV_LO12_I(最後の_IはI-Typeの命令を使用することを意味する)asm 4: R_RISCV_LO12_I global_v2 4: R_RISCV_RELAX *ABS* 8: 00078513 mv a0,a5 8: R_RISCV_LO12_I global_v2 8: R_RISCV_RELAX *ABS*
メモリストア:
LUI+SW命令を使ってメモリストアが実行される。- リロケーションシンボル:
R_RISCV_HI20+R_RISCV_LO12_S(最後の_SはS-Typeの命令を使用することを意味する)
- リロケーションシンボル:
medanyコードモデルの場合、PC相対アドレスを用いてアドレスが計算される。アドレスの計算:
AUIPC+ADDIを使ってアドレスの計算が行われる。- リロケーションシンボル:
R_RISCV_PCREL_HI+R_RISCV_PCREL_LO12_I
asm 0: 00000517 auipc a0,0x0 0: R_RISCV_PCREL_HI20 global_v2 0: R_RISCV_RELAX *ABS* 4: 00050513 mv a0,a0 4: R_RISCV_PCREL_LO12_I .L0 4: R_RISCV_RELAX *ABS*- リロケーションシンボル:
メモリロード:
AUIPC+LWを使ってメモリロードが実行される。- リロケーションシンボル:
R_RISCV_PCREL_HI20+R_RISCV_PCREL_LO12_I(最後の_IはI-Typeの命令を使用することを意味する)
- リロケーションシンボル:
メモリストア:
AUIPC+SW命令を使ってメモリストアが実行される。- リロケーションシンボル:
R_RISCV_PCREL_HI20+R_RISCV_PCREL_LO12_S(最後の_SはS-Typeの命令を使用することを意味する)
- リロケーションシンボル:
諸々合わせて表を作っておこう。-fpicを付けたときとそうでないときの場合分けも行った。riscv64-unknown-elf-gccを使ってチェックを行った。
どうもmedanyの時はR_RISCV_PCREL_LO12_Sが上手く効いてくれないなあ。何か制約があるのだろうか。
