我试图阅读由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;
}
我用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
让我感到困惑的是抵消0x0c
和0x10
的两条线,这是应该调用函数id
的地方。根据{{3}},auipc t1,0x0
应将PC + 0x0<<12
(等于PC
)写入t1
,然后jalr t1
(将其扩展为jalr ra,t1,0
t1
)跳转到ra
中存储的地址,并将返回地址存储到auipc
。因此,我们最终跳转到0x0c
行(偏移id
),而不是public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.print("What is your name? "); //asks question first,
String first = scan.next(); //THEN accepts first
String last = scan.next(); //and last name
String newFirst = first.substring(1);
String newLast = last.substring(1);
playGame(first, newFirst);
playGame(last, newLast);
}
的入口点。这里发生了什么?
答案 0 :(得分:2)
反汇编目标文件时,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
这些重定位条目告诉链接器以轻松的方式(RISC-V工具链的默认设置)重定位跳转指令。这意味着只要到目标地址的距离足够短,就可以只用一条auipc
指令来替换jalr
+ jal
对。这样的替换是有利的,因为它节省了指令,即所生成的程序更短。显然,这会使重新定位过程复杂化一点,因为后续跳转指令的偏移量需要相应地进行调整。
(可以使用-mno-relax
GCC标志禁用它。)
为什么汇编程序无法直接为不需要重新定位的翻译单元本地符号发出最终的auipc
/ jalr
/ jal
指令?毕竟,这些跳跃是相对于PC的。
通常不能这样,因为仅使用一个翻译单元的局部视图即可)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
如预期的那样,链接器将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
与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
但是,与GNU binutils objdump相比,llvm-objdump
不包括结果绝对地址作为注释。它也不会注释相应的符号。因此,一般而言,可以说GNU binutils objdump输出更有用。