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 会包含如下信息:

  1. symbol 地址如何使用 (直接使用, 移位后使用, 或者需要减去 pc 地址)
  2. symbol 地址在指令编码中的位置 (起始位置, 长度, …)

例如, 若 reloc_type 是 R_RISCV_HI20, 则会把 symbol 地址取高 20 位后写入指令编码的高 20 位

抽象出 reloc_type 中的信息是为了提供一个实现 reloc_type 的模板, 但有的 reloc_type 例如 R_RISCV_JAL 的编码比较复杂, 无法套用模板, 所以它并不会使用 reloc_type 中的全部信息.

1.1. example

下面这个例子展示了:

  1. R_RISCV_HI20
  2. R_RISCV_PCREL_HI20
  3. 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 基本上只用到了:

  1. type
  2. pc_relative
  3. size/bitsize
  4. 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, 但没有必要;

  1. 动态链接也可以不用 GOT 而是让 rtld 直接 patch 代码本身, 参考 PIE)

编译 c 代码时使用 `-fPIC` 时会使用 GOT_HI20

1.2.5. R_RISCV_JAL

JAL 的操作数也是一个 symbol, 但根据 symbol 的位置, 分为两种情况:

  1. symbol 在当前 section
  2. 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 为例:

Author: [email protected]
Date: 2022-04-08 Fri 13:50
Last updated: 2024-09-01 Sun 14:39

知识共享许可协议