Undenstanding用于函数调用的auipc + jalr序列

时间:2017-05-13 18:29:25

标签: assembly riscv

我试图阅读由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

让我感到困惑的是抵消0x0c0x10的两条线,这是应该调用函数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); } 的入口点。这里发生了什么?

1 个答案:

答案 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输出更有用。