Linker Relocation
Table of Contents
1. Linker Relocation
编译器生成 object file 时, symbol 地址在 assembling 阶段无法确定, assembler 只能通过 BFD 写入重定位信息到 rel.text, 后面需要 link edtitor 进行 relocation
symbol 可以使用不同的 reloc_type, 例如:
- 使用绝对地址
PCREL
pc-relative 地址, PCREL 对 PIE 至关重要, 通过 PCREL 才能以`位置无关`的形式找到 GOT, 进而支持 PIE
TPREL
和 tls relaxation 有关
GPREL
和 gp relaxation 有关
GOT
和 PIE 有关
- …
所谓不同的 reloc_type, 是指 linker 计算出真实地址后, 根据 reloc_type 决定如何 patch 指令, reloc_type 会包含如下信息:
- symbol 地址如何使用 (直接使用, 移位后使用, 或者需要减去 pc 地址)
- symbol 地址在指令编码中的位置 (起始位置, 长度, …)
例如, 若 reloc_type 是 R_RISCV_HI20, 则会把 symbol 地址取高 20 位后写入指令编码的高 20 位
抽象出 reloc_type 中的信息是为了提供一个实现 reloc_type 的模板, 但有的 reloc_type 例如 R_RISCV_JAL 的编码比较复杂, 无法套用模板, 所以它并不会使用 reloc_type 中的全部信息.
1.1. example
下面这个例子展示了:
- R_RISCV_HI20
- R_RISCV_PCREL_HI20
- R_RISCV_GOT_HI20
## riscv64-linux-gnu-gcc test.s -static .global main .text main: addi sp, sp, -4 sw ra, 4(sp) ## 绝对地址 ## 在汇编时并不会真正计算 hello_msg 的地址的高 20 位, ## 因为汇编时无法确定 hello_msg 的地址. ## 这里只是确定一个 symbol (hello_msg) 和 reloc_type (BFD_RELOC_RISCV_HI20), ## 这两个信息包含在 obj 文件中, 最后在链接时由 link editor 处理 lui a0, %hi(hello_msg) addi a0, a0, %lo(hello_msg) call printf ## pc-relative 地址 1: auipc a0, %pcrel_hi(hello_msg) addi a0, a0, %pcrel_lo(1b) call printf ## got 地址 1: auipc a0, %got_pcrel_hi(hello_msg) addi a0, a0, %pcrel_lo(1b) ## got 需要一个额外的 ld 才能获取 hello_msg 的地址 ld a0, 0(a0) ## la 伪指令在 pic 时会生成类似的代码: ## auipc a0,0x60 ## ld a0,956(a0) # 70810 <_GLOBAL_OFFSET_TABLE_+0x8> ## la a0,hello_msg call printf lw ra, 4(sp) addi sp, sp, 4 move a0, zero ret .data hello_msg: .asciz "hello\n"
上面的 `%hi`, `%pcrel_hi`, `%got_pcrel_hi` 等是 RISC-V Assembler Modifiers, 会导致 obj 使用不同的 relocation type:
readelf -a test.o ------ Relocation section '.rela.text' at offset 0x180 contains 17 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000004 00040000001a R_RISCV_HI20 0000000000000000 hello_msg + 0 000000000004 000000000033 R_RISCV_RELAX 0 000000000008 00040000001b R_RISCV_LO12_I 0000000000000000 hello_msg + 0 000000000008 000000000033 R_RISCV_RELAX 0 00000000000c 000800000012 R_RISCV_CALL 0000000000000000 printf + 0 00000000000c 000000000033 R_RISCV_RELAX 0 000000000014 000400000017 R_RISCV_PCREL_HI2 0000000000000000 hello_msg + 0 000000000014 000000000033 R_RISCV_RELAX 0 000000000018 000500000018 R_RISCV_PCREL_LO1 0000000000000014 .1 + 0 000000000018 000000000033 R_RISCV_RELAX 0 00000000001c 000800000012 R_RISCV_CALL 0000000000000000 printf + 0 00000000001c 000000000033 R_RISCV_RELAX 0 000000000024 000400000014 R_RISCV_GOT_HI20 0000000000000000 hello_msg + 0 000000000028 000600000018 R_RISCV_PCREL_LO1 0000000000000024 .L0 + 0 000000000028 000000000033 R_RISCV_RELAX 0 00000000002c 000800000012 R_RISCV_CALL 0000000000000000 printf + 0 00000000002c 000000000033 R_RISCV_RELAX 0 objdump -d test.o ------ 0000000000000000 <main>: 0: 1171 addi sp,sp,-4 2: c206 sw ra,4(sp) 4: 00000537 lui a0,0x0 8: 00050513 mv a0,a0 c: 00000097 auipc ra,0x0 10: 000080e7 jalr ra # c <main+0xc> 0000000000000014 <.1>: 14: 00000517 auipc a0,0x0 18: 00050513 mv a0,a0 1c: 00000097 auipc ra,0x0 20: 000080e7 jalr ra # 1c <.1+0x8> 24: 00000517 auipc a0,0x0 28: 00053503 ld a0,0(a0) # 24 <.1+0x10> 2c: 00000097 auipc ra,0x0 30: 000080e7 jalr ra # 2c <.1+0x18> 34: 4092 lw ra,4(sp) 36: 0111 addi sp,sp,4 38: 00000513 li a0,0 3c: 8082 ret
1.2. riscv relocation
relocation 是 psABI 的一部分, riscv 常用的 relocation 包括:
- R_RISCV_COPY
- R_RISCV_JUMP_SLOT
- R_RISCV_JAL
- R_RISCV_CALL
- R_RISCV_GOT_HI20
- R_RISCV_PCREL_HI20
- R_RISCV_PCREL_LO12_I
- R_RISCV_PCREL_LO12_S
- R_RISCV_HI20
- R_RISCV_LO12_I
- R_RISCV_LO12_S
- R_RISCV_TPREL_HI20
- R_RISCV_TPREL_LO12_I
- R_RISCV_TPREL_LO12_S
- R_RISCV_TPREL_ADD
- R_RISCV_ALIGN
- R_RISCV_GPREL_I
- R_RISCV_GPREL_S
- R_RISCV_TPREL_I
- R_RISCV_TPREL_S
- R_RISCV_RELAX
riscv 的 relocation type 定义在 `bfd/elfxx-riscv.c::howto_table` 中, 例如:
HOWTO( R_RISCV_JAL, /* type */ 0, /* rightshift */ 4, /* size */ 32, /* bitsize */ true, /* pc_relative */ 0, /* bitpos */ complain_overflow_dont, /* complain_on_overflow */ bfd_elf_generic_reloc, /* special_function */ "R_RISCV_JAL", /* name */ false, /* partial_inplace */ 0, /* src_mask */ ENCODE_JTYPE_IMM(-1U), /* dst_mask */ true), /* pcrel_offset */
这里定义的字段是为了抽象出不同 reloc type 中相同的逻辑, 但 riscv 基本上只用到了:
- type
- pc_relative
- size/bitsize
- dst_mask
1.2.1. R_RISCV_PCREL_HI20
000000000014 000400000017 R_RISCV_PCREL_HI2 0000000000000000 hello_msg + 0 表示 14: 00000517 auipc a0,0x0 这条指令在重定位时需要用 hello_msg 的真正地址的高 20 位与 pc 的高 20 位的差值 写到原指令中 (00000517) 的 imm 部分.
编译 c 代码时使用 `-mcmodel=medany` 会使用 PCREL_HI20
1.2.2. R_RISCV_PCREL_LO12
000000000018 000500000018 R_RISCV_PCREL_LO1 0000000000000014 .1 + 0 对应 18: 00050513 mv a0,a0
R_RISCV_PCREL_LO12 比较特殊的一点是它使用的 symbol 是前面 R_RISCV_PCREL_HI2 对应的 label, 例如:
1: auipc a0, %pcrel_hi(hello_msg) addi a0, a0, %pcrel_lo(1b) # NOTE: 使用了 1b 而不是 hello_msg
处理 auipc 时会通过 `riscv_record_pcgp_hi_reloc` 记录下 `hello_msg` 的 pc-relative 地址 (1b 为 key). 后面处理 addi 时通过 `_bfd_riscv_relax_pc` 根据 `1b` 找到前面记录的 hi_reloc 及 `hello_msg` 的 pc-relative 地址并取其低12 位.
之所以这样而不直接写成 `addi xx, xx, %pcrel_lo(symbol)` 是处理 `addi` 时需要相对于前面的 `auipc` 计算 pc-relative (而不是相对于 addi 本身), 因此采取这种复杂的方式
1.2.3. R_RISCV_HI20
000000000004 00040000001a R_RISCV_HI20 0000000000000000 hello_msg + 0 表示 4: 00000537 lui a0,0x0 这条指令的 imm 部分应该用 hello_msg 的真正地址的高 20 位直接填充
编译 c 代码时使用 `-mcmodel=medlow` 时会使用 HI20
1.2.4. R_RISCV_GOT_HI20
0000000000000024 0000000500000014 R_RISCV_GOT_HI20 0000000000000000 hello_msg + 0 对应 24: 00000517 auipc a0,0x0
GOT_HI20 与 PCREL_HI20 类似, 不同的是 GOT_HI20 并不是使用的 hello_msg 的地址, 而是 hello_msg 所在的 GOT 表项的地址, 因为 GOT 主要是为了解决动态链接时 static linker 无法确定符号地址的问题 (需要注意两点: 1. 静态链接也可以用 GOT, 但没有必要;
- 动态链接也可以不用 GOT 而是让 rtld 直接 patch 代码本身, 参考 PIE)
编译 c 代码时使用 `-fPIC` 时会使用 GOT_HI20
1.2.5. R_RISCV_JAL
JAL 的操作数也是一个 symbol, 但根据 symbol 的位置, 分为两种情况:
- symbol 在当前 section
- symbol 在其它 section
之所以区分这两种情况, 是因为 section 的地址是不确定的 (Linker Script 可以配置 section 的地址). 但如果 symbol 在当前 section, 因为 jal 的操作数是 PCREL 的, 所以操作数会是确定的值.
symbol 在同一个 section:
.global _start .section .text _start: jal here here: ret
$> /opt/riscv/bin/riscv-elf-gcc test.S -O0 -c $> /opt/riscv/bin/riscv-elf-objdump -d test.o Disassembly of section .text: 0000000000000000 <_start>: 0: 004000ef jal ra,4 <here> 0000000000000004 <here>: 4: 8082 ret
symbol 不在同一个 section:
.global _start .section .text _start: jal here .section .mysection here: ret
$> riscv64-linux-gnu-objdump -d test.o Disassembly of section .text: 0000000000000000 <_start>: 0: 000000ef jal ra,0 <_start> $> readelf -a test.o Relocation section '.rela.text' at offset 0x100 contains 1 entry: Offset Info Type Sym. Value Sym. Name + Addend 000000000000 000400000011 R_RISCV_JAL 0000000000000000 here + 0
R_RISCV_JAL 的 IMM 编码参考 ENCODE_JTYPE_IMM 以及 JAL 的指令格式, 另外, IMM 编码决定了 JAL 跳转的范围在 [-1M,1M] 之间, 所以下面的代码会链接不过
.global _start .section .text _start: jal here ## 看起来像 ld 的 bug: jal 的跳转范围应该是 [-1m,1m], 但 ## 这里显示是 [-0.5m,0.5m]... ## .rept 0x7ffff-1 可以链接通过 .rept 0x7ffff .byte 0 .endr here: ret
$> riscv64-linux-gnu-gcc test.S -nostdlib -O0 /tmp/ccuebMtl.o: in function `_start': (.text+0x0): relocation truncated to fit: R_RISCV_JAL against `here' collect2: error: ld returned 1 exit status
Backlinks
RISC-V Tutorial (RISC-V Tutorial > RISC-V Assembly > RV32I > Overview): 与 B 指令类似, 没有使用 imm[0], J 跳转范围扩大为 [-1M, +1M]+pc (jal relocation)
1.2.6. R_RISCV_RELAX
这个和 Linker Relaxation 有关
1.2.7. R_RISCV_64
前面的 relocate type 都是 patch 的指令的一部分, R_RISCV_64 patch 的是整个 64 位数据, 例如下面的例子:
$> cat test.s .section .text .globl _start _start: la a0, hello ld a0, 0(a0) .section .data hello: .dword hello $> riscv-gcc test.s -c $> riscv-objdump -r ./test.o ./test.o: file format elf64-littleriscv RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000000 R_RISCV_PCREL_HI20 hello 0000000000000000 R_RISCV_RELAX *ABS* 0000000000000004 R_RISCV_PCREL_LO12_I .L0 0000000000000004 R_RISCV_RELAX *ABS* RELOCATION RECORDS FOR [.data]: OFFSET TYPE VALUE ,---- | 0000000000000000 R_RISCV_64 hello `----
另外, 与一般的 reloc type 不同, R_RISCV_64 需要 patch 的地址并非指令, 因此在 riscv 中需要考虑 endianness 的影响. 实际上, 给 riscv 添加 big-endian 支持的 patch 中, 有相当一部分代码是修改 assember/linker, 让它们能处理类似 R_RISCV_64 这样的 reloc type
R_RISCV_64 可以用来实现超长跳转:
# 假设 main 离 _start 特别远, 无论其绝对地址或者 pc 相对地址都超过了 32 位 _start: la t0, trampoline ld t0, 0(t0) jalr ra, t0 trampoline: # main 的 relocation type 为 R_RISCV_64 main
1.2.8. R_RISCV_ADD64 & R_RISCV_SUB64
通过 ADD/SUB, 编译时可以通过 linker 对 label 做有限的运算
$> cat test.s .section .text .globl _start _start: la a0, hello ld a0, 0(a0) .section .data hello: .dword hello - _start $> riscv-gcc test.s -c $> riscv-objdump -r test.o test.o: file format elf64-littleriscv RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000000 R_RISCV_PCREL_HI20 hello 0000000000000000 R_RISCV_RELAX *ABS* 0000000000000004 R_RISCV_PCREL_LO12_I .L0 0000000000000004 R_RISCV_RELAX *ABS* RELOCATION RECORDS FOR [.data]: OFFSET TYPE VALUE ,---- | 0000000000000000 R_RISCV_ADD64 hello | 0000000000000000 R_RISCV_SUB64 _start `----
1.3. perform_relocation
除了一些通用的 relocation type, riscv 特有的 relocation type 的实现都是在 perform_relocation 中实现的, 以 R_RISCV_HI20 为例:
test.S:
.global _start .section .text .rept 1929 .byte 0 .endr here: ret _start: lui a1,%hi(here) addi a1, a1, %lo(here)
重定位结果为:
Disassembly of section .text: 0000000000010078 <here-0x789>: ... 0000000000010801 <here>: 10801: 00008067 ret 0000000000010805 <_start>: 10805: 000115b7 lui a1,0x11 10809: 80158593 addi a1,a1,-2047 # 10801 <here> 1080d: 0000 unimp
elfnn-riscv.c::perform_relocation:
static bfd_reloc_status_type perform_relocation( const reloc_howto_type *howto, const Elf_Internal_Rela *rel, bfd_vma value, asection *input_section, bfd *input_bfd, bfd_byte *contents) { /* NOTE: value 是 symbol 的真正地址, pc_relative 例如 *_PCREL_* 需要先从 * value 减去 pc. * * sec_addr(input_section) + rel->r_offset 就是 pc, 其中 input_section 和 * rel->r_offset 是从 reloc_table 能查到, 表示要 patch 的地址在哪里 * * */ if (howto->pc_relative) value -= sec_addr(input_section) + rel->r_offset; value += rel->r_addend; switch (ELFNN_R_TYPE(rel->r_info)) { case R_RISCV_HI20: case R_RISCV_TPREL_HI20: case R_RISCV_PCREL_HI20: case R_RISCV_GOT_HI20: case R_RISCV_TLS_GOT_HI20: case R_RISCV_TLS_GD_HI20: /* NOTE: ENCODE_UTYPE_IMM 是按 u 指令的 imm 规则来 encode 20 bit * 的数据 因为使用 HIGH_20 的指令只有 lui/auipc, 都是 u 指令 */ value = ENCODE_UTYPE_IMM(RISCV_CONST_HIGH_PART(value)); break; case R_RISCV_LO12_I: case R_RISCV_GPREL_I: case R_RISCV_TPREL_LO12_I: case R_RISCV_TPREL_I: case R_RISCV_PCREL_LO12_I: value = ENCODE_ITYPE_IMM(value); break; /* ... */ } bfd_vma word; if (riscv_is_insn_reloc(howto)) word = riscv_get_insn(howto->bitsize, contents + rel->r_offset); else word = bfd_get(howto->bitsize, input_bfd, contents + rel->r_offset); word = (word & ~howto->dst_mask) | (value & howto->dst_mask); if (riscv_is_insn_reloc(howto)) riscv_put_insn(howto->bitsize, word, contents + rel->r_offset); else bfd_put(howto->bitsize, input_bfd, word, contents + rel->r_offset); return bfd_reloc_ok; }
1.3.1. RISCV_CONST_HIGH_PART
上面的例子中 value 为 0x10801, 但 RISCV_CONST_HIGH_PART 返回的值为 0x11, 而不是 0x10, 因为低 12 位 0x801 无法用 12 位 sign extened 表示.
正常情况下 RISCV_CONST_HIGH_PART 的逻辑应该类似于这样:
if msb(low(value)) == 0: high_part = hi(value) else: high_part = hi(value) + 1
之所以加 1, 是因为后面的 ENCODE_ITYPE_IMM 会直接把 uint12_t 0x801 转换为 int12_t, 但是把 unsigned A 转换为 signed 相当于 A-(1<<12) (例如时钟的 8 点做为负数解释是 -4 点, 即 8-12点), 所以 high_part 需要加上 1.
所以 0x10801 的 high_part 为 0x11
具体实现上 `msb 为 1 则 high_part 加 1` 可以通过 `msb 加 1 后进位` 实现.
#define RISCV_CONST_HIGH_PART(VALUE) (((VALUE) + (1 << 11)) & 0xfffff)
1.3.2. ENCODE_ITYPE_IMM
如前面所述, ENCODE_ITYPE_IMM 直接取低 12 bit, 即使它被 `错误的` extened 成负数
/* 即低 12 bit, 然后右移 20 bit, 因为 I 指令的 imm 在指令的高 20 bit */ #define ENCODE_ITYPE_IMM(x) (RV_X(x, 0, 12) << 20)
1.3.3. riscv_put_insn
获得 value 后需要把 value 修改到原指令 word 中
word = (word & ~howto->dst_mask) | (value & howto->dst_mask);
其中 dst_mask 是 value 在 word 中的 mask, 例如 R_RISCV_HI20 的 HOWTO 定义为:
HOWTO( R_RISCV_HI20, /* type */ 32, /* bitsize */ false, /* pc_relative */ 0, /* bitpos */ "R_RISCV_HI20", /* name */ 0, /* src_mask */ ENCODE_UTYPE_IMM(-1U), /* dst_mask */ false),
其中 dst_mask 为 ENCODE_UTYPE_IMM(-1U), 即 0xfffff000, 通过这个 mask 就可以把 word 中的 value 部分修改成 relocation 的结果了.
Backlinks
LLVM Toy RISC-V Backend (LLVM Toy RISC-V Backend > toy-46: write object file Pt. 3): CALL 转换 jalr 时由于涉及到 symbol 的问题, 需要转换为 `lui t0, %hi(addr); addi t0, %lo(addr); jarl ra, 0(t0)`, 其中 hi/lo 需要转换为 fixup 信息 (类型, 原指令需 要被 patch 的 offset 和长度) 保存在 object 中 Linker Relocation
Static Linker (Static Linker > Linker Relocation): Linker Relocation
risc-v modifiers (RISC-V Tutorial > GNU Assembler > risc-v modifiers): 需要注意的是 linker relocation 处理 lo/pcrel_lo, hi/pcrel_hi 时并不是简单的取高 20/低 12 位, 以 lui/addi 为例: