Compile/Loader/Libraries: Part 1 Static linking

本文是《程序员的自我修养:链接、装载与库》的读书笔记 Part 1

Basics

早期计算机的抽象:

  • CPU 频率不高,和内存的频率一样
  • 每个设备都会有一个相应的 IO 控制器

img

随着 CPU 频率提高,慢慢抽象出了一个北桥芯片,用来处理快速的 CPU

img

All CPUs (two in the previous example, but there can be more) are connected via a common bus (the Front Side Bus, FSB) to the Northbridge. The Northbridge contains, among other things, the memory controller, and its implementation determines the type of RAM chips used for the computer. Different types of RAM, such as DRAM, Rambus, and SDRAM, require different memory controllers.

To reach all other system devices, the Northbridge must communicate with the Southbridge. The Southbridge, often referred to as the I/O bridge, handles communication with devices through a variety of different buses. Today the PCI, PCI Express, SATA, and USB buses are of most importance, but PATA, IEEE 1394, serial, and parallel ports are also supported by the Southbridge. Older systems had AGP slots which were attached to the Northbridge. This was done for performance reasons related to insufficiently fast connections between the Northbridge and Southbridge. However, today the PCI-E slots are all connected to the Southbridge.

上面这些也一定程度上成为历史了,但是理论仍然是相似的,北桥之所以叫北桥,是因为它在主板上真的在 CPU 下面。现在 CPU 已经将北桥功能做进去了。这并不代表我们知识是过期的,因为这部分原理仍然类似。下图是成书年间的图,仅供参考,可图一乐。

img

SMP 架构和多处理器也出现在了当年的 arch 中,并越来越火,直到今天

当年的系统架构如图:

img

这不禁让我想到一个刚看到的问题,随着 os 虚拟化的加入,我们可能更需要这样调试的能力了:问题排查:C++ exception with description “getrandom“ thrown in the test body - 大家好大家吃了吗的文章 - 知乎 https://zhuanlan.zhihu.com/p/5392960438

这里我再次想到了 OSTEP 的「虚拟化」这一概念,我们对 CPU 其实很熟悉了,就不介绍进程线程这些了。设备在这里也被「虚拟化」了,比如图形被虚拟化成 DirectX 这样的 API,网络被虚拟化成网卡,盘也被虚拟化成对应的接口。这样的东西也被称为 Device Driver。

驱动程序可以看作是操作系统的一部分,它往往跟操作系统内核一起运行在特权级,但它又与操作系统内核之间有一定的独性,使得驱动程序有比较好的灵活性。因为PC 的硬件多如牛毛,操作系统开发者不可能为每个硬件开发一个驱动程序,这些驱动程序的开发工作通常由硬件生产厂商完成。操作系统开发者为硬件生产厂商提供了一系列接口和框架,凡是按照这个接口和框架开发的驱动程序都可以在该操作系统上使用。

这节介绍了很多线程和内存的部分,都被我忽略了,感兴趣可以自己读。

静态链接

我以前博客写过一些流程(时间过的真快):

上面都有图有例子,至少介绍了一些最基本的东西。我们得利用之前的东西再接着看书,以免之前白写了

书里提到「链接」的行为在打孔纸袋写代码的时候就已经存在了,最早 jmp call 这些东西都没有,地址之类的很多调整都得手算,这个计算过程叫 重定位( Relocation )。汇编语言的出现让 jmp 之类的东西能登上舞台,Symbol 这个词也随之火热了起来,它用来表示一个地址,可以是变量地址,也可以是函数的地址。而随着程序增大,模块化的需求也来了,很多地方也会需要分开来写,而拼接这些符号的过程就是 Linking

静态链接包括了:

  • 地址和空间分配(Address and Storage Allocation)
  • 符号决议(Symbol Resolution) -> 这个名字更符合静态链接,绑定之类的其实更符合动态链接
  • 重定位(Relocation)

在这里,需要被修正的函数/全局变量是重定位入口(Relocation Entry),然后重定位就是让他们指向程序正确的位置。

Elf: Executable and Linking Format

Elf 的大致结构

Unix/Linux 社区有着 Elf 格式,windows 等社区有着 PE-COFF 格式, Mach-O

https://stackoverflow.com/questions/36293052/what-is-the-difference-between-executable-formats

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int printf(const char* format, ...);

int global_init_var = 84;
int global_uninit_var;

void func1(int i) {
printf("%d\n", i);
}

int main() {
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}

在 MacOS 上 LLVM Clang 13 编译,可以看到这里是 mach-o 格式;在 Linux 下编译则是 elf 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
clang -c 3-1.c

// MacOS 下
➜ llvm-objdump -h 3-1.o

3-1.o: file format mach-o arm64

Sections:
Idx Name Size VMA Type
0 __text 00000088 0000000000000000 TEXT
1 __data 00000008 0000000000000088 DATA
2 __cstring 00000004 0000000000000090 DATA
3 __bss 00000004 00000000000000d8 BSS
4 __common 00000004 00000000000000dc BSS
5 __compact_unwind 00000040 0000000000000098 DATA


// Linux 下
ch3-1.o: file format elf64-x86-64

Sections:
Idx Name Size VMA Type
0 00000000 0000000000000000
1 .strtab 000000d0 0000000000000000
2 .text 00000066 0000000000000000 TEXT
3 .rela.text 00000078 0000000000000000
4 .data 00000008 0000000000000000 DATA
5 .rodata.str1.1 00000004 0000000000000000 DATA
6 .bss 00000008 0000000000000000 BSS
7 .comment 00000016 0000000000000000
8 .note.GNU-stack 00000000 0000000000000000
9 .eh_frame 00000058 0000000000000000
10 .rela.eh_frame 00000030 0000000000000000
11 .llvm_addrsig 00000004 0000000000000000
12 .symtab 00000138 0000000000000000

看 llvm-size 可以看到各个 “type” 的段的 size

1
2
3
4
5
6
7
llvm-size 3-1.o                          
__TEXT __DATA __OBJC others dec hex
140 16 0 64 220 dc

llvm-size ch3-1.o
text data bss dec hex filename
194 8 8 210 d2 ch3-1.o

书上所示,程序中:

img

  • ELF / Mach-O 开头是 Header,描述整个文件的 ``, 这块可以参考后文
  • 编译后的机器指令常常放在 Code Section 中,常叫 “.code” / “.text”;全局变量和静态局部变量放在数据段,一般叫 “.data”
  • Section 有一个 CONTENTS 属性(我们会在后面介绍)
  • .rodata 存放只读数据,某种平台下这部分也可以放到只读存储器中。有的时候字符串常量会放到别的地方
  • .bss section 描述未初始化的全局变量和局部静态变量。我们后文可以看到,global_uninit_var 其实没有出现在这个地方,我理解因为这个地方提供的是编译单元内的变量. .bss 端似乎能够存放一些 = 0 这样初始化为 0 的字段来减少对磁盘空间的占用。
  • 这些段通常应 . 作为前缀,表示他们的名字是系统保留的,elf 因为历史原因也留下一些保留段名,同时 elf 允许同名段和「自定义段」。比如用 __attribute__((section("段名"))) 来声明

Elf 大概结构如下,

img

img

用 LLVM 工具验证上面的内容吧

Objdump 查汇编甚至反汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
ch3-1.o:        file format elf64-x86-64
Contents of section .strtab:
0000 002e7265 6c612e74 65787400 2e636f6d ..rela.text..com
0010 6d656e74 002e6273 73002e4c 2e737472 ment..bss..L.str
0020 00676c6f 62616c5f 756e696e 69745f76 .global_uninit_v
0030 61720067 6c6f6261 6c5f696e 69745f76 ar.global_init_v
0040 6172006d 61696e2e 73746174 69635f76 ar.main.static_v
0050 6172006d 61696e00 2e6e6f74 652e474e ar.main..note.GN
0060 552d7374 61636b00 2e6c6c76 6d5f6164 U-stack..llvm_ad
0070 64727369 67007072 696e7466 002e7265 drsig.printf..re
0080 6c612e65 685f6672 616d6500 6368332d la.eh_frame.ch3-
0090 312e6300 2e737472 74616200 2e73796d 1.c..strtab..sym
00a0 74616200 2e646174 61006d61 696e2e73 tab..data.main.s
00b0 74617469 635f7661 72320066 756e6331 tatic_var2.func1
00c0 002e726f 64617461 2e737472 312e3100 ..rodata.str1.1.
Contents of section .text:
Contents of section .rela.text:
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata.str1.1:
0000 25640a00 %d..
Contents of section .bss:
<skipping contents of bss section at [0000, 0008)>
Contents of section .comment:
0000 00636c61 6e672076 65727369 6f6e2031 .clang version 1
0010 372e302e 3300 7.0.3.
Contents of section .eh_frame:
Contents of section .llvm_addrsig:
0000 08090405 ....
Contents of section .symtab:


Disassembly of section .text:

0000000000000000 <func1>:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $0x10, %rsp
8: 89 7d fc movl %edi, -0x4(%rbp)
b: 8b 75 fc movl -0x4(%rbp), %esi
e: 48 8d 3d 00 00 00 00 leaq (%rip), %rdi # 0x15 <func1+0x15>
15: b0 00 movb $0x0, %al
17: e8 00 00 00 00 callq 0x1c <func1+0x1c>
1c: 48 83 c4 10 addq $0x10, %rsp
20: 5d popq %rbp
21: c3 retq
22: 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:(%rax,%rax)

0000000000000030 <main>:
30: 55 pushq %rbp
31: 48 89 e5 movq %rsp, %rbp
34: 48 83 ec 10 subq $0x10, %rsp
38: c7 45 fc 00 00 00 00 movl $0x0, -0x4(%rbp)
3f: c7 45 f8 01 00 00 00 movl $0x1, -0x8(%rbp)
46: 8b 3d 00 00 00 00 movl (%rip), %edi # 0x4c <main+0x1c>
4c: 03 3d 00 00 00 00 addl (%rip), %edi # 0x52 <main+0x22>
52: 03 7d f8 addl -0x8(%rbp), %edi
55: 03 7d f4 addl -0xc(%rbp), %edi
58: e8 00 00 00 00 callq 0x5d <main+0x2d>
5d: 8b 45 f8 movl -0x8(%rbp), %eax
60: 48 83 c4 10 addq $0x10, %rsp
64: 5d popq %rbp
65: c3 retq

这里内容还是比较清晰的代码段展示

-t 可以打印 objdump 的符号表,可以看到变量被怎么放置了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
➜ llvm-objdump -s -d -t 3-1.o

3-1.o: file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l F __TEXT,__text ltmp0
0000000000000090 l O __TEXT,__cstring l_.str
000000000000008c l O __DATA,__data _main.static_var
00000000000000d8 l O __DATA,__bss _main.static_var2
0000000000000088 l O __DATA,__data ltmp1
0000000000000090 l O __TEXT,__cstring ltmp2
00000000000000d8 l O __DATA,__bss ltmp3
00000000000000dc l O __DATA,__common ltmp4
0000000000000098 l O __LD,__compact_unwind ltmp5
0000000000000000 g F __TEXT,__text _func1
0000000000000088 g O __DATA,__data _global_init_var
00000000000000dc g O __DATA,__common _global_uninit_var
0000000000000038 g F __TEXT,__text _main
0000000000000000 *UND* _printf

可以看到:

  • global_init_var 被分配到了 __data
  • global_uninit_var 被分配到了 __common

相对于 objdump 打印文件信息,readelf 工具主要是能更好的解析 ELF 文件的格式:

img

接下来,用 readelf 工具可以读对应的 elf 文件头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
llvm-readelf -h 3-1.o 
MachHeader {
Magic: Magic64 (0xFEEDFACF)
CpuType: Arm64 (0x100000C)
CpuSubType: CPU_SUBTYPE_ARM64_ALL (0x0)
FileType: Relocatable (0x1)
NumOfLoadCommands: 4
SizeOfLoadCommands: 680
Flags [ (0x2000)
MH_SUBSECTIONS_VIA_SYMBOLS (0x2000)
]
Reserved: 0x0
}

llvm-readelf -h ch3-1.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 992 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 1

而 ELF Header 之后,可以跳转到 Section header table ( 即前面图中,文件尾部的 Section Table),去读取对应内容,这里也能看到文件中有多少个有效的段:

这里可以看到:

  • Type 里面的段类型,段的名字只在编译-链接过程有意义,真正决定段属性的还是 Type
  • Flg 代表段的执行属性
  • .rela.text.text 段的重定位表,重定位表类型是 RELA
  • .strtab 是字符串表
  • .syntab 是符号表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
llvm-readelf -S ch3-1.o
There are 13 section headers, starting at offset 0x3e0:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .strtab STRTAB 0000000000000000 00030c 0000d0 00 0 0 1
[ 2] .text PROGBITS 0000000000000000 000040 000066 00 AX 0 0 16
[ 3] .rela.text RELA 0000000000000000 000260 000078 18 I 12 2 8
[ 4] .data PROGBITS 0000000000000000 0000a8 000008 00 WA 0 0 4
[ 5] .rodata.str1.1 PROGBITS 0000000000000000 0000b0 000004 01 AMS 0 0 1
[ 6] .bss NOBITS 0000000000000000 0000b4 000008 00 WA 0 0 4
[ 7] .comment PROGBITS 0000000000000000 0000b4 000016 01 MS 0 0 1
[ 8] .note.GNU-stack PROGBITS 0000000000000000 0000ca 000000 00 0 0 1
[ 9] .eh_frame X86_64_UNWIND 0000000000000000 0000d0 000058 00 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 0002d8 000030 18 I 12 9 8
[11] .llvm_addrsig LLVM_ADDRSIG 0000000000000000 000308 000004 00 E 12 0 1
[12] .symtab SYMTAB 0000000000000000 000128 000138 18 1 8 8
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
R (retain), l (large), p (processor specific)

在教材中,printf 之类的内容被丢到 .rel.data,准备进行重定位,这里可以打印对应的 rel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
llvm-readelf -r 3-1.o  
Relocations [
Section __text {
0x7C 1 2 1 ARM64_RELOC_BRANCH26 0 _func1
0x64 0 2 1 ARM64_RELOC_PAGEOFF12 0 _main.static_var2
0x60 1 2 1 ARM64_RELOC_PAGE21 0 _main.static_var2
0x5C 0 2 1 ARM64_RELOC_PAGEOFF12 0 _main.static_var
0x58 1 2 1 ARM64_RELOC_PAGE21 0 _main.static_var
0x30 1 2 1 ARM64_RELOC_BRANCH26 0 _printf
0x20 0 2 1 ARM64_RELOC_PAGEOFF12 0 l_.str
0x1C 1 2 1 ARM64_RELOC_PAGE21 0 l_.str
0x14 1 2 1 ARM64_RELOC_BRANCH26 0 _wrap
}
Section __compact_unwind {
0x20 0 3 0 ARM64_RELOC_UNSIGNED 0 __text
0x0 0 3 0 ARM64_RELOC_UNSIGNED 0 __text
}
]

llvm-readelf -r ch3-1.o

Relocation section '.rela.text' at offset 0x260 contains 5 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000011 0000000300000002 R_X86_64_PC32 0000000000000000 .L.str - 4
0000000000000018 0000000900000004 R_X86_64_PLT32 0000000000000000 printf - 4
0000000000000048 0000000600000002 R_X86_64_PC32 0000000000000000 .data + 0
000000000000004e 0000000700000002 R_X86_64_PC32 0000000000000000 .bss - 4
0000000000000059 0000000800000004 R_X86_64_PLT32 0000000000000000 func1 - 4

Relocation section '.rela.eh_frame' at offset 0x2d8 contains 2 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000000020 0000000200000002 R_X86_64_PC32 0000000000000000 .text + 0
0000000000000040 0000000200000002 R_X86_64_PC32 0000000000000000 .text + 30

这里面上面一个就是著名的 .rel.text.

在链接的时候,这里可能有下列的符号:

  1. 本目标文件定义的全局符号
  2. 本目标文件引用的全局符号
  3. 段名
  4. 局部符号:编译单元内不可见,比如 static_var
  5. 行号等 debug 信息

这里符号还有强弱符号,有相关的定义:

  • 不允许强符号被多次定义
  • 如果一个符号在某个目标文件是强符号,剩下都是弱符号,链接器选择强符号
  • 如果全部都是弱符号,选择最大的弱符号(最好不要这么做)

对于符号表( .syntab ) 来说,符号表自身也有对应的结构:

  • 符号的类型是 「局部符号」「全局符号」还是「弱引用」
  • 符号的类型是「段」「类型」「文件名」还是什么
  • 定义在本目标中的文件的符号所在段

不过我这里看了一下符号表,发现 printfUND 中,即 UNDEFINED

这里还涉及一下 mangle demangle 的问题,对于 C++ 的 demangle,有个工具是:https://llvm.org/docs/CommandGuide/llvm-cxxfilt.html . 这里这个可以帮助 demangle 相关的 C++ 符号。

对于 C++ 中调用 C 函数,为了保证 C Style 的符号,需要 Extern “C”, 有一个很好的例子是:https://stackoverflow.com/questions/58665349/what-are-g-begin-decls-and-g-end-decls-for

1
2
3
4
5
G_BEGIN_DECLS

...

G_END_DECLS

ld 做链接,生产可执行文件的时候,这里会定义很多 Linker 段,我翻了一下之前做 rCore 的时候的笔记:https://github.com/mapleFU/mwishCore/issues/5 ,我怎么能写的这么清晰?实际上这里也和 Linker Script 有关,系统上 ld --verbose 或者

书上列了一些:

  • __executable_start: 表示程序起始地址
  • __etext 代码段结束地址
  • _edata 数据段结束地址
  • _end 程序结束地址
  • 上面都是被 LOAD 时候的虚拟地址,可以直接 extern char 使用这些地址

这里还允许带上 debug 信息,如果带上 -g 编译,可以多出下面的 debug 段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
llvm-readelf -S ch3-1-dbg.o
There are 24 section headers, starting at offset 0xae8:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 7] .debug_abbrev PROGBITS 0000000000000000 0000b4 0000b3 00 0 0 1
[ 8] .debug_info PROGBITS 0000000000000000 000167 0000af 00 0 0 1
[ 9] .rela.debug_info RELA 0000000000000000 0006b8 000060 18 I 23 8 8
[10] .debug_str_offsets PROGBITS 0000000000000000 000216 000044 00 0 0 1
[11] .rela.debug_str_offsets RELA 0000000000000000 000718 000168 18 I 23 10 8
[12] .debug_str PROGBITS 0000000000000000 00025a 0000a6 01 MS 0 0 1
[13] .debug_addr PROGBITS 0000000000000000 000300 000040 00 0 0 1
[14] .rela.debug_addr RELA 0000000000000000 000880 0000a8 18 I 23 13 8
[21] .debug_line_str PROGBITS 0000000000000000 000434 00002a 01 MS 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
R (retain), l (large), p (processor specific)

ELF 采用 DWARF 作为默认的 debug 格式,stacktrace 之类的方式都需要拿到一些对应的信息。摘录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
llvm-dwarfdump --all ch3-1-dbg.o
ch3-1-dbg.o: file format elf64-x86-64

.debug_abbrev contents:
Abbrev table for offset: 0x00000000
[1] DW_TAG_compile_unit DW_CHILDREN_yes
DW_AT_producer DW_FORM_strx1
DW_AT_language DW_FORM_data2
DW_AT_name DW_FORM_strx1
DW_AT_str_offsets_base DW_FORM_sec_offset
DW_AT_stmt_list DW_FORM_sec_offset
DW_AT_comp_dir DW_FORM_strx1
DW_AT_low_pc DW_FORM_addrx
DW_AT_high_pc DW_FORM_data4
DW_AT_addr_base DW_FORM_sec_offset

.debug_info contents:
0x00000000: Compile Unit: length = 0x000000ab, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x000000af)

0x0000000c: DW_TAG_compile_unit
DW_AT_producer ("clang version 17.0.3")
DW_AT_language (DW_LANG_C11)
DW_AT_name ("ch3-1.c")
DW_AT_str_offsets_base (0x00000008)
DW_AT_stmt_list (0x00000000)
DW_AT_comp_dir ("{}/learn-compile")
DW_AT_low_pc (0x0000000000000000)
DW_AT_high_pc (0x0000000000000066)
DW_AT_addr_base (0x00000008)

0x0000008c: DW_TAG_variable
DW_AT_name ("global_uninit_var")
DW_AT_type (0x0000002e "int")
DW_AT_external (true)
DW_AT_decl_file ("{}/learn-compile/ch3-1.c")
DW_AT_decl_line (4)
DW_AT_location (DW_OP_addrx 0x4)

.debug_line contents:
debug_line[0x00000000]
Line table prologue:
total_length: 0x00000080
format: DWARF32
version: 5
address_size: 8
seg_select_size: 0
prologue_length: 0x00000037
min_inst_length: 1
max_ops_per_inst: 1
default_is_stmt: 1
line_base: -5
line_range: 14
opcode_base: 13
standard_opcode_lengths[DW_LNS_copy] = 0
standard_opcode_lengths[DW_LNS_advance_pc] = 1
standard_opcode_lengths[DW_LNS_advance_line] = 1
standard_opcode_lengths[DW_LNS_set_file] = 1
standard_opcode_lengths[DW_LNS_set_column] = 1
standard_opcode_lengths[DW_LNS_negate_stmt] = 0
standard_opcode_lengths[DW_LNS_set_basic_block] = 0
standard_opcode_lengths[DW_LNS_const_add_pc] = 0
standard_opcode_lengths[DW_LNS_fixed_advance_pc] = 1
standard_opcode_lengths[DW_LNS_set_prologue_end] = 0
standard_opcode_lengths[DW_LNS_set_epilogue_begin] = 0
standard_opcode_lengths[DW_LNS_set_isa] = 1
include_directories[ 0] = "{}/learn-compile"
file_names[ 0]:
name: "ch3-1.c"
dir_index: 0
md5_checksum: 4bffa4da73a8e4e5e008eff1bb4d494e

.debug_line_str contents:
0x00000000: "{}/learn-compile"
0x00000022: "ch3-1.c"

.debug_addr contents:
Address table header: length = 0x0000003c, format = DWARF32, version = 0x0005, addr_size = 0x08, seg_size = 0x00
Addrs: [
0x0000000000000000
0x0000000000000000
0x0000000000000004
0x0000000000000000
0x0000000000000004
0x0000000000000000
0x0000000000000030
]

.debug_str_offsets contents:
0x00000000: Contribution size = 64, Format = DWARF32, Version = 5
0x00000008: 00000000 "clang version 17.0.3"
0x0000000c: 00000015 "ch3-1.c"
0x00000010: 0000001d "{}/learn-compile"
0x00000014: 0000003f "global_init_var"
0x00000018: 0000004f "int"
0x0000001c: 00000053 "char"
0x00000020: 00000058 "__ARRAY_SIZE_TYPE__"
0x00000024: 0000006c "static_var"
0x00000028: 00000077 "static_var2"
0x0000002c: 00000083 "global_uninit_var"
0x00000030: 00000095 "func1"
0x00000034: 0000009b "main"
0x00000038: 000000a0 "i"
0x0000003c: 000000a2 "a"
0x00000040: 000000a4 "b"

.debug 相关的信息和 .eh_frame 是给 dwarf 和异常处理使用的,参考:

同时,可以用 strip 来去掉这些调试信息,例如可以用 llvm-strip.

静态链接

这里执行逻辑差不多是链接多个 ELF 文件成为一个可执行 ELF 文件。sample 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// a.c
extern int shared;
void swap(int*, int*);

int main() {
int a = 100;
swap(&a, &shared);
}

/// b.c
int shared = 1;

void swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}

img

我们上一节提到过 Linker Script 和 rCore 的代码,这里链接器位目标文件分配地址和空间:

  • 分配输出的可执行文件的空间
  • 也确定 Loader 加载后虚拟地址的”虚拟”空间

成书的时候,链接器采用 Two-pass Linking:

  • 收集各个段的长度、属性、位置;然后收集所有符号表,统一起来合并到全局符号表,这个地方能建立起合并的段长度等
  • 符号解析和重定位。在符号表地址确定后,可以给原来的偏移地址,再给地址一个相对的偏移量

这里查看对应文件和链接后的文件, 这里 VMA (Virtual Memory Address) 表示虚拟地址,LMA (Loaded Memory Address) 表示加载后的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
llvm-objdump -h a.o --show-lma        

a.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA Type
0 00000000 0000000000000000 0000000000000000
1 .strtab 00000067 0000000000000000 0000000000000000
2 .text 0000002e 0000000000000000 0000000000000000 TEXT
3 .rela.text 00000030 0000000000000000 0000000000000000
4 .comment 00000016 0000000000000000 0000000000000000
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000
6 .eh_frame 00000038 0000000000000000 0000000000000000
7 .rela.eh_frame 00000018 0000000000000000 0000000000000000
8 .llvm_addrsig 00000002 0000000000000000 0000000000000000
9 .symtab 00000090 0000000000000000 0000000000000000

llvm-objdump -h b.o --show-lma

b.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA Type
0 00000000 0000000000000000 0000000000000000
1 .strtab 00000063 0000000000000000 0000000000000000
2 .text 0000002c 0000000000000000 0000000000000000 TEXT
3 .data 00000004 0000000000000000 0000000000000000 DATA
4 .comment 00000016 0000000000000000 0000000000000000
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000
6 .eh_frame 00000038 0000000000000000 0000000000000000
7 .rela.eh_frame 00000018 0000000000000000 0000000000000000
8 .llvm_addrsig 00000000 0000000000000000 0000000000000000
9 .symtab 00000078 0000000000000000 0000000000000000

llvm-objdump -h a.out --show-lma

a.out: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA Type
0 00000000 0000000000000000 0000000000000000
1 .interp 0000001c 00000000000002a8 00000000000002a8 DATA
2 .note.ABI-tag 00000020 00000000000002c4 00000000000002c4
3 .gnu.hash 0000001c 00000000000002e8 00000000000002e8
4 .dynsym 00000090 0000000000000308 0000000000000308
5 .dynstr 0000007d 0000000000000398 0000000000000398
6 .gnu.version 0000000c 0000000000000416 0000000000000416
7 .gnu.version_r 00000020 0000000000000428 0000000000000428
8 .rela.dyn 000000a8 0000000000000448 0000000000000448
9 .rela.plt 00000048 00000000000004f0 00000000000004f0
10 .init 0000001a 0000000000001000 0000000000001000 TEXT
11 .plt 00000040 0000000000001020 0000000000001020 TEXT
12 .text 000001c2 0000000000001060 0000000000001060 TEXT
13 .fini 00000009 0000000000001224 0000000000001224 TEXT
14 .rodata 00000004 0000000000002000 0000000000002000 DATA
15 .eh_frame_hdr 0000003c 0000000000002004 0000000000002004 DATA
16 .eh_frame 00000110 0000000000002040 0000000000002040 DATA
17 .init_array 00000008 0000000000003de8 0000000000003de8
18 .fini_array 00000008 0000000000003df0 0000000000003df0
19 .data.rel.ro 00000008 0000000000003df8 0000000000003df8 DATA
20 .dynamic 000001e0 0000000000003e00 0000000000003e00
21 .got 00000020 0000000000003fe0 0000000000003fe0 DATA
22 .got.plt 00000030 0000000000004000 0000000000004000 DATA
23 .data 00000008 0000000000004030 0000000000004030 DATA
24 .bss 00000008 0000000000004038 0000000000004038 BSS
25 .comment 00000070 0000000000000000 0000000000000000
26 .symtab 000003c0 0000000000000000 0000000000000000
27 .strtab 000001f8 0000000000000000 0000000000000000
28 .shstrtab 000000fd 0000000000000000 0000000000000000

我们对代码 llvm-objdump -d 一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// llvm-objdump -d a.o

a.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $0x10, %rsp
8: c7 45 fc 00 00 00 00 movl $0x0, -0x4(%rbp)
f: c7 45 f8 64 00 00 00 movl $0x64, -0x8(%rbp)
16: 48 8d 7d f8 leaq -0x8(%rbp), %rdi
1a: 48 8b 35 00 00 00 00 movq (%rip), %rsi # 0x21 <main+0x21>
21: e8 00 00 00 00 callq 0x26 <main+0x26>
26: 31 c0 xorl %eax, %eax
28: 48 83 c4 10 addq $0x10, %rsp
2c: 5d popq %rbp
2d: c3 retq

// llvm-objdump -drw -Mintel a.o

a.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
0: 55 push rbp
1: 48 89 e5 mov rbp, rsp
4: 48 83 ec 10 sub rsp, 0x10
8: c7 45 fc 00 00 00 00 mov dword ptr [rbp - 0x4], 0x0
f: c7 45 f8 64 00 00 00 mov dword ptr [rbp - 0x8], 0x64
16: 48 8d 7d f8 lea rdi, [rbp - 0x8]
1a: 48 8b 35 00 00 00 00 mov rsi, qword ptr [rip] # 0x21 <main+0x21>
000000000000001d: R_X86_64_REX_GOTPCRELX shared-0x4
21: e8 00 00 00 00 call 0x26 <main+0x26>
0000000000000022: R_X86_64_PLT32 swap-0x4
26: 31 c0 xor eax, eax
28: 48 83 c4 10 add rsp, 0x10
2c: 5d pop rbp
2d: c3 ret

// llvm-objdump -d a.out

Disassembly of section .text:
0000000000001150 <main>:
1150: 55 pushq %rbp
1151: 48 89 e5 movq %rsp, %rbp
1154: 48 83 ec 10 subq $0x10, %rsp
1158: c7 45 fc 00 00 00 00 movl $0x0, -0x4(%rbp)
115f: c7 45 f8 64 00 00 00 movl $0x64, -0x8(%rbp)
1166: 48 8d 7d f8 leaq -0x8(%rbp), %rdi
116a: 48 8d 35 c3 2e 00 00 leaq 0x2ec3(%rip), %rsi # 0x4034 <shared>
1171: e8 0a 00 00 00 callq 0x1180 <swap>
1176: 31 c0 xorl %eax, %eax
1178: 48 83 c4 10 addq $0x10, %rsp
117c: 5d popq %rbp
117d: c3 retq
117e: 66 90 nop

从上面可以看到:

  1. a.o 中并不知道 shared 的地址,用 movq (%rip), %rsi # 0x21 来处理
  2. 不知道 swap 的地址,用 callq 0x26 代替 swap
  3. 链接器用重定位表把他们链接起来

这里也可以参考:https://stackoverflow.com/questions/31818870/assembly-x86-call-instruction-and-memory-address

重定位表:还记得我们之前看到的 .rela.text 吗?Here it is!这里具体也可以看 maskray 的博客 https://maskray.me/blog/2021-08-29-all-about-global-offset-table . 下面的地方是对应的 Relocation Entry,这里的 Offset 表示入口在需要被 relocate 的 section 中的位置,也是 a.o 中对应的逻辑位置。

1
2
3
4
5
6
7
8
9
10
11
12
// llvm-objdump -r a.o

a.o: file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000001d R_X86_64_REX_GOTPCRELX shared-0x4
0000000000000022 R_X86_64_PLT32 swap-0x4

RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text

下面也展示了符号表( llvm-readelf -s ),Ndx 里面有对应的 UND,即连接器应该在全局中找到这里的信息。

1
2
3
4
5
6
7
8
9
10
// llvm-readelf -s a.o

Symbol table '.symtab' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 2 .text
3: 0000000000000000 46 FUNC GLOBAL DEFAULT 2 main
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap

LTO

链接的时候本身会:

  • 没用到的函数之类的可以丢了
  • 重复的可以尝试被合并掉
  • 可以尝试内联掉函数

具体看 LTO,这也是最近越来越广泛使用的的东西:

代码优化利器 LTO 介绍 - 左沙的文章 - 知乎 https://zhuanlan.zhihu.com/p/384160632

全局资源

现在在 a.out 中还存在 .init.fini 这两个段,main 之前就会执行 .init 段,main 之后执行 .fini 段。

ABI

感觉 abi 在这书和这部分都讨论了一些,原本是很重要的部分,但是笔者比较熟悉这个概念,就不介绍了。

静态库

一图解千言。clang -static --verbose -fno-builtin 看真相。

img

Linker Script

参考之前 rCore 的部分,我就不再写了。

References

  1. https://jonathan2251.github.io/lbd/elf.html 介绍过 LLVM 上的 ELF
  2. 程序员的自我修养—链接、装载与库
  3. CS61C 2023 Summer
  4. SNU Course: https://ocw.snu.ac.kr/sites/default/files/NOTE/08-GNU%20Linker.pdf
  5. 深入浅出ELF https://evilpan.com/2020/08/09/elf-inside-out/
  6. 二进制分析工具 bloaty https://github.com/google/bloaty
  7. Elf 有关的代码: https://github.com/bminor/glibc/blob/glibc-2.27/elf/elf.h
  8. https://coyorkdow.github.io/linking/2024/11/17/C++_linking_linux.html
  9. https://stackoverflow.com/questions/31818870/assembly-x86-call-instruction-and-memory-address
  10. https://maskray.me/blog/2021-08-29-all-about-global-offset-table
  11. 代码优化利器 LTO 介绍 - 左沙的文章 - 知乎 https://zhuanlan.zhihu.com/p/384160632