连接器,是把目标文件连接成可执行文件或动态库的工具。
它是将高级语言代码转化成二进制程序的最后一步。
编译之后的目标文件里,函数和全局变量的地址并不是真实内存地址,而是一个重定位符号。
连接器的作用,就是把这些重定位符号处理成真实的内存地址。
int printf(const char* fmt, ...);
int mAIn()
printf("hello world");
return 0;
这段代码在编译时有2个没法确定的数据:一是printf()函数的地址,二是字符串常量"hello world"的地址。
printf()函数是个库函数,它的地址可以在动态库里,也可以在静态库里,还可以在其他.o文件里,编译器是没法提前知道的。
字符串常量"hello world"是一个全局常量,它要放在.rodata数据段里。
.rodata数据段的位置编译器也是没法确定的,因为最终可能是多个目标文件连接成1个可执行程序,.rodata数据段的具体位置需要连接器来确定。
所以,编译器就在生成.o文件时就添加1个重定位节、1个符号表,他们包含2个重定位信息:printf()和"hello world"。
然后,由连接器去重写真实的内存地址。
上面代码用gcc -c编译成.o文件之后,用readelf -a查看它的信息,如下图:
ELF头
从ELF头可以看出,编译后的文件是可重定位文件,运行的系统架构是x86_64。
从它各个节的列表里可以找到.rela.text重定位节和.rodata节,前者存储重定位信息,后者存储常量数据。
各个节的列表
重定位节.rela.text的内容有2条:
1,一个指向.rodata节,表示这条重定位的地址在.rodata段里。
2,另一个没有具体的节,但给了一个函数名puts,表示要找的是这个函数(gcc在编译时都是把printf转化成puts函数)。
重定位节和符号表
在上图的符号表.symtab节里,也可以找到这2条信息:
1,其中的第5条(从0开始)就是"hello world"字符串的信息:它是一个LOCAL的字符串,也就是它的数据在当前文件里的某个节(SECTION),这个节的索引号是5(Ndx列)。
去上面的节列表里查找,可以发现.rodata段确实是第5个节。
2,第11条就是puts()函数的信息,它是GLOBAL的全局函数,不在当前文件的某个节里(Ndx是UND,undefined),需要连接器去其他地方找(库文件、其他.o文件,etc)。
Ndx这一列表示重定位数据所在的节,当前文件里实现的函数或变量都有节的索引号,但外部全局函数的索引号都是不确定的(UND)。
代码段,main函数的机器码
从代码段.text里的main()函数的机器码可以看出,装载"hello world"字符串的指令和调用printf()的指令里的地址都是00 00 00 00。
也就是说,这里需要的真实内存地址是32位的整数,有待连接器进一步填写。
00 00 00 00也就是高级语言里的NULL,在代码里都是无效的内存地址,如果不重填的话肯定会发生段错误。
lea指令装载全局变量时使用的内存地址,是变量地址与当前指令地址的偏移量。
rip,指令指针寄存器,它存的是当前指令的地址,x86_64对全局变量的寻址,都是使用的这种方式。
如果是静态连接,连接器把静态库.a和main函数的.o文件合在一起,然后修改这两个地址就可以了。
如果是动态连接,还需要用到全局偏移量表(GOT,global offset table)和PLT(过程连接表,procedure linkage table)。
动态连接之后的ELF头
gcc动态连接之后生成的可执行文件。
以前gcc都是生成可执行文件EXEC,现在都是生成动态库DYN直接运行了(即使main函数所在的文件也这样)。
上图ELF头可以看出类型是DYN,入口地址是0x530。
节的列表
动态链接之后文件有特别多的节,其中以.dyn开头的都是动态库相关的节。
.plt、.plt.got、.got,这3个就是动态连接所必须的节。
.rela.plt和.rodata依然存在,内容和静态连接得差不多。
所需的动态库信息
因为程序运行时要首先加载所需的动态库,所以必须含有动态库的信息,如上图。
这个程序比较简单,只需要libc.so.6库。
以下两图是重定位节的内容和动态库支持的库函数列表,可以看到他们都包含puts()函数,即main()函数所需的printf()。
重定位节
动态库函数的信息
最后简单说一下plt和got的内容:
plt分为2个节.plt和.plt.got。
.plt是只读的可执行代码,.plt.got是可写的数据。
操作系统不允许在运行时修改代码,只允许在运行时修改数据,所以动态连接的程序要想获得库函数的地址必须要一个小技巧[呲牙]
加载器必须把库函数的地址放在一个全局的函数指针变量里,然后让一段过渡代码去调用这个函数指针,从而实现动态运行。
这个全局的函数指针就是.plt.got里的一项。
当程序需要多个库函数时,这些函数指针就形成了一个函数指针数组,这就是.plt.got表。
调用(多个)库函数的过渡代码数组就是.plt表:它是有运行权限的,而且是只读的。
如下图:
1,最开始的时候,这个函数指针是加载器的加载函数。
2,当第一次调用puts()函数,加载函数会去动态库里查找它的真实地址,并填写在这里。
3,之后再调用时,就直接调用puts()函数了。
这是linux系统动态库函数的需求加载机制。
如果是普通变量,把它的地址放在.got表里就行。
动态库函数的需求加载