谈谈程序链接及分段那些事
如果读过我之前的文章就会知道,程序构建大概需要经历四个过程:预处理、编译、汇编、链接,这里主要介绍链接这一过程。
链接链的是什么?
链接链的就是目标文件,什么是目标文件?目标文件就是源代码编译后但未进行链接的那些中间文件,如Linux下的.o,它和可执行文件的内容和结构很相似,格式几乎是一样的,可以看成是同一种类型的文件,Linux下统称为ELF文件,这里介绍下ELF文件标准:
可重定位文件:Linux中的.o,这类文件包含代码和数据,可被链接成可执行文件或共享目标文件,例如静态链接库。
可执行文件:可以直接执行的文件,如/bin/bash文件。
共享目标文件:Linux中的.so,包含代码和数据,一种是链接器可以使用这种文件和其它的可重定位文件和共享目标文件链接,另一种是动态链接器可以将几个这种共享目标文件和可执行文件结合,作为进程映像的一部分来执行。
core dump文件:进程意外终止时,系统可以将该进程的地址空间的内容和其它信息存到coredump文件用于调试,如gdb。
我们可以使用command file来查看文件的格式:
目标文件的构成
目标文件主要分为文件头、代码段、数据段和其它。
文件头:描述整个文件的文件属性(文件是否可执行、是静态链接还是动态链接、入口地址、目标硬件、目标操作系统等信息),还包括段表,用来描述文件中各个段的数组,描述文件中各个段在文件中的偏移位置和段属性。
代码段:程序源代码编译后的机器指令。
数据段:数据段分为.data段和.bss段。
.data段内容:已经初始化的全局变量和局部静态变量
.bss段内容:未初始化的全局变量和局部静态变量,.bss段只是为未初始化的全局变量和局部静态变量预留位置,本身没有内容,不占用空间。
除了代码段和数据段,还有.rodata段、.comment、字符串表、符号表和堆栈提示段等等,还可以自定义段。
.bss段不占用存储空间?
看下面代码:
我们查看下文件大小和各个段大小:
再看这段初始化的代码:
再查看下文件大小和各个段大小:
可以看到仅仅是做了一次初始化,文件大小就从12368变成了16368,正好是初始化了的那a[1000]的大小,这4000字节从.bss段移动到了.data段,程序大小增加了,这里可以看出.bss段不占据磁盘空间。
既然.bss段不占据空间,那它的大小和符号存在哪呢?
.bss段占据的大小存放在ELF文件格式中的段表(Section Table)中,段表存放了各个段的各种信息,比如段的名字、段的类型、段在elf文件中的偏移、段的大小等信息。同时符号存放在符号表.symtab中。
.bss不占据实际的磁盘空间,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化。
其实程序里还有好多系统保留段,还可以自定义段,将某个变量放在自定义段,如下:
可以使用一些工具查看ELF文件头以及各个段的内容:
查看文件头:
可以使用readelf查看文件头:ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度和段的数量。
查看段表的方法:
使用objdump查看ELF文件中包含的关键的段:
使用readelf查看ELF文件中包含的段:
objdump只能查看关键的段,而readelf可以查看所有段。
其中,.rela.text是针对.text段的重定位表,链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据段那些对绝对地址的引用的位置,这些重定位的信息都会放在.rela.text中,.rel开头的都是用于重定位。
LINK表示符号表的下标,INFO表示它作用于哪个段,值是相应段的下标。
字符串表(.strtab):保存普通字符串,比如符号名字。
段表字符串表(.shstrtab):保存段表中用到的字符串,比如段名。
ELF文件头和段表都有各自的结构体,这里不列举,只需要知道它里面存储的是什么东西就好。
程序为什么要分成数据段和代码段
数据和指令被映射到两个虚拟内存区域,数据段对进程来说可读写,代码段是只读,这样可以防止程序的指令被有意无意的改写。
有利于提高程序局部性,现代CPU缓存一般被设计成数据缓存和指令缓存分离,分开对CPU缓存命中率有好处。
代码段是可以共享的,数据段是私有的,当运行多个程序的副本时,只需要保存一份代码段部分。
经典语录:真正了不起的程序员对自己程序的每一个字节都了如指掌。
链接器通过什么进行的链接
链接的接口是符号,在链接中,将函数和变量统称为符号,函数名和变量名统称为符号名。链接过程的本质就是把多个不同的目标文件之间相互“粘”到一起,像玩具积木一样各有凹凸部分,有固定的规则可以拼成一个整体。
可以将符号看作是链接中的粘合剂,整个链接过程基于符号才可以正确完成,符号有很多类型,主要有局部符号和外部符号,局部符号只在编译单元内部可见,对于链接过程没有作用,在目标文件中引用的全局符号,却没有在本目标文件中被定义的叫做外部符号,以及定义在本目标文件中的可以被其它目标文件引用的全局符号,在链接过程中发挥重要作用。
可以使用一些命令来查看符号信息:
command nm:
command objdump:
command readelf:
有些符号在程序中并没有被定义,但是可以直接声明并且引用的符号称为特殊符号,这些符号其实是定义在ld链接器脚本中的,如下面代码中的符号:
输出:
为什么需要extern "C"
C语言函数和变量的符号名基本就是函数名字变量名字,不同模块如果有相同的函数或变量名字就会产生符号冲突无法链接成功的问题,所以C++引入了命名空间来解决这种符号冲突问题。同时为了支持函数重载C++也会根据函数名字以及命名空间以及参数类型生成特殊的符号名称。
由于C语言和C++的符号修饰方式不同,C语言和C++的目标文件在链接时可能会报错说找不到符号,所以为了C++和C兼容,引入了extern "C",当引用某个C语言的函数时加extern "C"告诉编译器对此函数使用C语言的方式来链接,如果C++的函数用extern "C"声明,则此函数的符号就是按C语言方式生成的。
以memset函数举例,C语言中以C语言方式来链接,但是在C++中以C++方式来链接就会找不到这个memset的符号,所以需要使用extern "C"方式来声明这个函数,为了兼容C和C++,可以使用宏来判断,用条件宏判断当前是不是C++代码,如果是C++代码则extern "C"。
这种技巧几乎在所有的系统头文件中都会被用到。
强符号和弱符号
我们经常编程中遇到的multiple definition of 'xxx',指的是多个目标中有相同名字的全局符号的定义,产生了冲突,这种符号的定义指的是强符号。有强符号自然就有弱符号,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。attribute((weak))可以定义弱符号。
链接器规则:
不允许强符号被多次定义,多次定义就会multiple definition of 'xxx'
一个符号在一个目标文件中是强符号,在其它目标文件中是弱符号,选择强符号
一个符号在所有目标文件中都是弱符号,选择占用空间最大的符号,int类型和double类型选double类型
强引用和弱引用
一般引用了某个函数符号,而这个函数在任何地方都没有被定义,则会报错error: undefined reference to 'xxx',这种符号引用称为强引用。与此对应的则有弱引用,链接器对强引用弱引用的处理过程几乎一样,只是对于未定义的弱引用,链接器不会报错,而是默认其是一个特殊的值。
这里可以编译链接成功,运行此可执行程序,会报非法地址错误,所以可以做下面的改进:
这种强引用弱引用对于库来说十分有用,库中的弱引用可以被用户定义的强引用所覆盖,这样程序就可以使用自定义版本的库函数,可以将引用定义为弱引用,如果去掉了某个功能,也可以正常连接接,想增加相应功能还可以直接增加强引用,方便程序的裁剪和组合。
如下:
使用如下方式链接:
对于弱符号和弱引用,其都仅是GNU工具链GCC对C语言语法的扩展,并不是C本身的语言特性。
参考资料
https://blog.csdn.net/guyongqiangx/article/details/53067434?locationNum=6&fps=1
https://blog.csdn.net/Move_now/article/details/69307890
《程序员的自我修养---链接、装载与库》
更多文章,请关注我的V X 公 主 号:程序喵大人,欢迎交流。
版权声明: 本文为 InfoQ 作者【程序喵大人】的原创文章。
原文链接:【http://xie.infoq.cn/article/91edcb862c9b4ed13911a7ccc】。文章转载请联系作者。
评论