11.1 符号及符号表
1. 符号的定义和引用
局部变量temp分配在栈
中,不会在过程外被引用,因此不是符号定义
2. 连接符号的类型
(1)三种类型
每个可重定位目标模块m都有一个符号表,它包含了在m中定义和引用的符号,有三种链接器符号:
类型 | 特征 | 举例 |
---|---|---|
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 |
链接器局部符号不是指程序中的局部变量(分配在栈中的临时性变量),链接器不关心这种局部变量
(2)类型举例
3. 目标文件中的符号表
(1)符号表中的信息
(2)符号表信息举例
4. 符号解析(Symbol Resolution)
• 目的:将每个模块中
引用的符号
与某个目标模块中的定义符号
建立关联。
• 每个定义符号在代码段或数据段中都被分配了存储空间,将引用符号与定义符号建立关联后,就可在重定位时将引用符号的地址重定位为相关联的定义符号的地址
。符号解析也称符号绑定。
•本地符号
在本模块内定义并引用,因此, 其解析较简单,只要与本模块内唯一的定义符号关联即可。
•全局符号
(外部定义的、内部定义的)的解析涉及多个模块,故较复杂
Q:“符号的定义实质是什么?
A:指被分配了存储空间。为函数名即指其代码所在区;为变量名即指其所占的静态数据区。
所有定义符号的值
就是其目标所在的首地址
(1)全局符号的强、弱
强符号 | 函数名 和已初始化 的全局变量名 |
---|---|
弱符号 | 未初始化 的全局变量名 |
(2)链接器对符号的解析规则
多重定义符号的处理规则:
Rule 1: 强符号不能多次定义
–强符号只能被定义一次,否则链接错误
Rule 2: 若一个符号被定义为一次强符号和多次弱符号,则按强定义为准
–对弱符号的引用被解析为其强定义符号
Rule 3: 若有多个弱符号定义,则任选其中一个
–使用命令gcc –fno-common链接时,会告诉链接器在遇到多个弱定义的全局符号时输出一条警告信息。
符号解析时只能有一个确定的定义(即每个符号仅占一处存储空间)
(3)多重定义符号的解析举例
例一
例二
例三
5. 多重定义全局符号的问题
(1)尽量避免使用全局变量
(2)一定需要用的话,就按以下规则使用
–尽量使用本地变量(static)
–全局变量要赋初值
–外部全局变量要使用extern
多重定义全局变量会造成一些意想不到的错误,而且是默默发生的,编译系统不会警告,并会在程序执行很久后才能表现出来, 且远离错误引发处。特别是在一个具有几百个模块的大型软件中, 这类错误很难修正。
11.2 静态链接和符号解析
如何划分模块?
静态链接对象: 多个可重定位目标模块(.o文件)+ 静态库(标准库、自定义库) (.a文件,其中包含多个.o模块)
• 库函数模块: 许多函数无需自己写,可使用共享的库函数
–如数学库, 输入/输出库, 存储管理库,字符串处理等
• 对于自定义模块,避免以下两种极端做法
①将所有函数都放在一个源文件中
•修改一个函数需要对所有函数重新编译
•时间和空间两方面的效率都不高
②一个源文件中仅包含一个函数
•需要程序员显式地进行链接
•效率高,但模块太多,故太繁琐
1. 静态共享库
(1) 静态库(.a archive files)
- 将所有相关的目标模块(.o)打包为一个单独的库文件 (.a),称为静态库文件,也称存档文件(archive);
- 使用静态库,可增强链接器功能,使其能通过查找一个或多个库文件中定义的符号来解析符号 ;
- 在构建可执行文件时,只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来 ;
- 在gcc命令行中
无需明显指定C标准库libc.a(默认库)
。
(2)静态库的创建
(3)常用静态库
libc.a ( C标准库)
– 1392个目标文件(大约8 MB)
– 包含I/O、存储分配、信号处理、字符串处理、时间和日期、随机数生成、定点整数算术运算
libm.a (the C math library)
– 401 个目标文件(大约1 MB)
– 浮点数算术运算(如sin, cos, tan, log, exp, sqrt, …)
(4)自定义一个静态库文件
举例:将myproc1.o和myproc2.o打包生成mylib.a
调用
2. 符号解析
(1)符号解析过程
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一定是空的。 |
符号解析过程
Q:若命令为:$gcc –static –o myproc ./mylib.a main.o,结果怎样?
A:首先,扫描mylib,因是静态库,应根据其中是否存在U中未解析符号对应的定义符号来确定哪个.o被加入E。因为开始U为空,故其中两个.o模块都不被加入E中而被丢弃。 然后,扫描main.o,将myfunc1加入U,myfunc1需要又mylib中的符号进行解析,但mylib中两个.o模块都已被丢弃,所以直到最后它都不能被解析。因此,
出现链接错误
。
3. 小结
(1)链接器对外部引用的解析算法要点如下:
– 按照命令行给出的顺序扫描
.o 和.a 文件
– 扫描期间将当前未解析的引用
记录到一个列表U中
– 每遇到一个新的.o 或.a 中的模块,都试图用其来解析U中的符号
– 如果扫描到最后,U中还有未被解析的符号,则发生错误
(2)问题和对策
– 能否正确解析与命令行给出的顺序有关
– 好的做法:将静态库放在命令行的最后
(3)链接顺序问题
假设调用关系如下:
func.o → libx.a 和liby.a 中的函数
libx.a →libz.a 中的函数
libx.a 和liby.a 之间、liby.a 和libz.a 相互独立
则以下几个命令行都是可行的:-gcc -static -o myfunc func.o libx.a liby.a libz.a -gcc -static -o myfunc func.o liby.a libx.a libz.a -gcc -static -o myfunc func.o libx.a libz.a liby.a
假设调用关系如下:
func.o → libx.a 和liby.a 中的函数
libx.a →liby.a 同时liby.a →libx.a
则以下命令行可行:-gcc -static -o myfunc func.o libx.a liby.a libx.a