程序一定要从 main 函数开始运行吗?
程序一定要从main函数开始运行吗?本文涉及静态链接相关知识。
对于静态链接先提出两个问题:
Q:每个目标文件都有好多个段,目标文件在被链接成可执行文件时,输入目标文件中的各个段如何被合并到输出文件?
A:合并相似的段,将所有的.text段合并到输出文件的.text段,将所有的.data段合并到输出文件的.data段。
Q:链接器如何为他们分配在输出文件中的空间和地址?
A:这里涉及到程序链接的两个步骤:
空间与地址分配:扫描所有的输入目标文件,获得它们每个段的长度属性和位置,收集输入目标文件中的符号表中的所有符号定义和符号引用,统一放到一个全局符号表中,合并所有的段,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。
符号解析与重定位:使用第一步收集到的所有信息,读取输入文件中段的数据及重定位信息,进行符号解析和重定位,调整代码中的地址,将每个段中需要重定位的指令和数据进行“修补”,使他们都指向正确的位置。
tips:外部符号指的是目标文件需要引用的符号,但是定义在其它目标文件中,链接前外部符号地址都是000000之类,链接后的可执行文件就可以看见这些外部符号都是有地址的。链接就是把相似的段放在一起,先找到段的偏移地址,再找出符号在段中的偏移,这样可以确定符号在整个可执行程序中的地址。
对于那些需要重定位的符号,都会放在重定位表里,也叫重定位段,即.rel.data、.rel.text等,如果.text段有被重定位的地方,就有.rel.text段,如果.data段有被重定位的地方,就有.rel.data段。可以使用objdump查看目标文件的重定位表。
源代码:
使用nm也可以查看需要重定位的符号:
对于UND类型,这种未定义的符号都是因为该目标文件中有关于他们的重定位项,在链接器扫描完所有的输入目标文件后,所有这种未定义的符号都应该能在全局符号表中找到,否则报符号未定义错误。
注意:我们代码里明明用的是printf,为什么它却引用了puts的符号呢,因为编译器默认情况下会把只用一个字符串参数的printf替换成puts, 可以节省格式解析的时间,使用-fno-builtin会关闭这个内置函数优化选项,如下:
tips:现在的程序和库通常来讲都很大,一个目标文件可能包含成百上千个函数或变量,当需要用到某个目标文件的任意一个函数或变量时,就需要把它整个目标文件都链接进来,也就是说那些没有用到的函数也会被链接进去,这会导致链接输出文件变的很大,造成空间浪费。
有一个编译选项叫函数级别链接,可以使得某个函数或变量单独保存在一个段里面,都链接器需要用到某个函数时,就将它合并到输出文件中,对于没用到的函数则将他们抛弃,减少空间浪费,但这会减慢编译和链接过程,GCC编译器的编译选项是:
可能很多人都会以为程序都是由main函数开始执行和结束的,但其实不是,在main函数调用之前,为了保证程序可以顺利进行,要先初始化进程执行环境,如堆分配初始化、线程子系统等,C++的全局对象构造函数也是这一时期被执行的,全局析构函数是main之后执行的。
Linux一般程序的入口是__start函数,有两个段:
.init段:进程的初始化代码,一个程序开始运行时,在main函数调用之前,会先运行.init段中的代码。
.fini段:进程终止代码,当main函数正常退出后,glibc会安排执行该段代码。
如何指定程序入口
在ld链接过程中使用-e参数可以指定程序入口,由于一段简短的printf函数其实都依赖了好多个链接库,我们也不太方便使用链接脚本将目标文件与所有这些依赖库进行链接,所以使用下面这段内嵌汇编的程序来打印一段字符串,这段程序不依赖任何链接库就可以打印出字符串内容,读者如果不懂其中的含义也不用担心,只需要了解下面介绍的链接知识就好。
代码如下:
使用如下命令生成目标文件:
看下输出的test.o的符号:
这里由于我的源文件是.cc结尾,所以是以c++方式编译的,所以符号变成了上面的形式,如果变成了test.c,符号如下:
再使用-e指定入口函数符号:
如何使用自定义链接脚本实现自定义段的功能
在ld链接过程中使用-T参数可以指定链接脚本,通过ld -verbose可以查看默认的链接脚本,原文太长,这里简单截取了一部分:
这里自定义一个简单的链接脚本test.lds
再使用-T指定链接脚本:
上面的tinytext一行是指将.text段、.data段、.rodata段的内容都合并到tinytext段中,使用readelf查看段的信息。
工具小贴士
关于静态链接库:
关于objdump:
关于分析ELF文件格式:
关于查看目标文件符号信息:
关于符号的说明:
如果符号类型是小写的,表明符号是局部符号,大写表示符号是全局符号。
A:该符号的值是绝对的,在以后的链接过程中,不允许进行改变。这样的符号值,常常出现在中断向量表中,例如用符号来表示各个中断向量函数在中断向量表中的位置。
B:该符号的值出现在.bss段中,未初始化的全局和静态变量。
C:该符号的值在COMMON段中,里面的都是弱符号。
D:该符号位于数据段中。
I:该符号对另一个符号的间接引用
N:debug符号
R:该符号位于只读数据区
T:该符号位于代码段
U:该符号在当前文件未定义,定义在别的文件中
?:该符号类型没有定义
参考资料
https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/
《程序员的自我修养》
更多文章,请关注我的V X 公 主 号:程序喵大人,欢迎交流。
版权声明: 本文为 InfoQ 作者【程序喵大人】的原创文章。
原文链接:【http://xie.infoq.cn/article/254b336db11408e7f0694ed48】。文章转载请联系作者。
评论