生成可执行文件的过程:
//用linux gcc编译器,编译main.c,sum.c
①预处理(preprocess):生成main.i
cpp main.c -o main.i
②编译(compile):生成main.s
gcc main.i -o main.s
//CSAPP上说的是cc1,但linux找不到这个,只有cc,但是会报错。
③汇编(assembly):生成main.o
as mian.s -o main.o
④链接(linked):生成可执行文件
ld -o prog main.o sum.o
目标文件
-
三种形式:
可重定位目标文件(file.o):包含二进制的代码和数据,可在链接时和其他目标文件合并起来,生成可执行目标文件。
共享目标文件:特殊类型的可重定向目标文件,可以再加载、运行时被动态加载进内存并链接。
可执行目标文件:包含二进制的代码和数据,其可被直接复制到内存并执行。
可重定位目标文件
-
不同系统格式:
windows:可移植可执行格式(Portable Executable),PE格式。
Max OS-X:Mach-O格式。
Linux和Unix:可执行可链接模式(Executable and Linkable Format),ELF格式。
-
linux的可重定位目标文件:
ELF格式为一个序列:{ELF,... , 节头部表}
ELF和节头部表中间,包含了很多个节, 如:
.text .rodate .data等等,如下图:
ELF格式
-
符号和符号表
//linux中,用readelf name -s查看符号表。
每个目标文件(模块)中都有一个符号表symtab,其包含了本文件中定义、引用的符号的信息。
三种不同符号:
1:模块自己定义,并能被其他模块引用的全局符号(函数或者变量,非静态)。
2:由其他模块定义,被本模块引用的全局符号。
3:模块自己定义的静态全局符号,仅被自己使用。
单个符号的表,如下:
某个符号表
可以在linux中,用objdump -dx file.o 来得到符号表的反汇编代码:
符号表反汇编
//ndx代表节索引
全局符号根据初始化与否,被分到不同的节:
1:分配到 .data:全局变量初始化,且不为0。
2:分配到 .bss :全局变量初始化为0;未初始化的静态变量符号 //(Block Storage Start)
3:分配到 .COMMON :没有初始化的全局变量。
4:分配到 .UND:没有定义的全局函数。
//UND、COMMON是伪节,仅在重定位文件中,执行文件无。
-
链接器解析多重定义的全局符号
解析:将全局符号的引用和定义关联起来。
强符号:已初始化的全局符号。
弱符号:未初始化的全局符号。
重定义 有三种情况:
1:两个文件中,都有同样的强符号出现,会报错。
2:两个文件有同名符号,但只有一个强符号,则选择强符号。
3:都是弱符号,则随机选择一个。
如图:
与静态库的链接
-
静态库定义:
所有的编译系统都提供一种机制,将所有相关的目标模块打包成一个单独的文件,称之为静态库。格式为 lib.a
如图:
静态库链接过程//(.a表示archive 存档,里面包含了很多个.o模块,当某模块引用了库中的模块名时,变会将其复制链接到可执行文件)
//通常编译器会自动隐式链接libc.a 标准函数库,而自定义的一些库,需要显式链接。创建静态库用AR工具。
不使用静态库的实现方法及自定义库:CSAPP P475。
链接器使用静态库来解析引用
-
编译驱动器按照命令行从左到右,进行顺序扫描。
链接器维护了三个集合:E(将被合并成可执行文件)、U(还未被解析的引用符号)、D(已经解析的有定义的符号集合)
1:如果为file.o,则将其放入E,并修改U,D。
2:如果为libx.a,则将其与U中符号进行匹配,如果匹配,则将该模块.o 放入E中。并修改U、D(U中清空已解析引用,并添加模块.o中未解析引用),存档文件扫描完后,将无引用的模块丢弃。
3:链接器完成对命令行的遍历后,若U非空,则进行报错并终止。否则,会合并E中目标文件,生成可执行文件。
//所以切记,不要将.o和.a顺序弄错,否则会无法解析引用,链接失败。
重定位
-
解析引用之后,便是要重定位:
1:将不同模块的相同节进行拼接:
如.text,形成一个新的.text节:其包含了所有模块的指令代码。数据代码、符号表等待也一样。2:对引用的符号进行重定位:
对全局变量、全局函数等进行重定位,使其调用时,指向可执行代码中的定义位置。在可重定位目标程序中,如果有未定义的引用,则会将其放在.rel.text、.rel.data中(rel=relocation),使编译器知道在生成可执行代码时,对这些符号进行重定位。
-
每个重定位条目,类似结构体,包含:
offset :在代码中的偏移信息。如main函数内部调用一个全局函数f()时,则offset = f()相对于main首地址的偏移量。
type:重定位之后的类型。
symbol:模块拼接之后,f()在符号表中的偏移量。
addend:这个值是针对不同情况,如f()的实现在mian()之后时,因为到call时,PC指令已经指到下一个代码位置,需要减去f()偏移量的大小,64位=8,32位=4;
如下图:
重定位entry
可执行目标文件
当链接器将所有模块解析及重定位之后,会生成一个可执行文件的ELF表,其与可重定位目标文件表类似,但是没有了两个.rel节,因为不需要重定位了。
增加的节:
init:定义了一个_init函数,程序的初始化代码会调用它。
段头部表:描述了可执行文件中,代码段和数据段对内存的映射关系。
加载可执行目标文件
在linux中使用 ./prog 可以运行目标文件。
-
加载流程://无动态链接
1:将代码复制到内存:
操作系统常驻在内存的加载器(loader),会将可执行目标文件中的代码和数据从磁盘复制到内存。2:将控制交给prog:
通过跳转到程序的第一条指令或者入口点(entry point)来运行程序。
其在内存中,图示如下:
动态链接共享库
静态库的缺点是太耗磁盘和内存空间,尤其是当同时运行了上百个程序的时候,几乎每个程序中都有标准I/O函数及其他重复函数,占用了很大的内存的资源。
-
共享库
(shared library)用.so后缀表示。是第三种目标文件,即共享目标文件。在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程称为动态链接(dynamic linking)。动态连接器:dynamic linker。执行动态链接过程。
"共享"说明:
1:磁盘中,对于一个动态库只有一个.so文件,所有引用该文件的可执行目标文件共享一个库,而非静态库,需要将其内容复制链接到可执行文件中。
2:在内存中,共享库的.text节的副本,可以被不同的正在运行的进程所共享。大大节约了内存空间。
动态链接过程
动态链接时:
1:和静态库不同,生成prog21时,并没有复制代码和数据。反而是链接器,复制了一些重定位和符号表信息。
2:在内存中时,链接器根据这些信息,复制动态库中的文本和数据到另一个内存段,并对prog21中,有引用动态库里定义的符号进行重定位。
3:动态链接器将控制传递给prog21,开始运行。共享库的位置固定不变了。
从应用程序中加载和链接共享库、位置无关代码PIC(Position Independent Code)、库打桩机制没有学,因为书里比较简略。