Linker Relaxation
Table of Contents
1. Linker Relaxation
https://www.sifive.com/blog/all-aboard-part-3-linker-relaxation-in-riscv-toolchain (local archive)
https://riscv.org/wp-content/uploads/2019/03/11.15-Shiva-Chen-Compiler-Support-For-Linker-Relaxation-in-RISC-V-2019-03-13.pdf (local archive)
gcc 编译和优化是以单个 object file 为单位, 导致有些 symbol 的地址无法确定, gcc 必须通过 `auipc/lui` + `add/load/store/jalr` 多条指令的方式确保 symbol 在很 `远` 的情况下也能访问, 但链接后可能发现并不需要多条指令.
linker relaxation 是一种链接阶段的优化, 用于解决上面的问题
riscv 中的 linker relaxation 主要有几种方式:
- lui+(addi,ld,sw…) 替换为 gp/tp+addi,ld,sw…
- auipc+(addi,ld,sw…) 替换为 gp/tp+addi,ld,sw…
- auipc+jalr 替换为 jal
- 根据 reloc 中的 align 信息重新调整 align
- 把一些指令替换成 c 指令, 例如把 jal 替换为 c.jal
另外, 是否能使用 gp/tp 也取决于符号的位置, 例如:
- 如果 symbol 在 .tdata 中, 使用 tp 做基址寄存器, 以支持 elf_tls
- 如果 symbol 在 __global_pointer$ 附近, 则可以用 gp 做为基址寄存器
1.1. example
1.1.1. lui relaxation
test.S:
.global _start .section .text _start: lui a1,%hi(here) addi a1, a1, %lo(here) .section .data .rept (1<<10) .byte 0 .endr here: ret
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -nostdlib
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D a.out
Disassembly of section .text:
00000000000100b0 <_start>:
100b0: c0018593 addi a1,gp,-1024 # 114b4 <here>
Disassembly of section .data:
00000000000110b4 <__DATA_BEGIN__>:
...
00000000000114b4 <here>:
114b4: 8082 ret
1.1.2. jalr relaxation
test.S:
.global _start .section .text _start: call near call far .rept (1<<18)-8 .word 0 .endr near: ret .rept 10 .word 0 .endr far: ret
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -c
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D test.o
Disassembly of section .text:
0000000000000000 <_start>:
0: 00000097 auipc ra,0x0
4: 000080e7 jalr ra # 0 <_start>
8: 00000097 auipc ra,0x0
c: 000080e7 jalr ra # 8 <_start+0x8>
...
00000000000ffff0 <near>:
ffff0: 8082 ret
...
000000000010001a <far>:
10001a: 8082 ret
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -nostdlib -march=rv64g
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D a.out
Disassembly of section .text:
0000000000010078 <_start>:
10078: 7edff0ef jal ra,110064 <near>
1007c: 00100097 auipc ra,0x100
10080: 014080e7 jalr 20(ra) # 110090 <far>
...
0000000000110064 <near>:
110064: 00008067 ret
...
0000000000110090 <far>:
110090: 00008067 ret
1.1.3. gp relaxation
test.S:
.global _start .section .text _start: la a1,here .section .data .rept (1<<19)+1 .byte 0 .endr .section .sdata here: ret
test.lds:
SECTIONS
{
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
.sdata : { *(.sdata); __global_pointer$ = .;}
}
没有经过 linker relaxation 的 obj 文件:
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -c
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D ./test.o
Disassembly of section .text:
0000000000000000 <_start>:
# NOTE: 这里显示 0x0 及 mv a1, a1 是因为相关操作数是 0
# mv a1, a1 实际上是 addi a1, a1, 0
0: 00000597 auipc a1,0x0
4: 00058593 mv a1,a1
Disassembly of section .data:
0000000000000000 <.data>:
...
Disassembly of section .sdata:
0000000000000000 <here>:
0: 8082 ret
# 通过 objdump -r 可以显示 reloc 信息
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D -r ./test.o
Disassembly of section .text:
0000000000000000 <_start>:
0: 00000597 auipc a1,0x0
0: R_RISCV_PCREL_HI20 here
0: R_RISCV_RELAX *ABS*
4: 00058593 mv a1,a1
4: R_RISCV_PCREL_LO12_I .L0
4: R_RISCV_RELAX *ABS*
Disassembly of section .data:
0000000000000000 <.data>:
...
Disassembly of section .sdata:
0000000000000000 <here>:
0: 8082 ret
linker relaxation 的结果:
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -nostdlib -T test.lds
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D ./a.out
Disassembly of section .text:
0000000000000000 <_start>:
0: ffe18593 addi a1,gp,-2 # 80005 <here>
Disassembly of section .data:
0000000000000004 <.data>:
...
Disassembly of section .sdata:
0000000000080005 <here>:
80005: 8082 ret
lds 中必须有定义 __global_pointer$ 才可以使用基于 gp 的 relaxation. gcc 自带的 linker script 里 __global_pointer$ 定义为:
__global_pointer$ = MIN(__SDATA_BEGIN__ + 0x800,
MAX(__DATA_BEGIN__ + 0x800, __BSS_END__ - 0x800));
其中的 0x800 使得通过 gp 可以访问 SDATA_BEGIN 开始的 13-bit 地址范围 (因为 riscv 中 imm 是有符号数)
另外, 基于 gp 的 relaxation 不适用于 call 指令.
还有一点要注意, gp 的值需要程序初始化为 `__global_pointer$`, 例如 crt0.s 的的代码:
_start: # Initialize global pointer .option push .option norelax 1:auipc gp, %pcrel_hi(__global_pointer$) addi gp, gp, %pcrel_lo(1b) .option pop
如果使用了 nostdlib 或 nostartfiles , 则需要手写这段代码
Backlinks
Linker Relocation (Linker Relocation > reloc_type): 和 gp relaxation 有关
1.1.4. align relaxation
test.S:
.global _start .section .text _start: call here .balign 16 move a0,a0 here: ret
没有经过 linker relaxation 的 obj:
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -march=rv64g -c
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D test.o -r
Disassembly of section .text:
0000000000000000 <_start>:
0: 00000097 auipc ra,0x0
0: R_RISCV_CALL here
0: R_RISCV_RELAX *ABS*
4: 000080e7 jalr ra # 0 <_start>
8: 00000013 nop
8: R_RISCV_ALIGN *ABS*+0xc
c: 00000013 nop
10: 00000013 nop
14: 00050513 mv a0,a0
0000000000000018 <here>:
18: 00008067 ret
1c: 0000 unimp
gcc 编译时针对 align 插入了 3 个 nop, 且 reloc 中有一项 `R_RISCV_ALIGN=0xc`, 表示插入了 12 个 byte. 之所以是 3 个 nop, 是因为虽然 link 时到底多少个 nop 取决于 _start 的起始地址以及前面 R_RISCV_CALL 的 relaxation 的结果, 但是由于 linker relaxation 只能删除和替换指令(而不能增加指令), 所以这里会插入理论上最多的 nop. 对于 n 个 word 的 align, 需要插入的最多的 nop 是 n-1
relaxation 的结果:
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -march=rv64g -nostdlib
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D a.out
Disassembly of section .text:
0000000000010080 <_start>:
10080: 014000ef jal ra,10094 <here>
10084: 00000013 nop
10088: 00000013 nop
1008c: 00000013 nop
# mv a0,a0 确实是 16 bytes 对齐
10090: 00050513 mv a0,a0
0000000000010094 <here>:
10094: 00008067 ret
10098: 0000 unimp
...
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -march=rv64g -nostdlib -mno-relax
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -D a.out
Disassembly of section .text:
0000000000010080 <_start>:
10080: 00000097 auipc ra,0x0
10084: 014080e7 jalr 20(ra) # 10094 <here>
10088: 00000013 nop
1008c: 00000013 nop
# -mno-relax 时针对 align 的 relaxation 还是会工作
10090: 00050513 mv a0,a0
0000000000010094 <here>:
10094: 00008067 ret
10098: 0000 unimp
...
1.1.5. tls relaxation
访问 thread local 变量时, 编译是不能确定符号对应的 tls slot (相对 tp 的偏移量) 有多大, 会需要额外的 lui 指令, 然后 tls relaxation 时有可能会把 lui 去掉
.global _start .section .text _start: lui a5,%tprel_hi(here) # R_RISCV_TPREL_HI20 (symbol) lw t0,%tprel_lo(here)(a5) # R_RISCV_TPREL_LO12_I (symbol) addi t0,t0,1 sw t0,%tprel_lo(here)(a5) .section .tdata here: .word 0
$> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -march=rv64g -c $> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -d ./test.o 0000000000000000 <_start>: 0: 000007b7 lui a5,0x0 4: 0007a283 lw t0,0(a5) # 0 <_start> 8: 00128293 addi t0,t0,1 c: 0057a023 sw t0,0(a5) $> /opt/riscv/bin/riscv64-unknown-linux-gnu-gcc test.S -O0 -march=rv64g -nostdlib $> /opt/riscv/bin/riscv64-unknown-linux-gnu-objdump -d ./a.out 0000000000010120 <_start>: 10120: 00022283 lw t0,0(tp) # 0 <here> 10124: 00128293 addi t0,t0,1 10128: 00522023 sw t0,0(tp) # 0 <here>
Backlinks
Linker Relocation (Linker Relocation > reloc_type): 和 tls relaxation 有关
1.2. impls
linker relaxation 的实现在 BFD 中, 以 riscv 为例, 在 elfnn-riscv.c 中, 具体的:
- _bfd_riscv_relax_call, 对应 `auipc+jalr -> jal`
- _bfd_riscv_relax_lui, 对应 `lui+addi… -> gp+addi…`
- _bfd_riscv_relax_pc, 对应 `auipc+addi… -> gp+addi…`
- _bfd_riscv_relax_tls_le
- _bfd_riscv_relax_align, 对应 `align`
linker relaxation 的实现非常复杂, 它会修改和删除指令, 从而导致:
- 其它指令的 branch offset 需要调整
- DWARF 的 debug 信息需要调整
- align 需要调整
1.2.1. _bfd_riscv_relax_call
1.2.2. _bfd_riscv_relax_lui
1.2.3. _bfd_riscv_relax_pc
1.2.4. _bfd_riscv_relax_align
Backlinks
AArch64 Tutorial (AArch64 Tutorial > AArch64 Assembly > instructions > Branch > BR/BLR): RISC-V 中针对函数调用会生成 auipc+jalr, 依赖 Relaxation 把它转换为 jal, AArch64 针对函数调用则会生成 bl (而不是 adrp+blr), 靠 linker 生成 veneer 来处理过远的函 数调用
Linker Relocation (Linker Relocation > riscv relocation > R_RISCV_RELAX): 这个和 Linker Relaxation 有关
Static Linker (Static Linker > Linker Relaxation): Linker Relaxation
binutils (binutils > as > macro): riscv_call 生成了 `auipc+jalr`, 因为 assembler 无法知道跳转的范围有多大, 用 auipc+jalr 是最保险的做法, 在 link 阶段通过 linker relaxation 有可能会把它换成 jal
relax branch (binutils > as > relax branch): relax branch 的作用与 Linker Relaxation 类似, 但 linker relaxation 并没有处理 relax branch. 一种可能的 linker relaxation 中实现方法是: 让 blt 默认生成 `bge+jal`, 然后由 linker_relaxation 尝试把它替换回 `blt`
