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 为单位, 导致两个问题:

  1. symbol 的地址在 object file 中无法确定, gcc 必须通过 `auipc/lui` + `add/load/store/jalr` 多条指令的方式确保 symbol 在最 `远` 的情况下也能访问.

    即使考虑可以使用 pc 相对地址的函数调用, 由于被调用函数的最终可能会因为 LinkerScript 被放在其它的地址, 所以编译 object 时是无法确定被调用函数的 pc 相对地址的.

    另外, 如果代码中有 .align, 由于 object file 各个 section 的起始地址都是 0, 且指令有可能被 linker 修改, 导致编译时并不能确定 align 应该产生多少个 nop.

  2. 跨 object file 的优化无法实现

为了解决这两个问题, 需要 Static Linker 去优化/修改, 前者是 linker relaxation, 后者是 LTO

riscv 中的 linker relaxation 主要有几种方式:

  1. lui+(addi,ld,sw…) 替换为 gp/tp+addi,ld,sw…
  2. auipc+(addi,ld,sw…) 替换为 gp/tp+addi,ld,sw…
  3. auipc+jalr 替换为 jal
  4. 根据 reloc 中的 align 信息重新调整 align
  5. 把一些指令替换成 c 指令, 例如把 jal 替换为 c.jal

另外, 是否能使用 gp/tp 也取决于符号的位置, 例如:

  1. 如果 symbol 在 .tdata 中, 使用 tp 做基址寄存器, 以支持 elf_tls
  2. 如果 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): 和 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

tls relaxation 是为了支持 elf_tls

    .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): 和 tls relaxation 有关

1.2. impls

linker relaxation 的实现在 BFD 中, 以 riscv 为例, 在 elfnn-riscv.c 中, 具体的:

  1. _bfd_riscv_relax_call, 对应 `auipc+jalr -> jal`
  2. _bfd_riscv_relax_lui, 对应 `lui+addi… -> addi…`
  3. _bfd_riscv_relax_pc, 对应 `auipc+addi… -> addi…`
  4. _bfd_riscv_relax_tls_le
  5. _bfd_riscv_relax_align, 对应 `align`

linker relaxation 的实现非常复杂, 它会修改和删除指令, 从而导致:

  1. 其它指令的 branch offset 需要调整
  2. DWARF 的 debug 信息需要调整
  3. 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`

Author: [email protected]
Date: 2022-04-06 Wed 19:22
Last updated: 2023-11-28 Tue 14:59

知识共享许可协议