本文是《程序员的自我修养:链接、装载与库》的读书笔记 Part 1
Basics 早期计算机的抽象:
CPU 频率不高,和内存的频率一样
每个设备都会有一个相应的 IO 控制器
随着 CPU 频率提高,慢慢抽象出了一个北桥芯片,用来处理快速的 CPU
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 已经将北桥功能做进去了。这并不代表我们知识是过期的,因为这部分原理仍然类似。下图是成书年间的图,仅供参考,可图一乐。
SMP 架构和多处理器也出现在了当年的 arch 中,并越来越火,直到今天
当年的系统架构如图:
这不禁让我想到一个刚看到的问题,随着 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 的大致结构 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 ➜ llvm-objdump -h 3 -1. o 3 -1. o: file format mach-o arm64Sections: Idx Name Size VMA Type 0 __text 00000088 0000000000000000 TEXT 1 __data 00000008 0000000000000088 DATA 2 __cstring 00000004 0000000000000090 DATA 3 __bss 00000004 00000000000000 d8 BSS 4 __common 00000004 00000000000000 dc BSS 5 __compact_unwind 00000040 0000000000000098 DATA ch3-1. o: file format elf64-x86-64 Sections: Idx Name Size VMA Type 0 00000000 0000000000000000 1 .strtab 000000 d0 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 dcllvm-size ch3-1. o text data bss dec hex filename 194 8 8 210 d2 ch3-1. o
书上所示,程序中:
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 大概结构如下,
用 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 6 c612e74 65787400 2e636 f6d ..rela.text..com 0010 6 d656e74 002e6273 73002e4 c 2e737472 ment..bss..L.str 0020 00676 c6f 62616 c5f 756e696 e 69745f 76 .global_uninit_v 0030 61720067 6 c6f6261 6 c5f696e 69745f 76 ar.global_init_v 0040 6172006 d 61696e2 e 73746174 69635f 76 ar.main.static_v 0050 6172006 d 61696e00 2e6 e6f74 652e474 e ar.main..note.GN 0060 552 d7374 61636b 00 2e6 c6c76 6 d5f6164 U-stack ..llvm_ad 0070 64727369 67007072 696e7466 002e7265 drsig.printf ..re 0080 6 c612e65 685f 6672 616 d6500 6368332 d la.eh_frame.ch3- 0090 312e6300 2e737472 74616200 2e73796 d 1. c..strtab..sym 00 a0 74616200 2e646174 61006 d61 696e2 e73 tab..data.main.s 00b 0 74617469 635f 7661 72320066 756e6331 tatic_var2.func1 00 c0 002e726 f 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 25640 a00 %d.. Contents of section .bss: <skipping contents of bss section at [0000 , 0008 )> Contents of section .comment: 0000 00636 c61 6e672076 65727369 6f 6e2031 .clang version 1 0010 372e302 e 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 7 d fc movl %edi, -0x4 (%rbp) b: 8b 75 fc movl -0x4 (%rbp), %esi e: 48 8 d 3 d 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 > 1 c: 48 83 c4 10 addq $0x10 , %rsp 20 : 5 d popq %rbp 21 : c3 retq 22 : 66 66 66 66 66 2 e 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 3 d 00 00 00 00 movl (%rip), %edi # 0x4c <main+0x1c > 4 c: 03 3 d 00 00 00 00 addl (%rip), %edi # 0x52 <main+0x22 > 52 : 03 7 d f8 addl -0x8 (%rbp), %edi 55 : 03 7 d f4 addl -0xc (%rbp), %edi 58 : e8 00 00 00 00 callq 0x5d <main+0x2d > 5 d: 8b 45 f8 movl -0x8 (%rbp), %eax 60 : 48 83 c4 10 addq $0x10 , %rsp 64 : 5 d 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 arm64SYMBOL TABLE: 0000000000000000 l F __TEXT,__text ltmp00000000000000090 l O __TEXT,__cstring l_.str000000000000008 c l O __DATA,__data _main.static_var00000000000000 d8 l O __DATA,__bss _main.static_var20000000000000088 l O __DATA,__data ltmp10000000000000090 l O __TEXT,__cstring ltmp200000000000000 d8 l O __DATA,__bss ltmp300000000000000 dc l O __DATA,__common ltmp40000000000000098 l O __LD,__compact_unwind ltmp50000000000000000 g F __TEXT,__text _func10000000000000088 g O __DATA,__data _global_init_var00000000000000 dc g O __DATA,__common _global_uninit_var0000000000000038 g F __TEXT,__text _main0000000000000000 *UND* _printf
可以看到:
global_init_var
被分配到了 __data
global_uninit_var
被分配到了 __common
相对于 objdump
打印文件信息,readelf
工具主要是能更好的解析 ELF 文件的格式:
接下来,用 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 4 c 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 00030 c 0000 d0 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 0000 a8 000008 00 WA 0 0 4 [ 5 ] .rodata.str1.1 PROGBITS 0000000000000000 0000b 0 000004 01 AMS 0 0 1 [ 6 ] .bss NOBITS 0000000000000000 0000b 4 000008 00 WA 0 0 4 [ 7 ] .comment PROGBITS 0000000000000000 0000b 4 000016 01 MS 0 0 1 [ 8 ] .note.GNU-stack PROGBITS 0000000000000000 0000 ca 000000 00 0 0 1 [ 9 ] .eh_frame X86_64_UNWIND 0000000000000000 0000 d0 000058 00 A 0 0 8 [10 ] .rela.eh_frame RELA 0000000000000000 0002 d8 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 000000000000004 e 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
.
在链接的时候,这里可能有下列的符号:
本目标文件定义的全局符号
本目标文件引用的全局符号
段名
局部符号:编译单元内不可见,比如 static_var
行号等 debug 信息
这里符号还有强弱符号,有相关的定义:
不允许强符号被多次定义
如果一个符号在某个目标文件是强符号,剩下都是弱符号,链接器选择强符号
如果全部都是弱符号,选择最大的弱符号(最好不要这么做)
对于符号表( .syntab
) 来说,符号表自身也有对应的结构:
符号的类型是 「局部符号」「全局符号」还是「弱引用」
符号的类型是「段」「类型」「文件名」还是什么
定义在本目标中的文件的符号所在段
不过我这里看了一下符号表,发现 printf
在 UND
中,即 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 extern int shared;void swap (int *, int *) ;int main () { int a = 100 ; swap(&a, &shared); } int shared = 1 ;void swap (int * a, int * b) { int tmp = *a; *a = *b; *b = tmp; }
我们上一节提到过 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 0000002 e 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 0000002 c 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 0000001 c 00000000000002 a8 00000000000002 a8 DATA 2 .note.ABI-tag 00000020 00000000000002 c4 00000000000002 c4 3 .gnu.hash 0000001 c 00000000000002e8 00000000000002e8 4 .dynsym 00000090 0000000000000308 0000000000000308 5 .dynstr 0000007 d 0000000000000398 0000000000000398 6 .gnu.version 0000000 c 0000000000000416 0000000000000416 7 .gnu.version_r 00000020 0000000000000428 0000000000000428 8 .rela.dyn 000000 a8 0000000000000448 0000000000000448 9 .rela.plt 00000048 00000000000004f 0 00000000000004f 0 10 .init 0000001 a 0000000000001000 0000000000001000 TEXT 11 .plt 00000040 0000000000001020 0000000000001020 TEXT 12 .text 000001 c2 0000000000001060 0000000000001060 TEXT 13 .fini 00000009 0000000000001224 0000000000001224 TEXT 14 .rodata 00000004 0000000000002000 0000000000002000 DATA 15 .eh_frame_hdr 0000003 c 0000000000002004 0000000000002004 DATA 16 .eh_frame 00000110 0000000000002040 0000000000002040 DATA 17 .init_array 00000008 0000000000003 de8 0000000000003 de8 18 .fini_array 00000008 0000000000003 df0 0000000000003 df0 19 .data.rel.ro 00000008 0000000000003 df8 0000000000003 df8 DATA 20 .dynamic 000001e0 0000000000003e00 0000000000003e00 21 .got 00000020 0000000000003f e0 0000000000003f e0 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 000003 c0 0000000000000000 0000000000000000 27 .strtab 000001f 8 0000000000000000 0000000000000000 28 .shstrtab 000000f d 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 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 8 d 7 d f8 leaq -0x8 (%rbp), %rdi 1 a: 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 2 c: 5 d popq %rbp 2 d: c3 retq 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 8 d 7 d f8 lea rdi, [rbp - 0x8 ] 1 a: 48 8b 35 00 00 00 00 mov rsi, qword ptr [rip] # 0x21 <main+0x21 > 000000000000001 d: 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 2 c: 5 d pop rbp 2 d: c3 ret 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 8 d 7 d f8 leaq -0x8 (%rbp), %rdi 116 a: 48 8 d 35 c3 2 e 00 00 leaq 0x2ec3 (%rip), %rsi # 0x4034 <shared> 1171 : e8 0 a 00 00 00 callq 0x1180 <swap> 1176 : 31 c0 xorl %eax, %eax 1178 : 48 83 c4 10 addq $0x10 , %rsp 117 c: 5 d popq %rbp 117 d: c3 retq 117 e: 66 90 nop
从上面可以看到:
a.o
中并不知道 shared
的地址,用 movq (%rip), %rsi # 0x21
来处理
不知道 swap
的地址,用 callq 0x26
代替 swap
链接器用重定位表 把他们链接起来
这里也可以参考: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 a.o: file format elf64-x86-64 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 000000000000001 d 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 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
看真相。
Linker Script 参考之前 rCore 的部分,我就不再写了。
References
https://jonathan2251.github.io/lbd/elf.html 介绍过 LLVM 上的 ELF
程序员的自我修养—链接、装载与库
CS61C 2023 Summer
SNU Course: https://ocw.snu.ac.kr/sites/default/files/NOTE/08-GNU%20Linker.pdf
深入浅出ELF https://evilpan.com/2020/08/09/elf-inside-out/
二进制分析工具 bloaty https://github.com/google/bloaty
Elf 有关的代码: https://github.com/bminor/glibc/blob/glibc-2.27/elf/elf.h
https://coyorkdow.github.io/linking/2024/11/17/C++_linking_linux.html
https://stackoverflow.com/questions/31818870/assembly-x86-call-instruction-and-memory-address
https://maskray.me/blog/2021-08-29-all-about-global-offset-table
代码优化利器 LTO 介绍 - 左沙的文章 - 知乎 https://zhuanlan.zhihu.com/p/384160632