概述
在符号解析阶段,链接器根据所有输入的符号引用与定义信息,将程序中每个符号引用与链接过程中输入的可重定位目标文件中的符号表中的一个确定的符号关联起来。如果在链接过程中出现未定义的符号,那么链接过程中就会终止并且报错"undefined reference",这在我们日常开发中,并不少见:
多重定义的全局符号解析
对于局部符号以及未定义符号的解析,链接器的处理通常是很简单的,更复杂的情况是对全局符号的解析,尤其是当多个目标文件定义了相同的符号。为了能够正确处理这种多重定义的全局符号,编译器在编译时,会将这些符号区分成强符号和弱符号进行输出,这样链接器就可以根据符号的强弱属性,并按照特定的规则进行解析。
强符号与弱符号
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。注意,强符号和弱符号都是针对定义来说的,不是针对符号的引用。考虑下面的程序:
// foo1.c
#include <stdio.h>
int g_weak_val; // 未初始化的全局变量为弱符号
int g_strong_val = 5; // 已初始化的全局变量为强符号
int main(void)
{
printf("%d.\n", g_strong_val);
return 0;
}
根据强弱符号的定义,链接器使用下面的规则来处理多重定义的符号:
- 规则1:不允许强符号被多次定义,若有多个相同的强符号定义,则链接器报符号重复定义错误;
- 规则2:若在输入的目标文件中,定义了一个强符号以及多个同名的弱符号,那么选择强符号;
- 规则3:若在所有输入的目标文件中,只定义了多个同名的弱符号,那么选择其中占用空间最大的一个。
在日常程序开发中,我们经常会遇到编译时打印重复定义、未定义符号这一类的错误信息,现在从弱符号与强符号的处理规则,就可以理解编译的这一行为,假设我们在另外的源文件中定义同名的强符号:
// foo2.c
int g_strong_val = 9; // 重复定义的强符号,值为9
void foo(void)
{
return;
}
在这种情况下,如果将foo1.c
和foo2.c
进行编译链接,由于强符号g_strong_val
被多次定义,那么编译将报错:
使用GCC扩展定义弱符号
GCC扩展支持使用“attribute((weak))”来定义任何一个强符号为弱符号,这里我们修改上面foo1.c
,使用“attribute((weak))”来修饰g_strong_val
,即:
// foo1.c
int __attribute__((weak)) g_strong_val = 5;
再次进行编译foo1.c
和foo2.c
:
成功完成编译,并且根据强弱符号的处理规则,链接器选择了定义在foo2.c
文件中的g_strong_val
符号。
特殊符号
在使用ld链接器生成可执行文件时,会在链接脚本中定义一些特殊符号,这些符号可以用来对程序链接进行控制,同时也可以在程序中被引用,链接器会在将程序最终链接成可执行文件的时候将其解析成正确的值。一些常见的特殊符号定义如下:
__executable_start
:程序起始地址。__etext
或_etext
或etext
:代码段结束地址,即代码段最末尾的地址;_edata
或edata
:数据段结束地址,及数据段最末尾的地址;_end
或end
:程序结束地址。
相关参考
- 《程序员的自我修养——链接、装载与库》
- 《深入理解计算机系统》