<返回更多

技术文章:连接器的技术内幕

2022-09-12  今日头条  底层技术栈
加入收藏

连接器,是把目标文件连接成可执行文件动态库的工具。

它是将高级语言代码转化成二进制程序的最后一步。

编译之后的目标文件里,函数和全局变量的地址并不是真实内存地址,而是一个重定位符号。

连接器的作用,就是把这些重定位符号处理成真实的内存地址。

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表里就行。

动态库函数的需求加载

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>