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 赋值时可以使用如何的函数:
ADDR (section)
返回 section 的 VMA
LOADADDR (section)
返回 section 的 LMA
DEFINED (symbol)
是否已经定义了全局符号 symbol
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