GNU GCC使用ld链接器进行链接的完整过程是怎样的
2016-10-23 10:53
253 查看
转自:https://www.zhihu.com/question/27386057
作者:潘阳
链接:https://www.zhihu.com/question/27386057/answer/36489378
来源:知乎
著作权归作者所有,转载请联系作者获得授权。
链接的过程实际上是为了解决多个文件之间符号引用的问题(symbol resolution)。编译时编译器只对单个文件进行处理,如果该文件里面需要引用到其他文件中的符号(例如全局变量或者函数),那么这时在这个文件中该符号的地址是没法确定的,只能等链接器把所有的目标文件连接到一起的时候才能确定最终的地址。
这个填写地址的过程,根据填写的地址的类型和时机,可以分为解决程序内部跨文件引用的链接时重定位、引用外部库文件的装载时重定位和引用外部库文件时为了加快加载速度而引入的延迟绑定。
ld里做的事主要是解决链接时重定位。这里会引入一个重定位表的数据结构,在ELF文件中是以一个segment的方式存在,通常叫.rel.data和.rel.text(分别对应数据段和代码段的重定位表)。其定义在elf.h
/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
r_offset 是重定位时需要修正的起始位置相对于段起始的偏移。
r_info 的低八位表示重定位入口的类型,高24位表示重定位的符号在符号表中的下标。重定位入口的类型是指计算最终地址时的算法。不同的处理器指令格式不同,导致计算地址的方法也不一样,需要使用这个变量来区分。
所以在得到一个目标文件(处于编译之后链接之前的二进制文件)时,其里面的重定位表的r_offset所指向的偏移位置里面的值是0或者其他无意义的值,因为这个符号的定义在另外一个文件里。只有在链接时,链接器有了所有文件的信息,才能把每个文件需要的外部引用符号的实际地址确定,并且填入r_offset所在位置。
举个栗子
我现在有这么两个.c文件,a.c里只有一个main函数,里面调用了位于b.c文件中的void b()函数。在b()函数中只有一句话,输出"Hello world"。如下图所示:
<img src="https://pic1.zhimg.com/cc1faaaf49e4215304530cbdc23eecd4_b.jpg" data-rawwidth="1460" data-rawheight="634" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic1.zhimg.com/cc1faaaf49e4215304530cbdc23eecd4_r.jpg">
然后用gcc的-c选项把a.c和b.c分别编译成目标文件a.o和b.o。再用objdump -r来查看一下a.o的重定位表,如下:
<img src="https://pic1.zhimg.com/ce2f93240d3e99ee4a50dd881715dfa4_b.jpg" data-rawwidth="1460" data-rawheight="598" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic1.zhimg.com/ce2f93240d3e99ee4a50dd881715dfa4_r.jpg">
然后我们再用objdump -d来查看a.o的代码段中0x07处是什么。
<img src="https://pic4.zhimg.com/1b587734256510aec2172a82012fd8f3_b.jpg" data-rawwidth="1460" data-rawheight="634" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic4.zhimg.com/1b587734256510aec2172a82012fd8f3_r.jpg">
从这里能看到此处的值是0xFFFFFFFC(little-endian),是调用b函数的跳转指令。虽然这个负值(-4)在跳转指令中一般是有意义的,但是如果这么跳转,执行这一句之后会跳转到0x07处(这里的计算方法是用这条指令的下一条指令首偏移加上这个值,这个方法是被上一张图片中的type: R_386_PC32定义的,详细的就不讲了),也就是跳转到条指令,这里就变成死循环了,显然不符合源代码里的意义。我们这里先记下这个值。
然后我们再用gcc把两个.o文件链接成executable,然后再用objdump -d来查看这个executable里这条跳转指令跳转目标的值变成了什么。
<img src="https://pic2.zhimg.com/df4370a92c42de5236900e88ed20f5d9_b.jpg" data-rawwidth="1460" data-rawheight="1066" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic2.zhimg.com/df4370a92c42de5236900e88ed20f5d9_r.jpg">executable中这里的值变成了0x09,也就是跳转到0x08048414(0x804840b
+ 0x09),正好就是b函数的起始偏移。
executable中这里的值变成了0x09,也就是跳转到0x08048414(0x804840b
+ 0x09),正好就是b函数的起始偏移。
作者:潘阳
链接:https://www.zhihu.com/question/27386057/answer/36489378
来源:知乎
著作权归作者所有,转载请联系作者获得授权。
链接的过程实际上是为了解决多个文件之间符号引用的问题(symbol resolution)。编译时编译器只对单个文件进行处理,如果该文件里面需要引用到其他文件中的符号(例如全局变量或者函数),那么这时在这个文件中该符号的地址是没法确定的,只能等链接器把所有的目标文件连接到一起的时候才能确定最终的地址。
这个填写地址的过程,根据填写的地址的类型和时机,可以分为解决程序内部跨文件引用的链接时重定位、引用外部库文件的装载时重定位和引用外部库文件时为了加快加载速度而引入的延迟绑定。
ld里做的事主要是解决链接时重定位。这里会引入一个重定位表的数据结构,在ELF文件中是以一个segment的方式存在,通常叫.rel.data和.rel.text(分别对应数据段和代码段的重定位表)。其定义在elf.h
/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
r_offset 是重定位时需要修正的起始位置相对于段起始的偏移。
r_info 的低八位表示重定位入口的类型,高24位表示重定位的符号在符号表中的下标。重定位入口的类型是指计算最终地址时的算法。不同的处理器指令格式不同,导致计算地址的方法也不一样,需要使用这个变量来区分。
所以在得到一个目标文件(处于编译之后链接之前的二进制文件)时,其里面的重定位表的r_offset所指向的偏移位置里面的值是0或者其他无意义的值,因为这个符号的定义在另外一个文件里。只有在链接时,链接器有了所有文件的信息,才能把每个文件需要的外部引用符号的实际地址确定,并且填入r_offset所在位置。
举个栗子
我现在有这么两个.c文件,a.c里只有一个main函数,里面调用了位于b.c文件中的void b()函数。在b()函数中只有一句话,输出"Hello world"。如下图所示:
<img src="https://pic1.zhimg.com/cc1faaaf49e4215304530cbdc23eecd4_b.jpg" data-rawwidth="1460" data-rawheight="634" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic1.zhimg.com/cc1faaaf49e4215304530cbdc23eecd4_r.jpg">
然后用gcc的-c选项把a.c和b.c分别编译成目标文件a.o和b.o。再用objdump -r来查看一下a.o的重定位表,如下:
<img src="https://pic1.zhimg.com/ce2f93240d3e99ee4a50dd881715dfa4_b.jpg" data-rawwidth="1460" data-rawheight="598" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic1.zhimg.com/ce2f93240d3e99ee4a50dd881715dfa4_r.jpg">
然后我们再用objdump -d来查看a.o的代码段中0x07处是什么。
<img src="https://pic4.zhimg.com/1b587734256510aec2172a82012fd8f3_b.jpg" data-rawwidth="1460" data-rawheight="634" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic4.zhimg.com/1b587734256510aec2172a82012fd8f3_r.jpg">
从这里能看到此处的值是0xFFFFFFFC(little-endian),是调用b函数的跳转指令。虽然这个负值(-4)在跳转指令中一般是有意义的,但是如果这么跳转,执行这一句之后会跳转到0x07处(这里的计算方法是用这条指令的下一条指令首偏移加上这个值,这个方法是被上一张图片中的type: R_386_PC32定义的,详细的就不讲了),也就是跳转到条指令,这里就变成死循环了,显然不符合源代码里的意义。我们这里先记下这个值。
然后我们再用gcc把两个.o文件链接成executable,然后再用objdump -d来查看这个executable里这条跳转指令跳转目标的值变成了什么。
<img src="https://pic2.zhimg.com/df4370a92c42de5236900e88ed20f5d9_b.jpg" data-rawwidth="1460" data-rawheight="1066" class="origin_image zh-lightbox-thumb" width="1460" data-original="https://pic2.zhimg.com/df4370a92c42de5236900e88ed20f5d9_r.jpg">executable中这里的值变成了0x09,也就是跳转到0x08048414(0x804840b
+ 0x09),正好就是b函数的起始偏移。
executable中这里的值变成了0x09,也就是跳转到0x08048414(0x804840b
+ 0x09),正好就是b函数的起始偏移。
相关文章推荐
- STM32F4板子使用LWIP进行组播收发数据的完整过程,附代码
- 如何使用AFL进行一次完整的fuzz过程
- 使用eclipse+Struts Studio 怎样进行程序调试
- 怎样使用Junit Framework进行单元测试的编写
- 使用IHttpHandler进行重定向过程中遇到的困惑
- 使用OpenJWeb(RAD) Java快速开发平台定制功能的完整过程示例
- 使用面向对象工具进行着面向过程的开发
- [导入]网络链接使用过程中的法律问题探析
- 使用Rose2003进行数据库建模并导入SQLServer2000的图解详细过程
- 使用Rose2003进行数据库建模并导入SQLServer2000的图解详细过程
- 怎样使用AJAX进行应用程序开发
- 使用游标编写的存储过程进行分页
- ASP.NET项目怎样进行管理?(VSS的使用)
- 使用UML建立的完整的系统用例的实现过程
- SQLServer2005里怎样对使用with encryption选项创建的存储过程解密
- 怎样使用Junit Framework进行单元测试的编写
- 使用Rose2003进行数据库建模并导入SQLServer2000的图解详细过程
- ASP.NET项目怎样进行管理?(VSS的使用)
- 使用Rose2003进行数据库建模并导入SQLServer2000的图解详细过程
- 使用OpenJWeb(RAD) Java快速开发平台定制功能的完整过程示例