在上一篇,我们了解了静态链接和对应的环境 https://blog.mwish.me/2024/11/20/Compile-Loader-Libraries-Part-1-Static-linking/ 这部分我们需要介绍加载、动态链接和库
可执行文件的 Load 和进程 作者(有一些咬文嚼字的)认为:
程序是静态的概念,指编译好的指令/数据的文件(那个 ELF 可执行文件?),因为这个原因,所以有的时候可执行文件还会被称为映像文件(image)
进程是动态的概念,指被加载、运行的程序进程,有自己的虚拟地址空间
又翻到一张 rCore 的地址空间图。书上还有很多32位地址空间的讨论,但这块没看的必要了。
在书上提到了程序 Binary 的内存管理的方式,Overlay 和 Paging。Paging 是我们今天很熟悉的方式,以内存映射的方式来读取,Overlay 是以树形结构或者别的结构,自己 aware 程序的结构,并手动管理子结构的换入换出
Paging 映射的方式本质上也是现在操作系统 mmap / 内存管理的方式。在进程建立后(一些流程可能会在之后详细描述),这里执行 fork + exec
的逻辑大概是:
创建一个独立的虚拟地址空间
设置 PageTable 映射之类的
读取可执行文件头,进行映射
VMA 上构建磁盘上 section 到内存的映射
ELF 上有几种 section: 可执行的 text
,可读可写的(比如 .bss
)和只读的 (.strtab
等多种)。对于类似权限的 section,链接的时候可以某种程度上合并到一起,而 Load 的时候,也可以把它们一起 Load 。这种对象可以被称之为 segment,如图 6-7。Segment 包含一个或者多个属性类似的 Section。readelf -S
能显示对应的 Segment,readelf -l
也能看到被 load 的方式
指令寄存器设置到对应地址,启动!
这里可以尝试下面的程序,进入 llvm 例子阶段!
1 2 3 4 5 6 7 8 #include <unistd.h> int main () { while (1 ) { sleep(1000 ); } return 0 ; }
然后 readelf
查看 section 和 segments
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 There are 33 section headers, starting at offset 0xd4d60 : Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0 ] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1 ] .note.ABI-tag NOTE 0000000000400238 000238 000020 00 A 0 0 4 [ 2 ] .rela.plt RELA 0000000000400258 000258 000108 18 AI 0 24 8 [ 3 ] .init PROGBITS 0000000000401000 001000 00001 a 00 AX 0 0 4 [ 4 ] .plt PROGBITS 0000000000401020 001020 000058 00 AX 0 0 8 [ 5 ] .text PROGBITS 0000000000401080 001080 092 c26 00 AX 0 0 16 [ 6 ] __libc_thread_freeres_fn PROGBITS 0000000000493 cb0 093 cb0 0000b 2 00 AX 0 0 16 [ 7 ] __libc_freeres_fn PROGBITS 0000000000493 d70 093 d70 001 aef 00 AX 0 0 16 [ 8 ] .fini PROGBITS 0000000000495860 095860 000009 00 AX 0 0 4 [ 9 ] .rodata PROGBITS 0000000000496000 096000 01949 c 00 A 0 0 32 [10 ] .stapsdt.base PROGBITS 00000000004 af49c 0 af49c 000001 00 A 0 0 1 [11 ] __libc_thread_subfreeres PROGBITS 00000000004 af4a0 0 af4a0 000008 00 A 0 0 8 [12 ] __libc_subfreeres PROGBITS 00000000004 af4a8 0 af4a8 000050 00 A 0 0 8 [13 ] __libc_IO_vtables PROGBITS 00000000004 af500 0 af500 0006 a8 00 A 0 0 32 [14 ] __libc_atexit PROGBITS 00000000004 afba8 0 afba8 000008 00 A 0 0 8 [15 ] .eh_frame_hdr PROGBITS 00000000004 afbb0 0 afbb0 0022b 4 00 A 0 0 4 [16 ] .eh_frame PROGBITS 00000000004b 1e68 0b1 e68 00 dd58 00 A 0 0 8 [17 ] .gcc_except_table PROGBITS 00000000004b fbc0 0b fbc0 000105 00 A 0 0 1 [18 ] .tdata PROGBITS 00000000004 c0ec0 0b fec0 000020 00 WAT 0 0 16 [19 ] .tbss NOBITS 00000000004 c0ee0 0b fee0 000038 00 WAT 0 0 16 [20 ] .init_array INIT_ARRAY 00000000004 c0ee0 0b fee0 000010 08 WA 0 0 8 [21 ] .fini_array FINI_ARRAY 00000000004 c0ef0 0b fef0 000010 08 WA 0 0 8 [22 ] .data.rel.ro PROGBITS 00000000004 c0f00 0b ff00 0000e4 00 WA 0 0 32 [23 ] .got PROGBITS 00000000004 c0fe8 0b ffe8 000008 00 WA 0 0 8 [24 ] .got.plt PROGBITS 00000000004 c1000 0 c0000 000070 08 WA 0 0 8 [25 ] .data PROGBITS 00000000004 c1080 0 c0080 001690 00 WA 0 0 32 [26 ] .bss NOBITS 00000000004 c2720 0 c1710 002158 00 WA 0 0 32 [27 ] __libc_freeres_ptrs NOBITS 00000000004 c4878 0 c1710 000030 00 WA 0 0 8 [28 ] .comment PROGBITS 0000000000000000 0 c1710 000070 01 MS 0 0 1 [29 ] .note.stapsdt NOTE 0000000000000000 0 c1780 000f 40 00 0 0 4 [30 ] .symtab SYMTAB 0000000000000000 0 c26c0 00b 9e8 18 31 810 8 [31 ] .strtab STRTAB 0000000000000000 0 ce0a8 006b 47 00 0 0 1 [32 ] .shstrtab STRTAB 0000000000000000 0 d4bef 000171 00 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 file type is EXEC (Executable file) Entry point 0x401c3d There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000360 0x000360 R 0x1000 LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x094869 0x094869 R E 0x1000 LOAD 0x096000 0x0000000000496000 0x0000000000496000 0x029cc5 0x029cc5 R 0x1000 LOAD 0x0bfec0 0x00000000004c0ec0 0x00000000004c0ec0 0x001850 0x0039e8 RW 0x1000 NOTE 0x000238 0x0000000000400238 0x0000000000400238 0x000020 0x000020 R 0x4 TLS 0x0bfec0 0x00000000004c0ec0 0x00000000004c0ec0 0x000020 0x000058 R 0x10 GNU_EH_FRAME 0x0afbb0 0x00000000004afbb0 0x00000000004afbb0 0x0022b4 0x0022b4 R 0x4 GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10 GNU_RELRO 0x0bfec0 0x00000000004c0ec0 0x00000000004c0ec0 0x000140 0x000140 R 0x1 Section to Segment mapping: Segment Sections... 00 .note.ABI-tag .rela.plt 01 .init .plt .text __libc_thread_freeres_fn __libc_freeres_fn .fini 02 .rodata .stapsdt.base __libc_thread_subfreeres __libc_subfreeres __libc_IO_vtables __libc_atexit .eh_frame_hdr .eh_frame .gcc_except_table 03 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs 04 .note.ABI-tag 05 .tdata .tbss 06 .eh_frame_hdr 07 08 .tdata .init_array .fini_array .data.rel.ro .got None .comment .note.stapsdt .symtab .strtab .shstrtab
这里对应映射如图(书有点老,意思一下就行):
Type 有 LOAD 等类型的 Segments,fileSiz 是文件上的大小,MemSiz 是映射的内存上的大小。如果 MemSiz 更大,后面需要初始化成 0
BSS 段之类的内容要被 zero-initialize,初始化成 0,对于 0x001850 0x0039e8
这种,可能后面可以给 bss 开辟空间,令其被 zero-initialized
这里运行上面的程序,来查看 /proc
看虚拟空间(这里毕竟是一个 sleep 程序):
VMA 前四个是执行文件的 Segment
后面是 Anonymous Virtual Memory Area,没有影射到文件中
Vdso 是一个内核模块,它和 vsyscall 可以参考下面的博客,大概意思是 vdso / vsyscall 都是一些 syscall 绕过陷入内核的机制。
Linux vDSO概述 - 乾越的文章 -https://zhuanlan.zhihu.com/p/436454953
https://stackoverflow.com/questions/19938324/what-are-vdso-and-vsyscall
相比书中写作的时候,如果你 -fPIE,这里还有合适的 ASLR 机制,能够让访问的地址并不固定。不过现在咱们静态编译也没这个问题,这段我们后面还能遇到
借助 OS 的机制,可执行文件可能会有物理上的段合并,如上图 6-11
1 2 3 4 5 6 7 8 9 10 cat /proc/{pid}/maps 00400000 -00401000 r--p 00000000 fd:11 9043977 learn-compile/a.out00401000 -00496000 r-xp 00001000 fd:11 9043977 learn-compile/a.out00496000 -004 c0000 r--p 00096000 fd:11 9043977 learn-compile/a.out004 c0000-004 c3000 rw-p 000b f000 fd:11 9043977 learn-compile/a.out004 c3000-004 c5000 rw-p 00000000 00 :00 0 02418000 -0243b 000 rw-p 00000000 00 :00 0 [heap]7f fffe973000-7f fffe995000 rw-p 00000000 00 :00 0 [stack ]7f fffe9f8000-7f fffe9fa000 r-xp 00000000 00 :00 0 [vdso]ffffffffff600000-ffffffffff601000 r-xp 00000000 00 :00 0 [vsyscall]
我们现在知道 Huge Page 也有被用到程序中,用于优化一些程序加载,如下面的几个例子:
动态链接 静态链接的 Binary 都要链到一起,all in one binary 是个好主意(甚至现在很多地方都推荐源码整个编译)。解决方案是某种程度上分离程序,然后「动态」的
DLL/.so/.dylib 在这里也有版本问题,即 DLL Hell,早期的 so/dll 缺乏版本管理机制,导致这块很折磨人。
动态链接是把程序拆分开来,在运行的时候才链接成一个完整的程序。显然,这里执行的时候涉及多个文件,也需要操作系统的支持(如上图,你显然要合适的给进程分配出合适的位置)。ELF 动态链接文件被称为动态共享对象(DSO, Dynamic Shared Objects),一般以 .so
为扩展名,Windows 则常是 .dll
,Mac 下则是 .dylib
。Linux 下常用的 C 语言的运行库称之为 glibc,常放在 /lib
下的 libc.so
。系统的动态连接器会负责加载它,然后绑定没有决议的符号,进行 Relocate。
下面给出测试和实验的 code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> void foobar (int i) { printf ("printing from lib.so %d\n" , i); } #pragma once void foobar (int i) ;#include "lib.h" int main () { foobar (1 ); return 0 ; } #include "lib.h" int main () { foobar (2 ); return 0 ; }
这里编译一把 clang -fPIC -shared -o lib.so lib.c
后,可以再去编译:clang -o program1 program1.c lib.so
,然后就能跑了(记得 LD_LIBRARY_PATH
和 LD_PRELOAD
)
这里我们看到,链接的时候,我们还是指定了 lib.dylib
作为参数. 在这里,我们首先知道:
如果编译 program1.c
到 program1.o
,这个 ELF 文件是不知道 foobar
调用是动态还是静态的,相关信息都躺在 rela
里头
链接的时候,需要接受 lib
作为输入文件,它里面也保存了完整的符号信息,链接器知道最终是一个对动态符号的引用
在虚地址空间中,cat /proc/{pid}/maps
之后,能发现对 lib.so
和别的地方的映射 ,另外还有别的共享对象 ld-2.18.so
,就是动态链接器
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 cat /proc/26247 /maps 555f a03bc000-555f a03bd000 r--p 00000000 fd:11 9044026 ../learn-compile/program1555f a03bd000-555f a03be000 r-xp 00001000 fd:11 9044026 ../learn-compile/program1555f a03be000-555f a03bf000 r--p 00002000 fd:11 9044026 ../learn-compile/program1555f a03bf000-555f a03c0000 r--p 00002000 fd:11 9044026 ../learn-compile/program1555f a03c0000-555f a03c1000 rw-p 00003000 fd:11 9044026 ../learn-compile/program17f 3174554000-7f 31746fe000 r-xp 00000000 fd:01 675913 /usr/lib64/libc-2.18 .so.bak7f 31746fe000-7f 31748fe000 ---p 001 aa000 fd:01 675913 /usr/lib64/libc-2.18 .so.bak7f 31748fe000-7f 3174902000 r--p 001 aa000 fd:01 675913 /usr/lib64/libc-2.18 .so.bak7f 3174902000-7f 3174904000 rw-p 001 ae000 fd:01 675913 /usr/lib64/libc-2.18 .so.bak7f 3174904000-7f 3174908000 rw-p 00000000 00 :00 0 7f 3174908000-7f 3174929000 r-xp 00000000 fd:01 674975 /usr/lib64/ld-2.18 .so7f 3174b10000-7f 3174b13000 rw-p 00000000 00 :00 0 7f 3174b21000-7f 3174b22000 rw-p 00000000 00 :00 0 7f 3174b22000-7f 3174b23000 r--p 00000000 fd:11 9044017 ../learn-compile/lib.so7f 3174b23000-7f 3174b24000 r-xp 00001000 fd:11 9044017 ../learn-compile/lib.so7f 3174b24000-7f 3174b25000 r--p 00002000 fd:11 9044017 ../learn-compile/lib.so7f 3174b25000-7f 3174b26000 r--p 00002000 fd:11 9044017 ../learn-compile/lib.so7f 3174b26000-7f 3174b27000 rw-p 00003000 fd:11 9044017 ../learn-compile/lib.so7f 3174b27000-7f 3174b28000 rw-p 00000000 00 :00 0 7f 3174b28000-7f 3174b29000 r--p 00020000 fd:01 674975 /usr/lib64/ld-2.18 .so7f 3174b29000-7f 3174b2a000 rw-p 00021000 fd:01 674975 /usr/lib64/ld-2.18 .so7f 3174b2a000-7f 3174b2b000 rw-p 00000000 00 :00 0 7f ff460a6000-7f ff460c8000 rw-p 00000000 00 :00 0 [stack ]7f ff460ea000-7f ff460ec000 r-xp 00000000 00 :00 0 [vdso]ffffffffff600000-ffffffffff601000 r-xp 00000000 00 :00 0 [vsyscall]
还记得之前的装载属性吗?我们 llvm-readelf -l
看看呢:我们会发现这里的文件类型和普通类型不一样,同时,DYNAMIC
的属性出现了
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 -l program1 Elf file type is DYN (Shared object file) Entry point 0x1070 There are 11 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x000268 0x000268 R 0x8 INTERP 0x0002a8 0x00000000000002a8 0x00000000000002a8 0x00001c 0x00001c R 0x1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x0005c0 0x0005c0 R 0x1000 LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x00020d 0x00020d R E 0x1000 LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x000128 0x000128 R 0x1000 LOAD 0x002dd8 0x0000000000003dd8 0x0000000000003dd8 0x000264 0x000268 RW 0x1000 DYNAMIC 0x002df0 0x0000000000003df0 0x0000000000003df0 0x0001f0 0x0001f0 RW 0x8 NOTE 0x0002c4 0x00000000000002c4 0x00000000000002c4 0x000020 0x000020 R 0x4 GNU_EH_FRAME 0x002004 0x0000000000002004 0x0000000000002004 0x000034 0x000034 R 0x4 GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10 GNU_RELRO 0x002dd8 0x0000000000003dd8 0x0000000000003dd8 0x000228 0x000228 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 03 .init .plt .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .data .bss 06 .dynamic 07 .note.ABI-tag 08 .eh_frame_hdr 09 10 .init_array .fini_array .data.rel.ro .dynamic .got None .comment .symtab .strtab .shstrtab
地址无关代码和 GOT 最早期 PE/COFF 之类的地方引入了 “shared static library”,大概是需要一个固定分配的地址,见:https://maskray.me/blog/2023-12-03-linker-notes-on-pe-coff 。不管怎么样,感觉这种 “shared static library” 还是比较不方便的。
这里希望共享对象装载的时候能在任意地址装载,这里希望:链接的时候,绝对地址的引用不做重定位,推迟到 Load 的时候完成。之前静态链接则是「链接时重定位」,现在则变成了希望动态决定这些。
这个说法是比较简单的,但是程序编译的时候本身也有一个目标地址,如果 load 的时候再重定位不就行了吗!主意很好,但实际上这里麻烦可能在细一点的地方:之前静态链接的时候,大家都编译到一起,这没啥问题,但是反过来,动态链接那个被链接的共享对象,要在多个地方(进程)共享,可能不能比较方便的定下一个固定的地址
这里我们回到之前编译的参数:
如果只使用 -shared
,这里会在 Load 的时候重定位,直接在加载期间填写上一些对应的映射关系
如果使用 -shared
+ -fPIC
,就会生成位置无关代码(Position-independent Code),便于指令的共享。对于现代机器来说,这也不麻烦
这里可以写大概几种引用情况(希望生成书中的情况可以 -m32
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static int a;extern int b;extern void ext () ;void bar () { a = 1 ; b = 2 ; } void foo () { bar(); ext(); }
我们编译后 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 x.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <bar>: 0 : 55 pushq %rbp 1 : 48 89 e5 movq %rsp, %rbp 4 : c7 05 00 00 00 00 01 00 00 00 movl $0x1 , (%rip) # 0xe <bar+0xe > e: 48 8b 05 00 00 00 00 movq (%rip), %rax # 0x15 <bar+0x15 > 15 : c7 00 02 00 00 00 movl $0x2 , (%rax) 1b : 5 d popq %rbp 1 c: c3 retq 1 d: 0f 1f 00 nopl (%rax) 0000000000000020 <foo>: 20 : 55 pushq %rbp 21 : 48 89 e5 movq %rsp, %rbp 24 : e8 00 00 00 00 callq 0x29 <foo+0x9 > 29 : b0 00 movb $0x0 , %al 2b : e8 00 00 00 00 callq 0x30 <foo+0x10 > 30 : 5 d popq %rbp 31 : c3 retq x.o: file format elf64-x86-64 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 0000000000000006 R_X86_64_PC32 .bss-0x8 0000000000000011 R_X86_64_REX_GOTPCRELX b-0x4 0000000000000025 R_X86_64_PLT32 bar-0x4 000000000000002 c R_X86_64_PLT32 ext-0x4 RELOCATION RECORDS FOR [.eh_frame]: OFFSET TYPE VALUE 0000000000000020 R_X86_64_PC32 .text0000000000000040 R_X86_64_PC32 .text+0x20
Foo 对 bar 调用是一条相对地址调用指令,忽略共享对象的全局符号表介入(Global Symbol Interposition)问题,这里可以当成相对地址的跳转;bar 对 a
的访问也通过相对地址
模块间的数据访问 可能需要指向变量的数组,这里可以参考之前我们的 .got
, 用 llvm-objdump -h {} --show-lma
可以找到。这里 GOT 也可以保留目标函数的地址。(我个人觉得,某种意义上,这里逻辑是,代码段不可改,所以抽象出了一个可以改的数据段 .got
,同时处理这票逻辑)
我们看到上面,有 R_X86_64_PC32
( 对 a
这个模块内 static 的访问,使用 PC 相对地址 ), R_X86_64_PLT32
(对 bar 和 ext 的访问) 和 R_X86_64_REX_GOTPCRELX
( 对 b
的访问) 。这里我们有个非常好的参考对象(相对原文),maskray 的博客是这里比较好的介绍(虽然这是他在介绍 relocation overflow 的时候的博客: https://maskray.me/blog/2023-05-14-relocation-overflow-and-code-models )。这里特殊的是 R_X86_64_REX_GOTPCRELX
,逻辑类似 rdx = .got[n] = &var1
,ELF 在数据段 建立了一个 GOT 表
这里看上去很清晰了,但是特殊的情况是 ,一个模块引用了一个定义在共享对象的全局变量 的时候,比如 static in global
,然后这里可能会( 参考 https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam )
编译链接的时候,这里面创建 global
对象的一个副本
Got 如果发现这个数据,指向这个副本
这里还有个间接以来的行为,我们刚才说了 Unix 下动态链接定义导出符号的结果,那万一是下面这种有依赖的符号呢?实际上这里也需要被特别判定来处理。
1 2 static int a;static int * p = &a;
这里 maskray 的 threadlocal 文章还提到了这块和 thread local 的行为:https://maskray.me/blog/2021-02-14-all-about-thread-local-storage
MaskRay 博客上的讨论 https://maskray.me/blog/2021-08-29-all-about-global-offset-table : 见 Compiler behavior 一节,这里定义三种符号
The address may be:
a link-time constant
the load base plus a link-time constant
dependent on runtime computation by ld.so -> 这是比较常见的会存储在 GOT 的场景
这里编译出来的 binary 会带上 Global offset Table(通常由 .got
和 .got.plt
组成,.got.plt
持有 .got
需要的符号,而其余内容在 .got
中),他们持有 “link-time constant” 和动态链接产生的对象.
GOT 生成的重定位引用的第一个一个符号。当链接器第一次看到这样的引用符号时,它会在 GOT 中保留一个条目。对于引用相同符号的后续 GOT 生成重定位,链接器仅重用此条目。
这里还提到有的优化能把 .got
和 .got.plt
组合到一起。
延迟绑定和 PLT 这里比较好的内容是博客:https://maskray.me/blog/2021-09-19-all-about-procedure-linkage-table
也可以参考:https://ctf-wiki.org/executable/elf/structure/dynamic-sections/
在我之前举的例子中,有一个 memcpy@plt
1 2 3 4 call memcpy @PLT call qword ptr [rip + memcpy @GOTPCREL]
为了直观的展示这部分的印象,可以贴出对应的图,这个图源来自 https://ctf-wiki.org/executable/elf/structure/dynamic-sections/
上面的图为第一次调用的情况
下面的图为随后调用的情况
这里可以看到一个 call memcpy@PLT
的记号。plt 是 ELF 中提供间接映射的东西。我们这里还有:
1 2 3 4 5 6 7 8 9 10 11 Disassembly of section .plt: 0000000000001020 <.plt>: 1020 : ff 35 e2 2f 00 00 pushq 0x2fe2 (%rip) # 0x4008 <_GLOBAL_OFFSET_TABLE_+0x8 > 1026 : ff 25 e4 2f 00 00 jmpq *0x2fe4 (%rip) # 0x4010 <_GLOBAL_OFFSET_TABLE_+0x10 > 102 c: 0f 1f 40 00 nopl (%rax) 0000000000001030 <foobar@plt>: 1030 : ff 25 e2 2f 00 00 jmpq *0x2fe2 (%rip) # 0x4018 <_GLOBAL_OFFSET_TABLE_+0x18 > 1036 : 68 00 00 00 00 pushq $0x0 103b : e9 e0 ff ff ff jmp 0x1020 <.plt>
.plt
中 foobar@plt
是一个对应的 stub,值得一提的事这里没有实际代码。前面的 _GLOBAL_OFFSET_TABLE_
是一些预留的位置,可以看 maskray 的博客
In the assembly dump, foo@plt
is such a stub. Note that foo@plt
is not a symbol table entry. It is just that objdump displays the PLT entry as foo@plt
. We use the notation to describe a stub. The stub consists of 3 instructions. The first jmpq instruction loads an entry from .got.plt
and performs an indirect jump. The remaining two instructions will be described when introducing lazy binding.
.got.plt
holds an array of word size entries. On some architectures (x86-32, x86-64) .got.plt[0]
is the link time address of _DYNAMIC
. .got.plt[1]
and .got.plt[2]
are reserved by ld.so. .got.plt[1]
is a descriptor of the current component while .got.plt[2]
is the address of the PLT resolver.
The subsequent entries are for resolved function addresses. Each entry is relocated by an R_*_JUMP_SLOT
dynamic relocation describing the target function symbol. Resolving a function address is also called a binding. There are two binding schemes.
这里 _GLOBAL_OFFSET_TABLE_
对应 _DYNAMIC
段 ( .dynamic
),在 got 的博客也有介绍。这里 .got.plt
走 elf_machine_fixup_plt
来调用 plt resolver。
动态链接的过程 首先,在链接的输出程序中,会有一个 .interp
段。这里可以看到对应有一个 request program interpreter,这里配置链接器也可以参考:https://clang.llvm.org/docs/Toolchain.html#linker ,配置 -fuse-ld
1 2 3 4 5 llvm-readelf --all program1 | grep inter [ 1 ] .interp PROGBITS 00000000000002 a8 0002 a8 00001 c 00 A 0 0 1 [Requesting program interpreter: /lib64/ld-linux-x86-64. so.2 ] 01 .interp 02 .interp .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
ELF 中最重要的应该是 .dynamic
段,它有点像动态链接对应的 ELF Header。
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 learn-compile llvm-readelf -d lib.so Dynamic section at offset 0x2e20 contains 24 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [libc.so.6 ] 0x000000000000000c (INIT) 0x1000 0x000000000000000d (FINI) 0x115c 0x0000000000000019 (INIT_ARRAY) 0x3e08 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) 0x000000000000001a (FINI_ARRAY) 0x3e10 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x200 0x0000000000000005 (STRTAB) 0x320 0x0000000000000006 (SYMTAB) 0x230 0x000000000000000a (STRSZ) 139 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000003 (PLTGOT) 0x4000 0x0000000000000002 (PLTRELSZ) 96 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0x488 0x0000000000000007 (RELA) 0x3e0 0x0000000000000008 (RELASZ) 168 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffffe (VERNEED) 0x3c0 0x000000006fffffff (VERNEEDNUM) 1 0x000000006ffffff0 (VERSYM) 0x3ac 0x000000006ffffff9 (RELACOUNT) 3 0x0000000000000000 (NULL ) 0x0 ➜ learn-compile llvm-readelf -d program1 Dynamic section at offset 0x2df0 contains 27 entries: Tag Type Name/Value 0x0000000000000001 (NEEDED) Shared library: [lib.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so.6 ] 0x000000000000000c (INIT) 0x1000 0x000000000000000d (FINI) 0x1204 0x0000000000000019 (INIT_ARRAY) 0x3dd8 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) 0x000000000000001a (FINI_ARRAY) 0x3de0 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) 0x000000006ffffef5 (GNU_HASH) 0x2e8 0x0000000000000005 (STRTAB) 0x3e8 0x0000000000000006 (SYMTAB) 0x310 0x000000000000000a (STRSZ) 151 (bytes) 0x000000000000000b (SYMENT) 24 (bytes) 0x0000000000000015 (DEBUG) 0x0 0x0000000000000003 (PLTGOT) 0x4000 0x0000000000000002 (PLTRELSZ) 96 (bytes) 0x0000000000000014 (PLTREL) RELA 0x0000000000000017 (JMPREL) 0x560 0x0000000000000007 (RELA) 0x4b8 0x0000000000000008 (RELASZ) 168 (bytes) 0x0000000000000009 (RELAENT) 24 (bytes) 0x000000006ffffffb (FLAGS_1) PIE 0x000000006ffffffe (VERNEED) 0x498 0x000000006fffffff (VERNEEDNUM) 1 0x000000006ffffff0 (VERSYM) 0x480 0x000000006ffffff9 (RELACOUNT) 3 0x0000000000000000 (NULL ) 0x0
这里也可以看到依赖的库
1 2 3 4 5 linux-vdso.so.1 (0x00007fffb2953000 ) lib.so => {}/learn-compile/lib.so (0x00007f356767f000 ) libc.so.6 => /lib64/libc.so.6 (0x00007f35670b6000 ) /lib64/ld-linux-x86-64. so.2 (0x00007f356746a000 )
这里我们在看 lib.so
, -s
打印符号表还会看到 .dynsym
,这是动态链接用的符号表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 llvm-readelf -s lib.so Symbol table '.dynsym' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0 : 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1 : 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable 2 : 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf @GLIBC_2.2 .5 3 : 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 4 : 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable 5 : 0000000000000000 0 FUNC GLOBAL DEFAULT UND sleep@GLIBC_2.2 .5 6 : 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2 .5 7 : 0000000000001000 0 FUNC GLOBAL DEFAULT 8 _init 8 : 000000000000115 c 0 FUNC GLOBAL DEFAULT 11 _fini 9 : 0000000000001130 44 FUNC GLOBAL DEFAULT 10 foobar Symbol table '.symtab' contains 27 entries: Num: Value Size Type Bind Vis Ndx Name
我们之前也介绍过 rela.text
之类的,实际上动态链接也有 .rela.dyn
.rela.plt
, 用来做类似的事情。
显式动态链接
在Linux 中,从文件本身的格式上来看,动态库实际上跟一般的共享对象没有区别,正如我们前面讨论过的。主要的区别是共享对象是由动态链接器在程序启动之前负责装载和链接的,这一系列步骤都由动态连接器自动完成,对于程序本身是透明的;而动态库的装载则是通过一系列由动态链接器提供的 API,具体地讲共有 4 个:打开动态库 (dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库 (diclose ),程序可以通过这几个API对动态库进行操作。
TUM Codegen Paper 也提到了这个。最简单的 codegen 方式也就是 xjb 编译然后 dlopen 之类的去链接。
References