はじめに
ターゲットを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(Jump And Link Register) 命令とは
型
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
つまり、まずraにpc+4を代入。
ついで、pcにraを代入して、最下位ビットを0にする(アドレスを2バイトアラインにする)。
意味としては、pcにraを入れているが、
その直前でraにpc+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
おわりに
ジャンプ命令のところが不完全燃焼。
オブジェクトファイルだから、などということもあるらしいので、次回以降、最適化オプションを外しながら見ていく。