了解用于函数调用的 auipc+jalr 序列



我试图读取gcc生成的RISC-V程序集,我发现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

让我感到困惑的是偏移量0x0c0x10的两条线,这是应该调用函数id的地方。根据规范,auipc t1,0x0应该将PC + 0x0<<12(等于PC)写入t1,然后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

这些重定位条目告诉链接器以宽松的方式重新定位跳转指令(RISC-V 工具链的默认值)。这意味着如果到目标地址的距离足够短,则允许仅用一条jal指令替换auipc+jalr对。这种替换是有利的,因为它节省了指令,即生成的程序更短。显然,它使重新定位过程有点复杂,因为需要相应地调整以下跳转指令的偏移量。

(可以使用-mno-relaxGCC 标志禁用此功能。

为什么汇编器不能直接发出翻译单元本地不需要重新定位的符号的最终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 输出可以说是更有用的,一般来说。

最新更新