提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
目录
一. 编译的过程
GCC 编译分为4个步骤, 预处理,编译,汇编和链接,下面以hello_world为例:
#include <stdio.h>
int main (int argc, char *argv[])
{
printf("hello world");
}
预处理 (-E):
gcc -E hello_world.c -o hello_world.i //生成预编译文件,替换宏定义和一些路径
编译 (-S)
gcc -S hello_world.i -o hello_world.s //生成汇编文件
汇编 (-o)
gcc -o hello_world.s -o hello_world.o //生成目标文件
链接 (ld):
ld hello_world.o -e main -o hello_world
这句话编译器会报错误,因为printf是外部函数,链接器执行的时候需要链接所有相关的.o文件, 我们可以通过gcc -v参数去看正常编译链接的全过程。
gcc hello_world.c -o hello_world --> 这条命令等于上面的四条命令
cc -v hello_world.c -o hello_world
Using built-in specs.
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-languages=c,c++,objc,obj-c++,java,fortran,ada --enable-java-awt=gtk --disable-dssi --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-1.5.0.0/jre --enable-libgcj-multifile --enable-java-maintainer-mode --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --disable-libjava-multilib --with-ppl --with-cloog --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.4.7 20120313 (Red Hat 4.4.7-23) (GCC)
COLLECT_GCC_OPTIONS='-v' '-o' 'hello_world' '-mtune=generic'
/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/cc1 -quiet -v hello_world.c -quiet -dumpbase hello_world.c -mtune=generic -auxbase hello_world -version -o /tmp/cc4wYAxQ.s
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.4.7/include-fixed"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../x86_64-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/local/include
/usr/lib/gcc/x86_64-redhat-linux/4.4.7/include
/usr/include
End of search list.
GNU C (GCC) version 4.4.7 20120313 (Red Hat 4.4.7-23) (x86_64-redhat-linux)
compiled by GNU C version 4.4.7 20120313 (Red Hat 4.4.7-23), GMP version 4.3.1, MPFR version 2.4.1.
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: f1e04b41791fd0c9eea88d5989031e7d
COLLECT_GCC_OPTIONS='-v' '-o' 'hello_world' '-mtune=generic'
as -V -Qy -o /tmp/ccIvLpKe.o /tmp/cc4wYAxQ.s
GNU assembler version 2.20.51.0.2 (x86_64-redhat-linux) using BFD version version 2.20.51.0.2-5.48.el6_10.1 20100205
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/:/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'hello_world' '-mtune=generic'
/usr/libexec/gcc/x86_64-redhat-linux/4.4.7/collect2 --eh-frame-hdr --build-id -m elf_x86_64 --hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o hello_world /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../.. /tmp/ccIvLpKe.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.4.7/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.4.7/../../../../lib64/crtn.o
加粗部分CC1相当于编译 gcc -S, collect2 相当于链接 ld, 我们可以看到链接除了需要hello_world.o之外还需要其他的库和一些.o文件。
二. 静态链接
在一个大型的项目里,我们会有很多.c文件, 但最终经过编译器编译之后只会有一个可执行文件,因此编译器做的主要工作就是将同一个项目下的所有目标文件(.o) 链接成最终的可执行文件。
2.1 目标文件
再讲静态链接之前需要简单了解下什么是目标文件, 目标文件就是.o文件, 由.c文件编译而成,每个.c文件都有自己对应的.o文件。
这次我们以Lib.c 和 Program1.c 为例:
/* a.c */ /* b.c */
extern int shared; int shared
int main() void swap (int *a, int *b)
{ {
int a = 100; *a ^= *b ^= *a ^= *b;
swap (&a, &shared);
} }
首先分别编译两个.c问件生成对应的.o文件:
gcc -c a.c -o a.o
gcc -c b.c -o b.o
objdump -h a.o
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000027 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000068 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000068 2**2
ALLOC
3 .comment 0000002e 0000000000000000 0000000000000000 00000068 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000096 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
objdump -h b.o
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000004c 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 0000008c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000090 2**2
ALLOC
3 .comment 0000002e 0000000000000000 0000000000000000 00000090 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000be 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
链接a,o和 b.o, 这次就可以连接成功,因为我们的例子中没有引用其他外部函数,比如标准C库函数 printf 或stdio.h 头文件,因此整个链接过程只需要a.o 和 b.o参与。
ld a.o b.o -e main -o ab
objdump -h ab
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000074 00000000004000e8 00000000004000e8 000000e8 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000058 0000000000400160 0000000000400160 00000160 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 00000000006001b8 00000000006001b8 000001b8 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .comment 0000002d 0000000000000000 0000000000000000 000001bc 2**0
CONTENTS, READONLY
下面开始做一个简单的解释,在.o文件和可执行文件中我们都看到了.text段和 .data段,我们称之为section, 区别就是.o文件中的各个section VMA (虚拟地址)LMA(加载地址)是有值的,因此我们可以知道连接器会在链接完成之后确定程序的虚拟地址,这个虚拟地址可以理解成实际程序运行所在的地址空间。
因此链接的目的就是将各个.o中的相同section合并到一起,至于怎么合并,因为每个.o文件中都会引用一些变量,因此每个.o文件里还有一个叫符号表的东西,连接器根据各个.o文件中的符号表最终链接生成可执行文件。
下图摘自程序员的自我修养, 我们可以看到 .o文件中只有文件偏移的概念,图中的text段是0x34, 但我们这边是0x40
2.2.符号解析重定位
这节主要简单描述下连接器连接成 ab可执行文件的符号表解析过程, 上例子
objdump -d a.o
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: be 00 00 00 00 mov $0x0,%esi
18: 48 89 c7 mov %rax,%rdi
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25 <main+0x25>
25: c9 leaveq
26: c3 retq
看我加粗的部分,首先所有.o文件都是从虚拟地址0开始的,因为这时还不是可执行文件, 第二点看第13条指令"be 00 00 00 00",这条指令是上面的a,c中shared赋值语句,但因为shared是在b.c中定义的,因此这里先用 0代替, 第20条调转指令一样,因为swap是外部函数,目前不知道地址,因此也用0代替。
再来看最终的可执行文件ab
objdump -d ab
Disassembly of section .text:
00000000004000e8 <main>:
4000e8: 55 push %rbp
4000e9: 48 89 e5 mov %rsp,%rbp
4000ec: 48 83 ec 10 sub $0x10,%rsp
4000f0: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
4000f7: 48 8d 45 fc lea -0x4(%rbp),%rax
4000fb: be b8 01 60 00 mov $0x6001b8,%esi
400100: 48 89 c7 mov %rax,%rdi
400103: b8 00 00 00 00 mov $0x0,%eax
400108: e8 03 00 00 00 callq 400110 <swap>
40010d: c9 leaveq
40010e: c3 retq
40010f: 90 nop0000000000400110 <swap>:
400110: 55 push %rbp
400111: 48 89 e5 mov %rsp,%rbp
400114: 53 push %rbx
400115: 48 89 7d f0 mov %rdi,-0x10(%rbp)
400119: 48 89 75 e8 mov %rsi,-0x18(%rbp)
40011d: 48 8b 45 f0 mov -0x10(%rbp),%rax
400121: 8b 10 mov (%rax),%edx
400123: 48 8b 45 e8 mov -0x18(%rbp),%rax
400127: 8b 08 mov (%rax),%ecx
400129: 48 8b 45 f0 mov -0x10(%rbp),%rax
40012d: 8b 18 mov (%rax),%ebx
40012f: 48 8b 45 e8 mov -0x18(%rbp),%rax
400133: 8b 00 mov (%rax),%eax
400135: 31 c3 xor %eax,%ebx
400137: 48 8b 45 f0 mov -0x10(%rbp),%rax
40013b: 89 18 mov %ebx,(%rax)
40013d: 48 8b 45 f0 mov -0x10(%rbp),%rax
400141: 8b 00 mov (%rax),%eax
400143: 31 c1 xor %eax,%ecx
400145: 48 8b 45 e8 mov -0x18(%rbp),%rax
400149: 89 08 mov %ecx,(%rax)
40014b: 48 8b 45 e8 mov -0x18(%rbp),%rax
40014f: 8b 00 mov (%rax),%eax
400151: 31 c2 xor %eax,%edx
400153: 48 8b 45 f0 mov -0x10(%rbp),%rax
400157: 89 10 mov %edx,(%rax)
400159: 5b pop %rbx
40015a: c9 leaveq
40015b: c3 retq
我们可以看到可执行文件ab包含swap和main连个函数,并且虚拟地址不再是0,而是 4000e8,
在main数中 "4000fb: be b8 01 60 00 mov $0x6001b8,%esi"
比较a.o中 : 13: be 00 00 00 00 mov $0x0,%esi
shared已经有值了(0x6001b8), 具体这个值是如何算出来的,我们可以理解为连接器内部(LD)有自己算法,当他加载各个.o文件的时候,会读取各个文件里面一个叫符号表的section,然后合并每个.o文件中相同的段,并解析符号表来确定最终的合并地址,这里仅限于静态链接,动态链接要复杂写,稍后再讲。
2.3 符号表
可以先用nm查看符号结果
nm a.o
0000000000000000 T main
U shared
U swap
符号表也是文件中的一个段,section名叫symtab,
readelf -s a.o
Symbol table '.symtab' contains 11 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 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 39 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
简单说下, main前面那个1代表 section 1,section 1也就是代码段,代表main位于代码段
a.o 全部section, 我们可以看到.text是在 1. GLOBA 代表全局变量, UND 就是undefine,证明符号是个外部符号。
readelf -S a.o
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000027 0000000000000000 AX 0 0 4
[ 2] .rela.text RELA 0000000000000000 00000550
0000000000000030 0000000000000018 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000068
0000000000000000 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 00000068
0000000000000000 0000000000000000 WA 0 0 4
[ 5] .comment PROGBITS 0000000000000000 00000068
000000000000002e 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 00000096
0000000000000000 0000000000000000 0 0 1
[ 7] .eh_frame PROGBITS 0000000000000000 00000098
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000580
0000000000000018 0000000000000018 10 7 8
[ 9] .shstrtab STRTAB 0000000000000000 000000d0
0000000000000059 0000000000000000 0 0 1
[10] .symtab SYMTAB 0000000000000000 00000430
0000000000000108 0000000000000018 11 8 8
[11] .strtab STRTAB 0000000000000000 00000538
0000000000000016 0000000000000000 0 0 1
那么连接器是怎么知道哪些指令是要被调整的呢 ? 这些指令那部分要调整,怎么调整 ?
事实上 ELF文件中有个重定位表的结构用来保存所有重定位信息:
objdump -r a.o
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000014 R_X86_64_32 shared
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
OFFSET 14 和 21就是需要重定位的位置, 对照a.o反汇编:正好对应 shared赋值和call swap函数。
objdump -d a.o
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: be 00 00 00 00 mov $0x0,%esi
18: 48 89 c7 mov %rax,%rdi
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25 <main+0x25>
25: c9 leaveq
26: c3 retq
总结: 之所以链接是因为我们目标文件中用到的符号被定义在其他目标文件, 所以要把他们链接起来, 连接器会加载各个.o文件读取他们的重定位表,其实重定位过程也是符号解析过程,每个目标文件都可以定义一些符号,也可能引用到另一在其他目标文件的符号,重定位过程,每个重定位入口都对应一个符号引用,这时连接器就回去查找由所有目标文件的符号表组恒的全局符号表,找到相应的符号进行重定位。
根据符号表 "readelf -s a.o" 进行指令修正。
2.4 如何根据符号表重定位表进行指令修正