静态链接
Time: 2020-01-20 Tags: binaryLink: 静态链接
链接器是个比编译器还古老的前辈。
我们准备两个小程序
第一个:
/* a.c */
extern int shared;
int main()
{
int a=100;
swap( &a , shared );
}
第二个:
/* b.c */
int shared = 1;
void swap(int* a, int* b )
{
*a ^= *b ^= *a ^= *b;
}
然后编译成目标文件:
gcc -c a.c b.c
这样我们就得到了两个文件a.o和b.o。模块a.c里面引用到了模块b.c里面的变量shared和函数swap。
相似段合并
早期的链接器会直接把目标文件的各个段按照次序叠加起来,这样就造成了空间浪费的问题。
于是一个更实际的方法——相似段合并,诞生了。它的思路是将所有输入文件的”.text”段合并到输出文件的”.text”段中,接着是”.data”段、”.bss”段等。
实际上我们在谈到空间分配时只关注于虚拟空间的分配。现在的链接器基本上采用相似段合并的策略。使用这种策略一般采用一种叫做两步链接(Two-pass Linking)的方法。
第一步是空间与地址分配。收集所有段的长度、属性和位置。收集符号定义和符号引用,统一放到一个全局变量表。
第二步是符号解析和重定位。链接的核心,尤其是重定位过程。
接下来我们把a.o和b.o连接起来。
ld a.o b.o -e main -o ab
-e main : 将main作为程序的入口,ld链接器默认程序入口为_start
-o ab : 链接的输出文件名为ab,默认为a.out
之后用objdump查看各个段的属性。
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h a.o
a.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .group 00000008 00000000 00000000 00000034 2**2
CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
1 .text 0000004a 00000000 00000000 0000003c 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data 00000000 00000000 00000000 00000086 2**0
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000000 00000000 00000000 00000086 2**0
ALLOC
. . .
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h b.o
b.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .group 00000008 00000000 00000000 00000034 2**2
CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
1 .text 00000043 00000000 00000000 0000003c 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data 00000004 00000000 00000000 00000080 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000000 00000000 00000000 00000084 2**0
ALLOC
. . .
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h ab
ab: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000091 08049000 08049000 00001000 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
. . .
3 .data 00000004 0804c00c 0804c00c 0000300c 2**2
CONTENTS, ALLOC, LOAD, DATA
. . .
我们关心以上结果中的VMA(Virtual Memory Address,即虚拟地址)和Size。
在链接之前,目标文件所有段中的VMA都是0,因为虚拟地址没有分配,他们默认都是0。
(图片与内容对应有误)
当第一步完成之后,各个段在链接之后的虚拟地址就确定了。比如”.text”段起始地址为0x08049000,”.data”段起始地址为0x0804c00c。之后链接器开始计算各个符号的虚拟地址。即链接器要给每个符号加上一个偏移量,使他们能够调整到正确的虚拟地址。
符号解析与重定位
重定位
接下来来到了链接的核心部分。
我们先用objdump来查看编译器在编译a.c成指令时如何访问外部变量的。
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -d a.o
a.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl -0x4(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 53 push %ebx
e: 51 push %ecx
f: 83 ec 10 sub $0x10,%esp
12: e8 fc ff ff ff call 13 <main+0x13>
17: 05 01 00 00 00 add $0x1,%eax
1c: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%ebp)
23: 83 ec 08 sub $0x8,%esp
26: 8b 90 00 00 00 00 mov 0x0(%eax),%edx ;8b 90 00 00 00 00 四个字节为0
2c: 52 push %edx
2d: 8d 55 f4 lea -0xc(%ebp),%edx
30: 52 push %edx
31: 89 c3 mov %eax,%ebx
33: e8 fc ff ff ff call 34 <main+0x34> ;四个字节为-4
38: 83 c4 10 add $0x10,%esp
3b: b8 00 00 00 00 mov $0x0,%eax
40: 8d 65 f8 lea -0x8(%ebp),%esp
43: 59 pop %ecx
44: 5b pop %ebx
45: 5d pop %ebp
46: 8d 61 fc lea -0x4(%ecx),%esp
49: c3 ret
. . .
对shared的引用用到了一个”mov”指令,这个指令有6个字节,后面的 00 00 00 00 是shared的地址。因为编译器编译的时候shared定义在其他目标文件中,所以编译器暂时把地址0看成shared的地址。
另一个偏移为0x33的指令其实是对swap函数的调用。add指令的的地址为0x38,-4即 0x38 - 4 = 0x34 。
编译器暂时把地址部分用0x00000000和0xFFFFFFFC替代,剩下的工作留给了链接器。链接器已经收集了所有符号的信息了,所以可以根据符号对地址进行修正。
我们用objdump来查看修正之后的结果。
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -d ab
ab: file format elf32-i386
Disassembly of section .text:
08049000 <main>:
8049000: 8d 4c 24 04 lea 0x4(%esp),%ecx
8049004: 83 e4 f0 and $0xfffffff0,%esp
8049007: ff 71 fc pushl -0x4(%ecx)
804900a: 55 push %ebp
804900b: 89 e5 mov %esp,%ebp
804900d: 53 push %ebx
804900e: 51 push %ecx
804900f: 83 ec 10 sub $0x10,%esp
8049012: e8 33 00 00 00 call 804904a <__x86.get_pc_thunk.ax>
8049017: 05 e9 2f 00 00 add $0x2fe9,%eax
804901c: c7 45 f4 64 00 00 00 movl $0x64,-0xc(%ebp)
8049023: 83 ec 08 sub $0x8,%esp
8049026: c7 c2 0c c0 04 08 mov $0x804c00c,%edx
804902c: 52 push %edx
804902d: 8d 55 f4 lea -0xc(%ebp),%edx
8049030: 52 push %edx
8049031: 89 c3 mov %eax,%ebx
8049033: e8 16 00 00 00 call 804904e <swap>
8049038: 83 c4 10 add $0x10,%esp
804903b: b8 00 00 00 00 mov $0x0,%eax
8049040: 8d 65 f8 lea -0x8(%ebp),%esp
8049043: 59 pop %ecx
8049044: 5b pop %ebx
8049045: 5d pop %ebp
8049046: 8d 61 fc lea -0x4(%ecx),%esp
8049049: c3 ret
0804904e <swap>:
804904e: 55 push %ebp
相对于add指令来说0x8049038 + 0x16 = 0x804904e,所以swap指令填充地址为 16 00 00 00 。
重定位表
ld链接器通过重定位表保存与重定位相关的信息。重定位表往往是ELF中的一个段,所以重定位表也可以叫做重定位段。比如有”.text”有要被重定位的地方,那么必定有一个相对应的叫做”.rel.text”的段保存代码段的重定位信息。我们用objdump来查看目标文件的重定位表。
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -r a.o
a.o: file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000013 R_386_PC32 __x86.get_pc_thunk.ax
00000018 R_386_GOTPC _GLOBAL_OFFSET_TABLE_
00000028 R_386_GOT32X shared
00000034 R_386_PLT32 swap
这个0x28和0x34就是”mov”指令和”call”指令的地址部分。
符号解析
我们用readelf查看a.o的符号表。
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ readelf -s a.o
Symbol table '.symtab' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
2: 00000000 0 SECTION LOCAL DEFAULT 2
3: 00000000 0 SECTION LOCAL DEFAULT 4
4: 00000000 0 SECTION LOCAL DEFAULT 5
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 8
7: 00000000 0 SECTION LOCAL DEFAULT 9
8: 00000000 0 SECTION LOCAL DEFAULT 7
9: 00000000 0 SECTION LOCAL DEFAULT 1
10: 00000000 74 FUNC GLOBAL DEFAULT 2 main
11: 00000000 0 FUNC GLOBAL HIDDEN 6 __x86.get_pc_thunk.ax
12: 00000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
13: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
14: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
UND即”undefined”未定义类型。未定义的原因即该目标文件中有关于他们的重定位项。所以链接器会屁颠屁颠来找这些UND。
指令修正方式
原因:寻址模式太™多了。
x86规定了两种基本的修正方式
对照前面的重定位表,我们的系统是x64,emmmm。
那翻译一下其实就解决了吧。shared的修正类型是R_386_32,swap修正类型是R_386_PC32。
我们可以假设一下。
假设ab生成之后,main的虚拟地址为0x1000,swap的虚拟地址为0x2000,shared的虚拟地址为0x3000。
绝对地址修正
S+A: S : shared的实际地址0x3000 A : 被修正位置的值,即0x00000000
所以修正以后指令变成:
1018:c7 c2 00 30 00 00 mov $0x3000,%edx
相对地址修正
S+A+-P: S:swap的实际地址0x2000 A:被修正位置的值0xFFFFFFFC P:被修正的位置即0x1000+0x34
所以修正之后的地址应该是0x2000 + (-4) - (0x1000 + 0x27) = 0xFD5
所以修正后指令变成:
1026:e8 d5 0f 00 00 call 0xfd5 <swap>
可以看出,绝对地址修正之后的地址是该符号的实际地址,而相对地址修正之后的地址为地址差。
COMMON块
由于弱符号和强符号的机制,编译器和链接器都支持COMMON块的机制来处理空间问题。
静态库链接
一种语言的开发环境往往会附带有语言库,这些库就是对操作系统的API的包装。比如在Linux下,printf是一个”write”的系统调用,在windows下,它是”WriteConsole”系统API。
由于目标文件的零散程度,人们使用”ar”压缩程序将目标文件压缩到了一起,使用”ar”工具可以查看文件中包含了哪些目标文件。
ar -t libc.a
下面假设我们用如下方法编译hello.c
gcc -c -fno-builtin hello.c
-fno-builtin:禁止函数优化选项,因为gcc会把仅有字符串输出的printf替换成puts。
-c:生成.o文件
然后用”ar”解压出printf:
ar -x libc.a
接着用ld将hello.o和printf.o链接再一起,结果是失败了。
printf.o:In function '_IO_printf':undefined reference to 'stdout'
printf.o:In function '_IO_printf':undefined reference to 'vfprintf'
很显然,printf依赖于其他文件。
实际上,人工操作一次hello.o的链接过程非常麻烦,我们用-verbose命令将整个编译链接过程打印出来:
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ gcc -static --verbose -fno-builtin hello.c
Using built-in specs.
COLLECT_GCC=/usr/bin/gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/i686-linux-gnu/9/lto-wrapper
Target: i686-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 9.2.1-21' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=i686-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-targets=all --enable-multiarch --disable-werror --with-arch-32=i686 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=i686-linux-gnu --host=i686-linux-gnu --target=i686-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-mutex
Thread model: posix
gcc version 9.2.1 20191130 (Debian 9.2.1-21)
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'
-----------------------------------------------
/usr/lib/gcc/i686-linux-gnu/9/cc1 -quiet -v -imultiarch i386-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=i686 -auxbase hello -version -fno-builtin -fasynchronous-unwind-tables -o /tmp/ccWvkCsF.s
-----------------------------------------------
GNU C17 (Debian 9.2.1-21) version 9.2.1 20191130 (i686-linux-gnu)
compiled by GNU C version 9.2.1 20191130, GMP version 6.1.2, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22-GMP
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/i386-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/i686-linux-gnu/9/../../../../i686-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/i686-linux-gnu/9/include
/usr/local/include
/usr/lib/gcc/i686-linux-gnu/9/include-fixed
/usr/include/i386-linux-gnu
/usr/include
End of search list.
GNU C17 (Debian 9.2.1-21) version 9.2.1 20191130 (i686-linux-gnu)
compiled by GNU C version 9.2.1 20191130, GMP version 6.1.2, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22-GMP
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 65914783ff6ceeacd00570e1b7ef81be
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'
----------------------------------------------
as -v --32 -o /tmp/cc7lBA5Q.o /tmp/ccWvkCsF.s
----------------------------------------------
GNU assembler version 2.33.1 (i686-linux-gnu) using BFD version (GNU Binutils for Debian) 2.33.1
COMPILER_PATH=/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/
LIBRARY_PATH=/usr/lib/gcc/i686-linux-gnu/9/:/usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/:/usr/lib/gcc/i686-linux-gnu/9/../../../../lib/:/lib/i386-linux-gnu/:/lib/../lib/:/usr/lib/i386-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/i686-linux-gnu/9/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'
----------------------------------------
/usr/lib/gcc/i686-linux-gnu/9/collect2 -plugin /usr/lib/gcc/i686-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/i686-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/cc4jYbv3.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id -m elf_i386 --hash-style=gnu --as-needed -static /usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/crt1.o /usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/crti.o /usr/lib/gcc/i686-linux-gnu/9/crtbeginT.o -L/usr/lib/gcc/i686-linux-gnu/9 -L/usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu -L/usr/lib/gcc/i686-linux-gnu/9/../../../../lib -L/lib/i386-linux-gnu -L/lib/../lib -L/usr/lib/i386-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/i686-linux-gnu/9/../../.. /tmp/cc7lBA5Q.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/i686-linux-gnu/9/crtend.o /usr/lib/gcc/i686-linux-gnu/9/../../../i386-linux-gnu/crtn.o
COLLECT_GCC_OPTIONS='-static' '-v' '-fno-builtin' '-mtune=generic' '-march=i686'
----------------------------------------
第一步:调用cc1程序,把hello.c编译成/tmp/ccWvkCsF.s
第二步:调用as程序,把ccWvkCsF.s汇编成cc7lBA5Q.o,这个cc7lBA5Q.o实际上就是前面的hello.o
第三步:调用collect2程序完成链接。
collect2可以看成是ld的一个包装,在这里可以把他简单地看成ld链接器。最后一步中至少有这些目标文件和库被链接入最终的可执行文件:
crt1.o
crti.o
crtbeginT.o
crtend.o
crtn.o
libgcc.a
libgcc_eh.a
libc.a
链接控制过程
由于整个链接过程有很多内容需要确定,链接器提供了三种控制链接过程的方法。
- 使用命令行指定参数。-o、-e就属于这类。
- 存放在目标文件中。
- 使用链接控制脚本。使用参数-T可以指定使用脚本。
最”小”的程序
hello.c盲打(蒙上眼睛)也能正常运行(指鼠标精确地定位到运行的位置
但是经典的helloworld
- 用到了printf,c语言库与hello.o链接才能生成可执行文件。
- helloworld有main函数,程序的入口在_start处。
- main的指令会生成.text段,字符串”Hello world!\n”会被存放在.data段或者.rodata段,还有各种c语言库生成的段,为了方便,我们把很多段放在同一个段中,即tinytext段。
TinyHelloWorld.c的源代码如下:
char* str = "Hello world!\n";
void print()
{
asm("movl $13,%%edx \n\t"
"movl %0,%%ecx \n\t"
"movl $0,%%ebx \n\t"
"movl $4,%%eax \n\t"
"int $0x80 \n\t"
::"r"(str):"edx","ecx","ebx");
}
void exit()
{
asm("movl $42,%ebx \n\t"
"movl $1,%eax \n\t"
"int $0x80 \n\t");
}
void nomain()
{
print();
exit();
}
此程序使用了gcc内嵌汇编,系统调用通过0x80中断实现,其中eax为调用号,ebx、ecx、edx等通用寄存器用来传递参数。例如WRITE调用的原型是
int write(int filedesc, char* buffer, int size)
- WRITE调用号为4,则执行movl $4,%%eax
- filedesc使用ebx传参,在默认终端(stdout)输出,所以movl $0,%%ebx
- buffer表示写入的缓冲区地址,
- size是字节数,”Hello world!\n”有13字节。
我们用普通命令行编译和链接
gcc -c -fno-builtin TinyHelloWorld.c
ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o
-e nomain 是指定以nomain为程序的入口函数。
程序可以正常运行。
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ ./TinyHelloWorld
Hello world!
我们用objdump查看这个ELF文件
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h TinyHelloWorld
TinyHelloWorld: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000065 08049000 08049000 00001000 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 0000000e 0804a000 0804a000 00002000 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .eh_frame 00000090 0804a010 0804a010 00002010 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .got.plt 0000000c 0804c000 0804c000 00003000 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .data 00000004 0804c00c 0804c00c 0000300c 2**2
CONTENTS, ALLOC, LOAD, DATA
5 .comment 00000026 00000000 00000000 00003010 2**0
CONTENTS, READONLY
接下来分析各个段
- .text保存指令,只读
- .rodata保存”Hello world!\n”,只读
- .data保存str全局变量,但是我们并没有改写此变量,所以实际也是只读的
- .comment保存编译器和系统版本信息,可以丢弃
据上分析,我们可以将三个段合并,将.comment段丢弃。
使用ld链接脚本
TinyHelloWorld.lds
ENTRY(nomain)
SECTIONS
{
. = 0x08048000 + SIZEOF_HEADERS;
tinytext : {*(.text) *(.data) *(.rodata)}
/DISCARD/ : { *(.comment)}
}
- ENTRY(nomain)规定了程序的入口函数为nomain()。
- SECTIONS是链接脚本的主体。
- 第一条是赋值语句,将当前虚拟地址设置成0x08048000 + SIZEOF_HEADERS
- 第二条语句是段转换规则,将”.text”、”.data”、”.rodata”合并到”tinytext”
- 第三条语句把”.comment”段丢掉。
gcc -c -fno-builtin TinyHelloWorld.c
ld -static -T TinyHelloWorld.lds -e nomain -o TinyHelloWorld TinyHelloWorld.o
用objdump查看一波
surager@LAPTOP-0H9RPTHA:~/linkers_loaders$ objdump -h TinyHelloWorld
TinyHelloWorld: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 tinytext 0000006f 08048094 08048094 00000094 2**0
CONTENTS, ALLOC, LOAD, CODE
1 .text.__x86.get_pc_thunk.ax 00000004 08048103 08048103 00000103 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .eh_frame 00000090 08048108 08048108 00000108 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .data.rel.local 00000004 08048198 08048198 00000198 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .got.plt 0000000c 0804819c 0804819c 0000019c 2**2
CONTENTS, ALLOC, LOAD, DATA
合并成功。