はじめに

ターゲットをRiscVにして作成したELFファイルのテキスト領域を解釈する、というのをもう一度行う。

今度は少しだけ複雑にするが、まだ簡単である。結果だけ載せる。

題材1

Cコード

int add(int a, int b) { return a + b; }

コマンド

FILE=add_sub_risc;
clang --target=riscv64-linux-musl -static -c -o $FILE.o -fuse-ld=lld $FILE.c
riscv64-unknown-elf-objdump --disassemble $FILE.o

オブジェクトファイルのRiscV命令列

0000000000000000 <add>:
   0:   1101                    addi    sp,sp,-32
   2:   ec06                    sd      ra,24(sp)
   4:   e822                    sd      s0,16(sp)
   6:   1000                    addi    s0,sp,32
   8:   fea42623                sw      a0,-20(s0)
   c:   feb42423                sw      a1,-24(s0)
  10:   fec42503                lw      a0,-20(s0)
  14:   fe842583                lw      a1,-24(s0)
  18:   9d2d                    addw    a0,a0,a1
  1a:   60e2                    ld      ra,24(sp)
  1c:   6442                    ld      s0,16(sp)
  1e:   6105                    addi    sp,sp,32
  20:   8082                    ret

解釈

関数のセットアップ処理

sp -= 32
[sp + 24] = ra
[sp + 16] = s0
s0 = sp + 32

関数の処理

変数int aがs0-20に対応。 a0の値をaに保存。aは4byteなので、32bit。

[s0 - 20] = a0[31:0]

変数int bがs0-24に対応(変数bの参照が[s0-24]に対応)。

a1の値をbに保存。変数aは4byteなので、変数bはaの位置より4byte下に設定する。

[s0 - 24] = a1[31:0]

a0レジスタという、関数実行時には1番目引数、終了時には返り値を表すレジスタに、変数aの値を代入。

a0 = [s0 - 20][31:0]

a1レジスタに、変数bの値を代入。

a1 = [s0 - 24][31:0]

a0レジスタに、2番目引数を表すa1レジスタの値を加算。 a0は返り値レジスタだが、a0レジスタで直接計算。

別のレジスタで計算してからa0にロードするのではなく、そのままa0レジスタで計算。

a0 += a1

関数の後処理(エピローグというらしい)

ra = [sp + 24]
s0 = [sp + 16]
sp += 32
ret

RiscVレジスタメモ

a0

レジスタ番号: x10

関数実行時には、第一引数を表す。

また、ret時には関数の返り値を表す(a0レジスタに値を設定してretを行うと、返り値としてa0レジスタの値が返る。)。

a1-a7

レジスタ番号: x11-x17

第2-第8引数。a1は第2返り値に利用されることもあるらしい。

題材2

Cコード

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return add(a, -b); }

コマンド

FILE=add_sub_risc;
clang --target=riscv64-linux-musl -static -c -o $FILE.o -fuse-ld=lld $FILE.c
riscv64-unknown-elf-objdump --disassemble $FILE.o

オブジェクトファイルのRiscV命令列

Disassembly of section .text:

0000000000000000 <add>:
   0:   1101                    addi    sp,sp,-32
   2:   ec06                    sd      ra,24(sp)
   4:   e822                    sd      s0,16(sp)
   6:   1000                    addi    s0,sp,32
   8:   fea42623                sw      a0,-20(s0)
   c:   feb42423                sw      a1,-24(s0)
  10:   fec42503                lw      a0,-20(s0)
  14:   fe842583                lw      a1,-24(s0)
  18:   9d2d                    addw    a0,a0,a1
  1a:   60e2                    ld      ra,24(sp)
  1c:   6442                    ld      s0,16(sp)
  1e:   6105                    addi    sp,sp,32
  20:   8082                    ret

0000000000000022 <sub>:
  22:   1101                    addi    sp,sp,-32
  24:   ec06                    sd      ra,24(sp)
  26:   e822                    sd      s0,16(sp)
  28:   1000                    addi    s0,sp,32
  2a:   fea42623                sw      a0,-20(s0)
  2e:   feb42423                sw      a1,-24(s0)
  32:   fec42503                lw      a0,-20(s0)
  36:   fe842603                lw      a2,-24(s0)
  3a:   4581                    li      a1,0
  3c:   9d91                    subw    a1,a1,a2
  3e:   00000097                auipc   ra,0x0
  42:   000080e7                jalr    ra # 3e <sub+0x1c>
  46:   60e2                    ld      ra,24(sp)
  48:   6442                    ld      s0,16(sp)
  4a:   6105                    addi    sp,sp,32
  4c:   8082                    ret

解釈

sub関数についてみていく

関数セットアップ処理

  22:   1101                    addi    sp,sp,-32
  24:   ec06                    sd      ra,24(sp)
  26:   e822                    sd      s0,16(sp)
  28:   1000                    addi    s0,sp,32

関数の処理

第1引数を、a変数(メモリ)に保存。

[s0 - 20] = a0[31:0]

第2引数を、b変数(メモリ)に保存。

[s0 - 24] = a1[31:0]

返り値のa0レジスタに、a変数の値を保存。

a0 = [s0 - 20][31:0]

a2レジスタ(一時保管用)に、b変数の値を保存。

a2 = [s0 - 24][31:0]

a1レジスタに0を保存。

a1 = 0

a1レジスタにおいて、a2レジスタの値だけ減算して保存。

ここで、a1レジスタには元々0が設定されているので、a1レジスタにはマイナスa2、すなわちマイナスbが保存される。

a1 -= a2[31:0]

add関数を実行するための準備 auipc(Add Upper Immediate to PC) 命令とは

次のような構文。

rd = pc + (imm << 12)

今回だと、rdはraレジスタ、immは0x0なので、下記になる。

ra = pc

jalr rd, rs1, imm

意味

rd = pc + 4
pc = (rs1 + imm) & ~1

なお、~はビットの反転を表すので、~1は64ビット版のRiscVにおいて、64'b1111111111111111111111111111111111111111111111111111111111111110ということ。

今後は、わかりやすく~64'd1で表す(Verilog風)。

なので、& ~1は、最下位ビットを0にする、ことを意味する。

今回の話にもどると、下記になるらしい。

jalr ra, ra, 0

つまり、まずrapc+4を代入。

ついで、pcraを代入して、最下位ビットを0にする(アドレスを2バイトアラインにする)。

意味としては、pcraを入れているが、 その直前でrapc+4を入れているので、純粋にpcを+4して(加えて2byteアラインしてのみ)いるように見える。

どうやら、この部分はオブジェクトファイル状態ではまだ、add関数に紐付はしないらしい。

この場所で最終的にadd関数にジャンプしてadd関数が実行され戻ってきたときには、結果がa0に格納される。

Cの実装としてadd関数の返り値をそのままsub関数の返り値にしているので、a0レジスタに入れたまま終了して良い。

関数の後処理(エピローグ)

いつもと同じ。

  46:   60e2                    ld      ra,24(sp)
  48:   6442                    ld      s0,16(sp)
  4a:   6105                    addi    sp,sp,32
  4c:   8082                    ret

おわりに

ジャンプ命令のところが不完全燃焼。

オブジェクトファイルだから、などということもあるらしいので、次回以降、最適化オプションを外しながら見ていく。