- 什么是符号
- 什么是符号的定义,什么是符号的引用
- 链接符号的类型有哪些,各自有什么特点
- 符号表是什么
- 链接器对符号的解析规则、过程
一、
1、 什么是符号?
符号:就是其实程序中的变量名、函数名。
2、什么是符号的定义,什么是符号的引用?
符号的定义:就是程序中变量名、函数名的定义(位于定义位置的 变量名、函数名);
符号的引用:就是程序中变量名、函数名的引用(不在定义位置的 变量名、函数名);
3、 链接符号的类型有哪些,各自有什么特点?
每个可重定位目标模块m 都有一个符号表,它包含了在m中定义和引用的符号,有3种链接器符号:
类型 | 特征 | 举例 |
Global symbols (模块内部定义的全局符号) | 由模块m定义并能被其他模块引用的符号。 例如,非static C函数和非 static的C全局变量(指不带static的函数\全局变量) | 如,main.c 中的全局变量名buf |
External symbols (外部定义的全局符号) | 由其他模块定义并被模块m引用的全局符号 | 如,main.c 中的函数名swap |
Local symbols (本模块的局部符号) | 仅由模块m定义和引用的本地符号。 例如,在模块m中定义的带static的(C函数和全局变量) | 如,swap.c 中的static变量名bufp1 |
注意:1.局部变量temp分配在栈
中,不会在过程外被引用,因此不在符号的类型中;
2. 链接器的局部符号 不是指程序中的局部变量(分配在栈中的临时性变量),链接器不关心这种局部变量;
4、符号表是什么?
目标文件的.symtab节记录着符号表信息,符号表 是一个结构体数组,每个表项(16字节)的结构如下:
使用readelf -s 可查看目标文件的符号表信息,以上面的main.o和swap.o为例:
二、链接器对符号的解析规则
1、首先要知道2个名词(全局符号的强、弱)
强符号 | 函数名 和已初始化 的全局变量名 |
---|---|
弱符号 | 未初始化 的全局变量名 |
2、符号的解析:就是确定符号引用关系,将每个模块中引用的符号 与 某个目标模块的符号定义建立关联。
将符号引用和符号定义建立关联后,将引用符号的地址“重定位”为相关联的符号定义的地址。(重定位)
符号解析时,只能有一个确定的定义(即每个符号仅占一处存储空间)。
所以,如果碰到符号存在多重定义时,就得有相应的处理规则:
- Rule 1:强符号不能多次定义
强符号只能被定义一次,否则链接错误。
- Rule 2:若一个符号被定义为一次强符号和多次弱符号,则按强符号定义为准。
- Rule 3:若有多个弱符号定义,则任选其中一个。
使用命令 gcc -fno-common链接时,会告诉链接器在遇到多个弱定义的全局符号时输出一条警告信息。
三、链接器对符号的解析过程(篇一)
3.1、背景知识
程序到可执行文件一共有四个操作:
- 预处理
- 编译
- 汇编
- 链接
经过汇编以后已经生成了二进制的文件。但是我们为了好看,还是用汇编代码代替。
再回顾一下第4个操作链接的步骤:1.符号解析;2.重定位;
- 确定符号引用关系(符号解析)
- 合并相关 .o 文件(重定位)
- 确定每个符号的地址(重定位)
- 在指令中填入新的地址(重定位)
确定符号引用关系,将每个模块中引用的符号与某个目标模块的定义符号建立关联
看到上面的 P0.o
和 P1.o
中的箭头了吗。符号解析就是去干箭头干的活。
也就是说,每个定义符号在代码段(函数)和数据段(变量)都分配了存储空间,将引用符号与定义符号建立关联后,就可以在重定位时将引用符号的地址重定位为相关联的定义符号的地址。
为了能建立这样的联系,定义了一个叫做「符号表」(symbol table)的东西
=》所以符号解析的整体过程如下:
- 程序中有定义和引用的符号(包括函数和变量)
- 编译器将定义的符号放在符号表中
- 符号表是一个结构数组
- 每个表项包含符号名、长度位置等信息
- 链接器将每个符号的引用都与一个确定的符号定义建立关联
3.2、若干集合
E 将被合并到一起以组成可执行文件的所有目标文件集合(可执行文件\静态库文件)
U 未解析符号(未不对应定义符号关联的的引用符号)的集合 (符号)
比如说:在一个.o文件用了max(a,b),但是.o文件中却没有找到这个max符号的定义,这个max现在就称作未解析符号。
D 当前已被加入到E的所有目标文件中定义符号的集合(定义符号)
3.3、举例
开始E、U、D为空,符号解析过程如下:(扫描文件的顺序:按gcc指令的顺序扫描)
①命令行中文件按照顺序出现,假如现在是f文件,链接器看它是什么文件:
是可重定位目标文件:就将f放到E中,并将f中未解析符号放到U,定义符号放到D。
是静态库文件:链接器尝试把U中所有未解析符号与f中各个目标模块中的定义的符号匹配。如果f中的某个m模块定义了U中的未解析符号x,就将:m放入E,x从U移到D,直到U和D不再变化。又假设f中的模块n,里面的定义符号都没在链接中用到,那么n就被扔了!(随扫随丢)库文件里也有可能存在那种未解析符号,那么就把静态库里的未解析符号也放到U里去。
如图例,处理main.o的时候,先把这个模块放到E中。发现d是定义符号,那么d就加入D,而main.c第8行的d已经被定义了,所以d不会被放到U里面。
②若往D中加入了一个已经存在的符号(双重定义),或扫描完所有文件时U非空,则连接器报错并停止。否则,链接器将生成.o文件,最终U中一定为空,D中符号唯一。
③最终还会自动检索默认库,也是匹配符号的过程。它不需要在 gcc -static 后明显指出。
【注意】链接器对外部引用的解析
①顺序扫描.o和.a,一般把静态库放到后面。如果静态库之间用相互引用关系,则必须按照引用关系在命令行中排列静态库文件,使得:对每个静态库目标模块中的外部引用(定义)的符号,包含其定义的静态库文件排在其后面。【每个静态库中,可能有外引符号,因此,为方便理解:可以把静态库中用到的模块,当成.o文件,理解成库内模块中的未定义符号也放到U。】
我们的目标是:每一个找不到家的孩子找到家,每一个未关联符号U都找到它的定义!
②最终U中一定是空集,D每个符号都是唯一的!
原文链接:https://blog.csdn.net/qq_39286580/article/details/106516434
三、链接器对符号的解析过程(篇二)
首先创建三个集合 E、U、D
- E:合并在一起的所有目标文件(还未重定位)
- U:没有解析的符号(定义符号和引用符号还没有被建立联系)
- D:定义符号的集合
① 开始E、U、D为空,首先扫描main.o,把它加入E, 同时把myfun1加入U,main加入D。 |
② 接着扫描到 mylib.a,将U中所有符号(本例中为myfunc1)与 mylib.a中所有目标模块(myproc1.o和myproc2.o )依次匹配,发现在myproc1.o中定义了myfunc1 ,故myproc1.o加入E,myfunc1从U转移到D。在 myproc1.o中发现还有未解析符号printf,将其加到 U。 |
③ 不断在mylib.a的各模块上进行迭代以匹配U中的符号,直到U、D都不再变化。 |
④ 此时U中只有一个未解析符号printf,而D中有main和myfunc1。因为模块 myproc2.o没有被加入E中,因而它被丢弃。 |
⑤ 接着,扫描默认的库文件libc.a,发现其目标模块printf.o定义了 printf,于是printf也从U移到D,并将 printf.o加入E,同时把它定义的所有符号 加入D,而所有未解析符号加入U。 |
处理完libc.a时,U一定是空的。 |