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 赋值时可以使用如何的函数:
ADDR (section)
返回一个 section 的绝对地址
DEFINED (symbol)
是否已经定义了全局符号 symbol
- 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) 并不同, 例如:
- 数据需要放在 rom 中, 但运行时需要加载到 ram 中
- 数据需要加载到较高的 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