"连接是将各类代码和数据部分收集起来并组合成为一个单一文件的过程",说的通俗一点(不许确),连接就是把编译生成的*.o文件整合成可执行文件的过程。一般编译器会帮咱们把预编译、编译、汇编和连接的过程都给作了,咱们不常常用到连接器,资料也少,下面就将笔者的体会和整理奉上。linux
注:广义的编译指的是预编译、编译、汇编和连接整个过程,狭义的编译指*.i文件生成*.o文件的过程。程序员
一个源程序通过预编译、编译、汇编和连接成为可执行文件,相信你们已经很熟悉了,确定有很多读者用gcc还原过这个过程,下面咱们再还原一遍,重点关注连接这一步。函数
1. 咱们要编译的文件列表spa
├── func.c
└── main.c3d
2. 文件内容以下orm
main.cblog
void func(); int main_global = 2; static int static_global = 3; int main() { int local = 3; func(); return 0; }
func.c内存
extern int main_global; void func() { }
第二节 分解编译过程编译器
看完上面的文件内容和关系,相信你们用一条命令就生成了可执行文件main:it
# gcc -o main main.c func.c 注:此命令会生成可执行文件main
或者多用几条命令也可生成:
# gcc -c main.c 注:此命令会生成main.o
# gcc -c func.c 注:此命令会生成func.o
# gcc -o main main.o func.o 注:此命令会生成可执行文件main
下面咱们把编译过程分解下:
第1步. 预编译(将宏、头文件等展开)
# gcc -E main.c -o main.i 注:此命令会生成main.i
# gcc -E func.c -o func.i 注:此命令会生成func.i
第2步. 编译(生成汇编语言)
# gcc -S main.i -o main.s 注:此命令会生成main.s
# gcc -S func.i -o func.s 注:此命令会生成func.s
第3步. 汇编(生成可重定位目标文件)
# gcc -c main.s -o main.o 注:此命令会生成main.o
# gcc -c func.s -o func.o 注:此命令会生成func.o
第4步. 连接(生成可执行文件)
# gcc -o main main.o func.o 注:此命令会生成可执行文件main
好了,至此,咱们经过gcc将源文件编译成了可执行文件。
读者可能会问:说好的连接器ld呢?别急,连接器理应出如今第4步。如今,咱们尝试用ld连接完成第4步。
1. 使用ld
咱们man了下ld,用法以下:
ld files... [options] [-o outputfile]
因而,咱们满怀信心,不假思索地写下了下面的语句:
# ld -o main main.o func.o
2. 出错
可是,结果并不如预期,出现了以下错误:
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8
3. 错误缘由
错误提示说的很明确,找不到入口符号_start ,咱们要在连接的时候指明程序入口。
4. 解决
既然如此,咱们用-e选项指明程序入口。
# ld -o main main.o func.o -e main
连接没报错,可是当咱们运行main时,提示Segmentation fault,这是由于连接时还缺乏一些参数。
那么还缺乏什么参数呢,让咱们看下上一节中的第4步,即用gcc连接目标文件的命令:
# gcc -o main main.o func.o 注:此命令连接main.o func.o, 生成可执行文件main
不难猜想,上面的命令用到了连接器, 如何验证呢,其实熟悉gcc的读者很清楚,给gcc加个-v参数,就能够打印gcc的执行过程用到的命令。好了,让咱们加个-v参数,一探究竟吧!
# gcc -v -o main main.o func.o
打印出以下信息:
...这里省略了一些打印信息...
/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2 --eh-frame-hdr -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/../lib64/crt1.o /usr/lib/../lib64/crti.o /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtbegin.o -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../.. main.o func.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtend.o /usr/lib/../lib64/crtn.o
好了,看到/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2了吗,这就是个连接器,什么?不是ld吗?别慌,collect2只是ld的一个别名。看到-l参数了吧,后面跟的就是连接用到的库,-L参数是查找路径。咱们用上面的命令完成最后的连接吧(固然,你能够把/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2换成ld):
/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2 --eh-frame-hdr -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/../lib64/crt1.o /usr/lib/../lib64/crti.o /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtbegin.o -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../.. main.o func.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtend.o /usr/lib/../lib64/crtn.o
好了,至此,咱们用完成了连接,并生成了可执行文件,下面就让咱们看看连接是如何工做的。
为了便于理解,咱们开篇就说了句通俗易懂的话:“连接就是把编译生成的*.o文件整合成可执行文件的过程”。那么是如何整合的呢,难道是把文件内容都拷贝到一个文件中吗,显然不是,可是咱们能够说连接是有规则的拷贝,按照什么规则呢,要想知道,还得先了解下*.o文件的格式。
咱们把*.o叫目标文件,实际上*.o文件只是目标文件的一种,目标文件的格式在Unix系统下被称为ELF格式(Executable and Linking Format,可执行和可连接格式),目标文件有三种:
1. 可重定位目标文件
上面咱们产生的*.o文件即main.o和func.o被称为可重定位目标文件,它与其余可重定位目标文件合并后生成可执行目标文件。典型的(可能还有其余节)ELF可重定位目标文件格式以下:
ELF header、Segment header table和.init等被称为节,每一个节的内容经过节的名字不难推测出来,此处再也不赘述。咱们重点来看下.symtab这个节,.symtab是一个符号表,里面存了一堆符号(变量、函数等),后面会讲到符号解析,说白点,就是查找这张表,找出表里的符号定义在哪里。每一个可重定位目标文件在.symtab中都有一张符号表,须要注意的是,这张符号表不包含局部变量信息,每一个可重定位目标文件obj都包含三种不一样的符号:
在obj中定义的,被其余文件引用的全局符号(如本文的main_global就是在main.c中定义的,能被其余文件使用的符号);
由其余文件定义的,被obj使用的全局符号(如本文func函数就是这样的符号,它在func.c文件中定义,被main.c文件使用);
只被obj文件定义和使用的全局符号(如本文static_global就是main.c中定义和使用的全局符号,其余文件不能使用)。
好了,让咱们看一下main.o的符号表,linux下咱们用readelf命令来查看ELF文件格式信息。
命令:
# readelf -s main.o 参数是小写s,查看符号表信息
输出:
.symtab中并无包含咱们在main.c中定义的local变量,连接只关心全局的符号信息。下面命令能够查看ELF文件节的信息。
命令:
# readelf -S main.o 参数是大写s,查看节信息
输出:
2. 可执行目标文件
咱们生成的main就是可执行目标文件,它能够被加载到内存中运行,它的格式和可重定位目标文件相似,以下图所示,须要注意的是,其头部包括程序的入口点(entry point),也就是文件被载入内存后要执行的第一条指令的地址。
咱们来看下可执行目标文件中的节:
命令:
# readelf -S main 参数是大写s,查看节信息
输出:
咱们能够看到Addr一列中,已是非0值,说明能够载入内存了,而可重定位目标文件main.o中的Addr一列为0.
3. 共享目标文件
一种特殊的可重定位目标文件,能够在运行时被动态地加载到内存中连接,如一些动态库.so文件,此处不讨论。
连接就是把一些类似的段合并到一块儿的过程,以下图所示:
这个合并要分两步完成,第一步是分析每一个可重定位目标文件中段的属性、长度和位置,进行地址分配;第二步是重定位,就是把符号引用和符号定义关联起来。
第一步 分配地址空间:
咱们来看下连接先后段属性的变化。
命令:
# objdump -h main.o
输出:
命令:
# objdump -h func.o
输出:
命令:
# objdump -h main
输出(省略了不关心的信息):
上面输出结果中VMA一列表示Virtual Memory Adress,即虚拟地址,咱们看到main.o和func.o的VMA都是0,由于还没分配地址空间,而在可执行文件main中已经有了值0x0804****(32位从0x08048000开始,64位从0x00400000开始),说明分配了地址空间。
第二步 符号解析和重定位:
咱们来看下main.o中的符号:
咱们看到main.o中的符号func前面有个标志U,即Undefined未定义的,这是显然的,由于main.c中用到的函数func是在func.c中定义的,咱们在编译main.c时,并不知道func在哪里,那么我么是何时知道它们在哪里呢,答案是连接后。咱们来看下连接后的可执行文件main的符号:
命令:
# nm main
输出:
D:Global data 符号,T:Global text符号。在连接事后,能够找到符号了,关于连接的详细介绍参考本文最后给出的参考文献。
(未完待续)
参考:
1. Randal E. Bryant..;<<Computer Systems: A Programmer's Perspective>>.
2.《程序员的自我修养:连接、装载和库》