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_I
asm 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
が上手く効いてくれないなあ。何か制約があるのだろうか。