我试图读取RISC-V由生成的程序集gcc,我发现它为某些函数调用gcc创建了auipc+序列,jalr但我不明白它是如何工作的。这是一个简单的例子。考虑以下C源文件:
unsigned long id(unsigned long x) {
return x;
}
unsigned long add_one(unsigned long x) {
return id(x)+1;
}
Run Code Online (Sandbox Code Playgroud)
我编译它gcc -O2 -fno-inline -c test.c并得到以下汇编代码:
$ objdump -d test.o
test.o: file format elf64-littleriscv
Disassembly of section .text:
0000000000000000 <id>:
0: 00008067 ret
0000000000000004 <add_one>:
4: ff010113 addi sp,sp,-16
8: 00113423 sd ra,8(sp)
c: 00000317 auipc t1,0x0
10: 000300e7 jalr t1
14: 00813083 ld ra,8(sp)
18: 00150513 addi a0,a0,1
1c: 01010113 addi sp,sp,16
20: 00008067 ret
Run Code Online (Sandbox Code Playgroud)
让我感到困惑的是偏移量0x0c和处的两行0x10,这是函数id应该被调用的地方。根据规范,auipc t1,0x0should write PC + 0x0<<12(等于PC) tot1然后jalr t1(扩展到jalr ra,t1,0)跳转到存储在的地址t1并将返回地址存储到ra。所以我们最终跳到了auipc行(偏移量0x0c),而不是 的入口点id。这里发生了什么?
反汇编目标文件时,auipc/中显示的地址信息jalr是任意的,因为无论如何它都会被链接器重新定位。
您可以在转储重定位信息时看到(添加-r到您的 objdump 调用):
0000000000000000 <id>:
0: 8082 ret
0000000000000002 <add_one>:
2: 1141 addi sp,sp,-16
4: e406 sd ra,8(sp)
6: 00000097 auipc ra,0x0
6: R_RISCV_CALL id
6: R_RISCV_RELAX *ABS*
a: 000080e7 jalr ra # 6 <add_one+0x4>
e: 60a2 ld ra,8(sp)
10: 0505 addi a0,a0,1
12: 0141 addi sp,sp,16
14: 8082 ret
Run Code Online (Sandbox Code Playgroud)
这些重定位条目告诉链接器以轻松的方式重定位跳转指令(RISC-V 工具链的默认设置)。这意味着 只要到目标地址的距离足够短,就可以用一条指令替换auipc+对。这种替换是有利的,因为它节省了指令,即生成的程序更短。显然,这使重定位过程变得有点复杂,因为后续跳转指令的偏移量需要相应调整。jalrjal
(可以使用 GCC 标志禁用此功能-mno-relax。)
为什么汇编器不能直接为不需要重定位的翻译单元本地符号发出最终auipc//指令?毕竟,这些跳跃是与电脑相关的。jalrjal
一般来说,它不能,因为仅使用一个翻译单元的本地视图 1) 到外部符号的轻松重定位可能会更改所有后续到内部符号的偏移量,2) 链接器甚至可能应用一些高级规则,例如内部符号的位置被外部覆盖,因此它确实必须在链接器中重新定位。或者,另一个例子,链接器删除了一个符号。
如果您想查看重定位的地址/偏移量,您必须反汇编链接的二进制文件,例如:
000000000001015c <id>:
1015c: 8082 ret
000000000001015e <add_one>:
1015e: 1141 addi sp,sp,-16
10160: e406 sd ra,8(sp)
10162: ffbff0ef jal ra,1015c <id>
10166: 60a2 ld ra,8(sp)
10168: 0505 addi a0,a0,1
1016a: 0141 addi sp,sp,16
1016c: 8082 ret
Run Code Online (Sandbox Code Playgroud)
正如预期的那样,链接器将auipc+放宽jalr为jal。不幸的是, objdump 不显示原始jal偏移量 -1015c是将偏移量添加到 后的绝对地址10162。1
您可以自行解码第二列的二进制指令来验证:
0xffbff0ef
= 0b11111111101111111111000011101111 | split into the offset parts
=> 1 1111111101 1 11111111 | i.e. off[20], off[10:1], off[11], off[19:12]
| merge them into off[20:1]
=> 0b11111111111111111101 | left-shift by 1
=> 0b111111111111111111010 | sign-extend
=> 0b11111111111111111111111111111010
= -6
=> 0x10162 - 6
= 0x1015c
Run Code Online (Sandbox Code Playgroud)
这与 objdump 输出匹配。
1这意味着 GNU binutils objdump 不显示原始jal偏移量。相比之下,llvm-objdump(LLVM 9 引入了官方 RISC-V 支持)确实显示了原始偏移量:
000000000001015e add_one:
1015e: 41 11 addi sp, sp, -16
10160: 06 e4 sd ra, 8(sp)
10162: ef f0 bf ff jal -6
10166: a2 60 ld ra, 8(sp)
10168: 05 05 addi a0, a0, 1
1016a: 41 01 addi sp, sp, 16
1016c: 82 80 ret
Run Code Online (Sandbox Code Playgroud)
但是,与 GNU binutils objdump 相比,llvm-objdump它不包含生成的绝对地址作为注释。它也没有注释相应的符号。因此,一般来说,GNU binutils objdump 输出可以说更有用。
| 归档时间: |
|
| 查看次数: |
2909 次 |
| 最近记录: |