在Linux系统中,每个可重定位目标模块都有一个符号表,它包含该模块定义和引用的符号信息。

符号的种类

符号类型 定义与引用规则 对应成员
全局符号 由本模块定义,并能被其他模块引用 非静态的C函数和全局变量
外部符号 由其他模块定义,并被本模块引用 在其他模块中定义的非静态的C函数和全局变量
局部符号 由本模块定义,并只能在本模块引用 静态的C函数和全局变量

值得注意的是,全局符号和外部符号是相对于某个模块而言的。
例如,一个由模块main.o定义、在模块temp.o中引用的符号,对于模块main.o,它是全局符号;而对于模块temp.o,它则是外部符号。

可重定位目标模块的.symtab节中的符号表包含了上述的符号,而非静态程序变量在运行时被储存在栈中。特别的是,静态的局部变量由编译器在.data节或.bss节中分配空间,并在符号表中创建一个有唯一名字的本地连接器符号。

对于同一模块中不同函数的同名静态局部变量,编译器将自动输出两个的不同名字。例如,用x.1表示函数f中x的定义,用x.2表示函数g中x的定义。

符号解析

链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。
局部符号在每个模块中只允许有一个定义。因此,局部符号的解析较为简单明了。而对全局符号的引用解析则相对复杂,因为全局符号可能在不同的模块中拥有同名的定义。

强符号与弱符号

全局符号可按照一定规则被划分为强符号或弱符号。

  • 强符号:已初始化的局部变量、函数。
  • 弱符号:未初始化的局部变量。

值得注意的是,强符号和弱符号是针对符号的定义而言的,而非针对引用。

多重定义的符号名的解析规则

根据上述强弱符号的定义,Linux链接器采用以下规则来处理多重定义的符号名。

  • 不允许有多个同名的强符号。
  • 如果有一个强符号和多个弱符号同名,那么选择强符号。
  • 如果有多个弱符号同名,那么从这些弱符号中任意选择一个。

其中,若违反第一条规则,程序将无法顺利链接,程序员能够非常明显地察觉到错误。但是,第二条和第三条规则的应用可能导致一些难以察觉的运行时错误。例如以下这个示例。

/* codeA.c */#include<stdio.h>voidf(void);int y =15212;int x =15213;intmain(){f();printf("x = 0x%x y = 0x%x", x, y);return0;}
/* codeB.c */double x;voidf(){
	x =-0.0;}

在一台x86-64/Linux机器上,double类型是8个字节,int类型是4个字节。x、y各占据4个字节,且y的地址在x之后。因此,代码B的第6行中的赋值x=-0.0将用-0的双精度浮点表示覆盖内存中x和y的位置,最终输出的结果应该将是:

x =0x0 y =0x80000000

规避隐性错误

由上述示例,链接器针对多重定义的符号名的第二条与第三条规则可能导致一些非常严重且难以察觉的错误。为规避此类隐性错误,程序员应该养成良好的编码习惯。

  • 尽可能避免使用全局变量。
  • 尽可能为全局变量添加static关键字。
  • 为全局变量赋初值。
  • 为引用的外部变量添加extern关键字。