FPGA開発日記

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

RISC-Vのコードモデルについて(2. コードモデルとRelaxationの挙動まとめ)

RISC-Vのコードモデルについていろいろ調べる機会があったのでまとめておく。

参考にしたのは、

www.sifive.com

  • RISC-V Large Code Model Software Workaround

https://sifive.cdn.prismic.io/sifive%2F15d1d90e-f60e-42a2-b6bf-c805c6c73b0d_riscv-large-code-model-workaround.pdf


さて、ここまで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_CALLR_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が上手く効いてくれないなあ。何か制約があるのだろうか。

f:id:msyksphinz:20200427010552p:plain