7.6.1 链接器如何解析多重定义的全局符号

7.6.1 链接器如何解析多重定义的全局符号

链接器的输入是一组可重定位目标模块。每个模块定义一组符号,有些是局部的(只对定义该符号的模块可见),有些是全局的(对其他模块也可见)。如果多个模块定义同名的全局符号,会发生什么呢?下面是 Linux 编译系统采用的方法。

在编译时,编译器向汇编器输出每个全局符号,或者是强(strong)或者是弱(weak),而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

根据强弱符号的定义,Linux 链接器使用下面的规则来处理多重定义的符号名:

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

比如,假设我们试图编译和链接下面两个 C 模块:

/* foo1.c */
int main()
{
    return 0;
}
/* bar1.c */
int main()
{
    return 0;
}

在这个情况中,链接器将生成一条错误信息,因为强符号 main 被定义了多次(规则 1):

linux> gcc foo1.c bar1.c
/tmp/ccq2Uxnd.o: In function `main':
bar1.c:(.text+0x0): multiple definition of `main'

相似地,链接器对于下面的模块也会生成一条错误信息,因为强符号 x 被定义了两次(规则 1):

/* foo2.c */
int x = 15213;

int main()
{
    return 0;
}
/* bar2.c */
int x = 15213;

void f()
{
}

然而,如果在一个模块里 x 未被初始化,那么链接器将安静地选择在另一个模块中定义的强符号(规则 2):

/* foo3.c */
#include <stdio.h>
void f(void);

int x = 15213;

int main()
{
    f();
    printf("x = %d\n", x);
    return 0;
}
/* bar3.c */
int x;

void f()
{
    x = 15212;
}

在运行时,函数 fx 的值由 15213 改为 15212,这会给 main 函数的作者带来不受欢迎的意外!注意,链接器通常不会表明它检测到多个 x 的定义:

linux> gcc -o foobar3 foo3.c bar3.c
linux> ./foobar3
x = 15212

如果 x 有两个弱定义,也会发生相同的事情(规则 3):

/* foo4.c */
#include <stdio.h>
void f(void);

int x;

int main()
{
    x = 15213;
    f();
    printf("x = %d\n", x);
    return 0;
}
/* bar4.c */
int x;

void f()
{
    x = 15212;
}

规则 2 和规则 3 的应用会造成一些不易察觉的运行时错误,对于不警觉的程序员来说,是很难理解的,尤其是如果重复的符号定义还有不同的类型时。考虑下面这个例子,其中 x 不幸地在一个模块中定义为 int,而在另一个模块中定义为 double

/* foo5.c */
#include <stdio.h>
void f(void);

int y = 15212;
int x = 15213;

int main()
{
    f();
    printf("x = 0x%x y = 0x%x \n",
           x, y);
    return 0;
}
/* bar5.c */
double x;

void f()
{
    x = -0.0;
}

在一台 x86-64/Linux 机器上,double 类型是 8 个字节,而 int 类型是 4 个字节。在我们的系统中,x 的地址是 0x601020y 的地址是 0x601024。因此,bar5.c 的第 6 行中的赋值 x = -0.0 将用负零的双精度浮点表示覆盖内存中 xy 的位置(foo5.c 中的第 5 行和第 6 行)!

linux> gcc -Wall -Og -o foobar5 foo5.c bar5.c
/usr/bin/ld: Warning: alignment 4 of symbol `x' in /tmp/cclUFK5g.o
is smaller than 8 in /tmp/ccbTLcb9.o
linux> ./foobar5
x = 0x0 y = 0x80000000

这是一个细微而令人讨厌的错误,尤其是因为它只会触发链接器发出一条警告,而且通常要在程序执行很久以后才表现出来,且远离错误发生地。在一个拥有成百上千个模块的大型系统中,这种类型的错误相当难以修正,尤其因为许多程序员根本不知道链接器是如何工作的。当你怀疑有此类错误时,用像 GCC -fno-common 标志这样的选项调用链接器,这个选项会告诉链接器,在遇到多重定义的全局符号时,触发一个错误。或者使用 -Werror 选项,它会把所有的警告都变为错误。

在 7.5 节中,我们看到了编译器如何按照一个看似绝对的规则来把符号分配为 COMMON 和 .bss。实际上,采用这个惯例是由于在某些情况中链接器允许多个模块定义同名的全局符号。当编译器在翻译某个模块时,遇到一个弱全局符号,比如说 x,它并不知道其他模块是否也定义了 x,如果是,它无法预测链接器该使用 x 的多重定义中的哪一个。所以编译器把 x 分配成 COMMON,把决定权留给链接器。另一方面,如果 x 初始化为 0,那么它是一个强符号(因此根据规则 2 必须是唯一的),所以编译器可以很自信地将它分配成 .bss。类似地,静态符号的构造就必须是唯一的,所以编译器可以自信地把它们分配成 .data.bss

练习题 7.2

在此题中,REF(x.i) → DEF(x.k) 表示链接器将把模块 i 中对符号 x 的任意引用与模块 k 中 x 的定义关联起来。对于下面的每个示例,用这种表示法来说明链接器将如何解析每个模块中对多重定义符号的引用。如果有一个链接时错误(规则 1),写“错误”。如果链接器从定义中任意选择一个(规则 3),则写“未知”。

A.

/* Module 1 */          /* Module 2 */
int main()              int main;
{                       int p2()
}                       {
                        }
(a) REF(main.1) → DEF(________.________)
(b) REF(main.2) → DEF(________.________)

B.

/* Module 1 */          /* Module 2 */
void main()             int main = 1;
{                       int p2()
}                       {
                        }
(a) REF(main.1) → DEF(________.________)
(b) REF(main.2) → DEF(________.________)

C.

/* Module 1 */          /* Module 2 */
int x;                  double x = 1.0;
void main()             int p2()
{                       {
}                       }
(a) REF(x.1) → DEF(________.________)
(b) REF(x.2) → DEF(________.________)