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>
             ~~~~~~~~ <---

1.2. SECTIONS

1.2.1. overview

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.2.2. memory

MEMORY {
    ram (rwx) : ORIGIN = 0x1000, LENGTH = 10M
    ram2 (rwx) : ORIGIN = 0x1000000, LENGTH = 10M
}

.sec1 : {

} >ram

.sec1 : {

} >ram2

通过 MEMORY 定义了 ram, ram2, 通过 `>ram` 可以方便的指定 section 放在哪个地址,并用能跟踪内存使用是否超出范围. 如果不使用 memroy, 则需要:

.sec1 0x1000 : {}
.sec2 0x1000000 : {}

或者使用 location counter:

. = 0x1000;
.sec1 : {}
. = 0x1000000;
.sec2 : {}

1.2.3. AT

稀疏的内存布局会导致 binary 很大, 无法打包在 rom 中. 通过 AT 或 section 中直接直接直接 LMA, 可以避免过大的 binary, 但需要运行时手动把数据从 LMA 复制到 VMA

1.2.3.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

1.2.3.2. 手动加载 binary

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

例如:

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

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

  _lma_data=LOADADDR(.data);
  _vma_data=ADDR(.data);
  _size_data=SIZEOF(.data);
}

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

memmove(_vma_data, _lma_data, _size_data);

1.3. location counter

location counter 用来读取和设置当前操作的 vma 地址

1.3.1. 读取 location counter

SECTIONS
{
    hello_1 = .;
    .cpio : {
        hello_2 = .;
        *(._archive_cpio)
        hello_3 = .;
    } >ram AT> rom
    hello_4 = .;
    hello_5 = ADDR(._archive_cpio);
}
  • hello2,5 一定对应 section 的 vma 的起始地址
  • hello_1 并不一定是 section 的 vma 起始地址, 因为 section 可能有对齐, 或者 section 通过 `<section> <addr> : {}` 直接指定了另一个 vma 地址
  • hello3,4 一定对应 vma 的结束地址
  • 并不存在 ram1 或 ram2 的 location counter, 只有一个全局 location counter

1.3.2. 设置 location counter

SECTIONS
{
    . = 0x1000;
    ._cpio : {
        . = 0x10;
        *(._archive_cpio)
    } >ram AT> rom
}
  • 在 section 内部对 location counter 赋值时指定的是相对于 section vma 起始地址的偏移量, `. = 0x10` 等价于 `. = . + 0x10`…
  • section 内部的赋值会在 section 中产生 padding
  • 在 section 外部对 location counter 赋值时使用的是绝对地址
  • section 外部的赋值会在 section 之间产生 margin
  • section 外部的赋值对使用了 `>ram` 形式的 section 没有作用…
  • 为弥补前面的问题, 可以通过 `<section> <addr> BLOCK(align) : {} >ram` 设置 section 的起始地址和 align

1.4. symbol

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

1.4.1. 直接赋值

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

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

1.4.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.4.3. PIE

1.5. arithmetic functions

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

  1. ADDR (section)

    返回 section 的 VMA

  2. LOADADDR (section)

    返回 section 的 LMA

  3. DEFINED (symbol)

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

  4. SIZEOF (section)

    返回 section 的大小

1.6. ENTRY

ENTRY (foo);

通过 entry 可以指定 entry pointer address

1.7. KEEP

阻止 section 被 gc

1.8. overwrite sections

当 lds 中使用了 `insert before/after <section>` 时, 当前的 lds 会与默认的 lds 合并, 而不是代替默认的 lds

当两个 lds 合并时, 所有 input section 会根据 lds 中 output section 声明的顺序写入包含这个 input section 的第一个 output section, 例如:

.text : {
    *(.text.a .text)
}

.text : {
    *(.text.a .text.b)
}

则最终生成的 elf 会包含两个 .text: 其中一个包含所有的 `.text.a` 和 `.text`, 另一个只包含 `.text.b`

1.9. overlay

让 LMA 不同的 section 使用相当的 VMA: 例如只执行一次的初始化代码可以和后面的代码使用相同的 VMA, 当初始化结束后手动把后面的代码加载到 VMA 以节省内存, 例如:

OVERLAY 0x1000 : AT (0x4000)
   {
     .text0 { o1/*.o(.text) }
     .text1 { o2/*.o(.text) }
   }

.text0 和 .text1 的 VMA 都是 0x1000, 但 .text0 的 LMA 是 0x4000, .text1 的 LMA 是 0x4000+sizeof(.text0)

用户需要手动利用 linker 定义了几个 symbol 来加载 text1:

memcpy ((char *) 0x1000, &__load_start_text1,
          &__load_stop_text1 - &__load_start_text1);

上面的 overlay 实际上相当于:

.text0 0x1000 : AT (0x4000) { o1/*.o(.text) }
__load_start_text0 = LOADADDR (.text0);
__load_stop_text0 = LOADADDR (.text0) + SIZEOF (.text0);
.text1 0x1000 : AT (0x4000 + SIZEOF (.text0)) { o2/*.o(.text) }
__load_start_text1 = LOADADDR (.text1);
__load_stop_text1 = LOADADDR (.text1) + SIZEOF (.text1);
. = 0x1000 + MAX (SIZEOF (.text0), SIZEOF (.text1));

1.10. misc

1.10.1. how to get the default linker script

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

1.10.2. how to use linker script

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

Backlinks

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-10-30 Wed 20:29

知识共享许可协议