编辑好的程序,依次经过预处理(注释,宏替换,头文件包含,生成.s文件)、编译(生成汇编文件.s )、汇编(生成静态可重定位目标文件,文件名是.o)、链接后最终得到可执行目标文件,这个笔记记录一下,链接多个.o文件时候,编译器做的事情。
1 程序的链接—链接器做的两件事
链接程序collect2/ld,将所有的可重定位目标文件静态链接在一起,就得到了可执行目标文件。如果链接动态库时候,只是在函数位置记录函数的相对地址,当程序运行起来后再动态加载动态库,然后重新计算该地址。链接程序时候,编译器做了两件事事情,分别是符号解析,地址重定位。
1.1 第一:符号解析
符号解析的目的就是将每个符号的引用和定义联系起来,从而确定模块中引用的每个符号都有明确的定义。
1.1.1 如何解析符号?
链接器检查可重定位目标文件的(.o)的symtab表,看符号的定义情况。
(1)情况1:符号在本模块定义的(本地符号)
如果符号在本模块定义,每个符号对应的空间就被定义在了本模块的某个节中(比如.text、.data等节) ,如果正确找到就编译通过。
(2)情况2:符号由本模块引用,但是在其它模块定义的(全局符号)
被标记为UND的符号就是引用的外部符号,链接器进行符号解析时,重点解析的是标记为UND的符号,解析时链接器会检查UND符号是否在其它模块中有定义。链接器会查看其它模块的.symtab符号表,看该符号是否在其它模块中有定义。如果找到定义,就将符号的引用和定义关联起来;如果找不到,链接器就会报undefined reference to ‘符号名’的错误。
1.1.2 符号解析相关的链接错误
(1)找不到外部符号
预编译,编译,这两个阶段都会报相应的错误,汇编阶段通过不会报错,说一下链接阶段的报错。
(2)多个文件重复定义的问题
这种情况的解决方法是:加变量或函数前加static 或者将定义变为声明。
(3) 函数重名问题
C语言的函数就是通过函数名生成符号的,所以函数名称不能相同。
C++是通过函数名+参数生成函数符号的,所以C++允许函数重载。
(4) 关于强弱符号的报错问题
链接器根据符号的强弱共存规则来解析符号,链接时,对于多个模块之间的全局符号来说,是区分强弱符号的,下面是全局变量和函数强弱符号的区分。
全局变量
强符号:初始化了的全局变量;
弱符号:未初始化的全局变量;
函数
强符号:函数定义
弱符号:函数声明
每个全局符号是强符号还是弱符号,在.symtab表中会有相应的记录。分别记录一下多个模块以及相同模块之间的强弱符号共存问题。
不同模块之间的重名符号共存原则:
1 不允许多个同名的强符号同时存在,存在的话就报错;
2 只能有一个强符号,其他都是弱符号,取强舍弱。
3 同名符号如果全都是弱符号的话,链接器留其中某个,其它舍弃。
上面的例子就是情况1.
相同模块之间的重名符号共存原则:
1 本模块内只能有一个强符号,如果有多个强符号就会报错,这种错误在编译阶段就会检查出来。
2 相同作用域内定义相同的全局符号和本地符号,编译时会报错。
static int a = 20;
int a = 10;
int main(void)
{
}
注: 编译器只认强弱符号,不会认声明和定义
编译器/链接器只知道强符号和弱符号,解析时是按照强弱符号的规则来处理的。所以编译器并不知道什么是声明什么是定义。
1.2 第二:地址重定位
链接器完成符号解析后,就开始重定位了。分为静态重定位和动态重定位。
动态重定位:在程序运行的过程中完成重定位的;静态重定位:就再链接阶段完成的,下面说说静态重定位做的事情。
首先,将同名节整合为新的同名聚合节;
然后,将可执行目标文件中各聚合节的地址,重定位为实际运行的地址。
首先,将同名节整合为新的同名聚合节。链接器重点是将.text和.data解聚合为同名聚合节,通过.rel.text 和 .rel.data
.rel.text中的信息:实现.text的聚合
.rel.data中的信息:实现.data的聚合
然后 ,如何制定重定位后的运行地址(虚存地址)?
将可执行目标文件中各聚合节的地址,重定位为实际运行的地址其实就是将程序在内存中实际运行时的内存地址赋给聚合节。程序在内存中运行时,就是按照重定位的地址来运行的。重定位之后的地址是虚拟内存地址,所以CPU取址时候,也是取的虚存地址,经过转换后的物理地址就是要取的指令地址。
地址的指定就是通过“链接脚本文件”来指定的,“链接脚本文件”里面会说明实际的运行地址是多少,重定位时会把实际的运行地址赋值给新的聚合节,如此一来,函数和全局变量就有了真正可以运行的运行地址。实际运行时,将程序拷贝到运行地址所指定的内存位置,cpu的pc存放第一条指令地址(指向第一条指令_start),然后整个程序就运行起来了。重定位时,给聚合节具体指定的运行地址应该是多少,要看系统。对于32位Linux,程序的虚存地址是从0x08048000 开始的。
每个进程的虚拟内存地址都是0x08048000,进程被调用运行时都是从该地址开始的,并且不同的进程不会冲突,因为不同进程的虚存对应的物理内存不同。