Linker Script

Table of Contents

1. Linker Script

1.1. 一个简单的 linker script 的例子

$> cat test.lds
PROVIDE (xxx = 0xcafebabe);
SECTIONS
{
    . = 0x10000;
    .text : { *(.text) }
    . = 0x9000000;
    .data : { *(.data) }
    .bss : { *(.bss) }
}

$> cat test.c
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

extern int xxx;
int main(int argc, char *argv[]) {
    printf("%x\n", &xxx);
}

$> arm-linux-androideabi-gcc test.c -fPIE -pie -O0 -g3 -T test.lds

$> ./a.out
b886b012

$> arm-linux-androideabi-readelf -a ./a.out |grep '] .text'
  [ 1] .text             PROGBITS        00010000 001000 0000f8 00  AX  0   0  4
                                         ~~~~~~~~ <---

$> arm-linux-androideabi-readelf -a ./a.out |grep '] .data'
  [13] .data             PROGBITS        09000000 002000 000000 00  WA  0   0  1
                                         ~~~~~~~~

$> arm-linux-androideabi-objdum -D ./a.out

Disassembly of section .got:

09000128 <_GLOBAL_OFFSET_TABLE_-0x18>:
 9000128:    09000010      stmdbeq         r0, {r4}
 900012c:    09000008      stmdbeq         r0, {r3}
 9000130:    09000000      stmdbeq         r0, {}	; <UNPREDICTABLE>
 9000134:    09000018      stmdbeq         r0, {r3, r4}
 9000138:    000100a8      andeq           r0, r1, r8, lsr #1
 900013c:    cafebabe      bgt             8faec3c <note_end+0x8f9e950>
             ~~~~~~~~ <---

$> arm-linux-androideabi-gcc test.c -fno-pie -O0 -g3 -T test.lds
$> arm-linux-androideabi-objdum -D ./a.out
000100a8 <main>:
   100a8:       e92d4800        push    {fp, lr}
   100ac:       e28db004        add     fp, sp, #4
   100b0:       e24dd008        sub     sp, sp, #8
   100b4:       e50b0008        str     r0, [fp, #-8]
   100b8:       e50b100c        str     r1, [fp, #-12]
   100bc:       e59f0010        ldr     r0, [pc, #16]   ; 100d4 <main+0x2c>
   100c0:       e59f1010        ldr     r1, [pc, #16]   ; 100d8 <main+0x30>
   100c4:       eb00000f        bl      10108 <printf@plt>
   100c8:       e1a00003        mov     r0, r3
   100cc:       e24bd004        sub     sp, fp, #4
   100d0:       e8bd8800        pop     {fp, pc}
   100d4:       0001012c        andeq   r0, r1, ip, lsr #2
   100d8:       cafebabe        bgt     fffbebd8 <xxx+0x34fd311a>
                ~~~~~~~~ <---

1.2. SECTIONS

SECTIONS 是 linker script 最主要的部分, 通过 SECTIONS 把多个文件的 section 合并成一个 section, 并指定 section 的地址, 例如:

SECTIONS {
    /* 所有输入文件的 .text section 被合并在输出文件的 .text section 中
    * , 并且 .text section 的 vaddr 为 0x20000 */
    .text 0x20000: {
        *(.text)
    }
    /* 把 location counter 置于 0x40000 */
    . = 0x40000;
    /* 把 foo.o 中的 .data 和 bar.o 中的 .data2 合并到输出文件的 .data 中
    * 这里没有指定地址, 所以地址为 location counter 当前的值 (0x40000)*/
    .data : {
        foo.o(.data)
        bar.o(.data2)
    }
    /* 这里 location counter 的值会自动加上 sizeof(.data), 所以 .bss
    的地址会接着 .data */
    .bss : {
        *(.bss)
    }

    /* location counter 也可以这样赋值 */
    . = . + 0x10000

    /* 输入文件的 section 只能使用一次, 所以这里 .tmp 并不会有任何内容 */
    .tmp : {
        *(.bss)
    }
    // section 可以通过 `>` 使用其它的 memory region, 而非默认的 memory region,
    // 例如 .tmp2 会被放在 ram 的开头, 前面指定的 location counter 不影响 ram
    // section 还可以通过 AT(xxx) 指定 LMA
    .tmp2 : AT (0x1000) {
        /*...*/
    } > ram
    // section 通过 section 最后的 AT>xxx 指定 LMA 使用的 memory region
    .tmp3 : {
        /*...*/
    } AT> rom
    /* 所有其它未指定的 section 会自动按同名的方式合并到输出文件中 */
}

1.3. symbol

通过 linker script 可以给全部符号赋值.

1.3.1. 直接赋值

xxx = 0xabc;
extern int xxx;
int main(int argc, char *argv[]) {
    printf("%x\n", &xxx);
}
$> ./a.out
b71b3abc

输出的结果中有 b71b3 前缀是因为 PIE 的原因

1.3.2. PROVIDE

PROVIDE 与直接赋值差不多, 但有一点差别:

PROVIDE (xxx = 0xabc);
$> cat test.c
extern int xxx;
int main(int argc, char *argv[]) {
    printf("%x\n", &xxx);
}

$> arm-linux-androideabi-gcc test.c -fPIE -pie -O0 -g3 -T test.lds
$> ./a.out
af92babc

$> cat test2.c
int xxx = 0;
int main(int argc, char *argv[]) {
    printf("%x\n", &xxx);
}

$> arm-linux-androideabi-gcc test2.c -fPIE -pie -O0 -g3 -T test.lds
$> ./a.out
ab8c9004

可见, PROVIDE 只能给未定义的全局符号赋值.

1.3.3. PIE

1.4. Arithmetic Functions

给 symbol 或 location counter 赋值时可以使用如何的函数:

  1. ADDR (section)

    返回一个 section 的绝对地址

  2. DEFINED (symbol)

    是否已经定义了全局符号 symbol

  3. SIZEOF (section)

1.5. ENTRY

ENTRY (foo);

通过 entry 可以指定 entry pointer address

1.6. KEEP

1.7. AT

使用 location counter 或在 section 中指定 start, memory region 等影响的都是 VMA, 有时 VMA 与 LMA (load memory address) 并不同, 例如:

  1. 数据需要放在 rom 中, 但运行时需要加载到 ram 中
  2. 数据需要加载到较高的 ram, 但直接放在 ram 中会导致很大的 bin

1.7.1. 使用 AT 避免过大的 binary

objcopy -O binary 生成的 binary 需要对应到一个 elf 加载后的布局, 所有 LOAD segment 的上下限地址决定了 bin 的大小 (objcopy 的结果与 LOAD 的大小只是大致相同, 因为实际上 objcopy 使用的是 section flag 来决定哪些 section 被复制, 而不是用 segment flag)

PROVIDE (xxx = 0xcafebabe);
SECTIONS
{
    . = 0x10000;
    .text : { *(.text) }
    . = 0x9000000;
    .data : AT (0x60000) { *(.data) }
    .bss : { *(.bss) }
}

// 2020-11-09 11:45

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int xxx[128 * 1024] = {0xcafe, 0xcafe, 0xcafe, 0xcafe};
int main(int argc, char *argv[]) { int x = xxx[0]; }

elf:

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  ABIFLAGS       0x010240 0x00010240 0x00010240 0x00018 0x00018 R   0x8
  LOAD           0x010000 0x00010000 0x00010000 0x0025c 0x0025c R E 0x10000
  LOAD           0x020000 0x09000000 0x00060000 0x80014 0x80034 RW  0x10000

od -x a.out:

0400000 == 0x20000

*
0400000 0000 feca 0000 feca 0000 feca 0000 feca
0400020 0000 0000 0000 0000 0000 0000 0000 0000
*

所以 elf 加载时并不需要考虑 PhysAddr: 它直接从 offset 开始 load 到 VirtAddr 即可

但对于 binary 就有区别了:

od -x a.bin:

O1200000 == 0x50000

*
1200000 0000 feca 0000 feca 0000 feca 0000 feca
1200020 0000 0000 0000 0000 0000 0000 0000 0000
*

之所以是 0x50000, 是因为 bin 将来需要被 load 到 0x10000 (第一个 LOAD segment 的 VirtAddr)

使用了 AT 后, bin 大小为 833K

若不使用 AT:

od -x a.bin:

01077600000 == 0x8FF0000

*
1077600000 0000 feca 0000 feca 0000 feca 0000 feca
1077600020 0000 0000 0000 0000 0000 0000 0000 0000
*

bin 大小为 145M

最后需要注意的是, 虽然 xxx 位于 bin 的 0x50000 offset (或加载后的 0x60000 地址), 但代码中涉及到 xxx 的地方还是使用的 VirtAddr, 即 0x9000000, 所以需要程序启动时手动的把 0x60000 的数据复制到 0x9000000

1.7.2. 使用 AT 加载 rom

AT 的典型用法是 bin 需要烧到一个较小的 rom 上, 但需要在较大的内存空间里运行, 例如:

MEMORY {
  rom (rx)   : ORIGIN = 0x8000,     LENGTH = 16K
  ram (rw!x) : ORIGIN = 0x10000000, LENGTH = 256M
}

SECTION {
  .data {
    /* ... */
  } > ram AT> rom
}

.data 数据放在 rom 中, 但对应的 VMA 却在 ram 中, 需要运行时代码把 .data 从 rom搬运到 ram

1.8. misc

1.8.1. how to get the default linker script

$> gcc test.c -Wl,-verbose
$> ld --verbose

1.8.2. how to use linker script

$> gcc test.c -Wl,--script=test.lds
$> gcc test.c -T test.lds

Backlinks

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

R_RISCV_JAL (Linker Relocation > riscv relocation > R_RISCV_JAL): 之所以区分这两种情况, 是因为 section 的地址是不确定的 (Linker Script 可以配置 section 的地址). 但如果 symbol 在当前 section, 因为 jal 的操作数是 PCREL 的, 所 以操作数会是确定的值.

Static Linker (Static Linker > Linker Script): Linker Script

Author: [email protected]
Date: 2017-03-31 Fri 00:00
Last updated: 2024-02-01 Thu 14:04

知识共享许可协议