c语言中的符号解析

来源:互联网 发布:淘宝刷单后果是什么 编辑:程序博客网 时间:2024/06/09 21:02

连接器通过把每一个引用和一个确切的符号(symbol)定义相关联来解析符号引用。而符号定义是从它的(linker)输入的可重定位(relocatable)目标文件(object files)的符号表(symbol tables)获得的。
而当引用的是在同一个模块module)中定义的本地符号(local symbols)的时候,符号解析是直接进行的。编译器只允许在一个模块中,每个本地符号仅仅只能有一个定义。编译器同时确保获得了本地连接器符号的静态本地变量(static local variables)拥有唯一的名字(name)。
                
但是,解析一个全局符号的引用,相对复杂些。当编译器遇到一个不是在当前模块(current module)定义的符号(一个变量或是函数名字(function name)),它假设该符号在其他别的模块定义了,产生一个链接器符号表入口(entry),并把它交给链接器(linker)处理。如果该链接器在它任何的输入模块中无法找到被引用符号的定义,它将打印一条错误信息并且终止。例如,如果我们试图在一台Linux机器上编译链接下面的原文件,
void foo (void);
int main ()
{
    foo ();
    return 0;
}
然后编译器(compiler)运行没有故障,但是链接器(linker)当它不能够解析foo的引用的时候终止:
unix> gcc -Wall -O2 -o linkerror linkerror.c
/tmp/ccSz5uti.o: In function 'main':
/tmp/ccSz5uti.o:(.text+0x7): undefined reference to 'foo'
collect2: ld returned 1 exit status
对于全局符号的解析同样复杂,因为同一个符号可能被多个目标文件定义。在这种情况下,链接器必须标注一个错误,或者设法在这些定义选择一个并且取消剩下的定义。Unix系统采用这种方法,这种方法设计到编译器,汇编器及链接器的协作,并且能够给不小心的程序员导致一些令人困惑的bugs。
aside:c++ 和JAVA 识别编码,识别解码(mangling和demangling)
1.链接器如何解析多重定义的全局符号
在编译期间,编译器输出每个全局符号给汇编器,它们要么是强的要么是弱了(strong or weak),汇编器在可重定位的目标文件的符号表中对这些信息进行明确的编码。函数(function)和初始化了的全局变量(initialized global variables)为强符号。没有初始化的全局变量(uninitiated global variables)为弱符号。
在强符号和弱符号的概念下,Unix链接器使用如下规则处理多重定义的符号:
A:不允许多个强符号定义(对于同一个符号而言)。
B:对于给定的一个强符号(Strong symbol)和多个弱符号(weak symbols),选择哪个强符号。
C:对于给定的多个弱符号,选择其中的任一个。
例如,假设我们试图编译链接如下的两个C模块(modules):
/* foo1.c */
int main ()
{
     return 0;
}
/* bar1.c */
int main ()
{
    return 0;
}
在这种情况下,链接器将会产生一个错误信息,因为强符号main被定义多次(Rule A):
unix> gcc foo1.c bar1.c
/tmp/cca015022.o: In function 'main':
/tmp/cca015022.o(.text+0x0):multiple definetion of 'main'
/tmp/cca015021.o(.text+0x0):first defined here
类似的,对于下面的模块,由于强符号x被定义了两次,链接器同样会产生一个错误信息(Rule A)。
/* foo2.c */
int x = 15213;
int main ()
{
    return 0;
}
/* bar2.c */
int x = 15213;
void f ()
{
}
但是,如果x在其中的一个模块没有初始化,链接器会默认选择在另一个定义的强符号(Rule B):
/* 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;
}
在运行时,函数f将x值从15213改为15212,这可能给main的作者带来不受欢迎的惊奇!注意正常情况下,链接器没有给出指示它检测到x的多个定义:
unix>gcc -o foobar3 foo3.c bar3.c
unix>./foobar3
x = 15212
如果对x有两个弱定义(weak definitions),同样的事情会发生(Rule C):
/* 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;
}
Rules B和C的应用可能会导致给粗心的程序员产生一些潜在的运行时难以理解的错误,特别是同一个符号(duplicate)但是却有不同的类型的情况。考虑下面的情况,其中x在一个模块中定义为int型,另一个中定义为double型的:
/* foo5.c */
#include stdio.h>
void f (void);
int x = 15213;
int y = 15212;
int main ()
{
    f ();
    printf ("x = 0x%x y = 0x%x /n", x, y);
    return 0;
}
/* bar5.c */
double x;
void f ()
{
    x = -0.0;
}
在一个IA32/Linux的机器上,doubles占用8字节(bytes),ints是4字节。因此,在bar5.c中的第五行的赋值x = -0.0会用双精度浮点数代表的负数(double-precision floating-point representation of negative one)重写(overwrite)x和y的内存位置(foo5.c中的第五行和第六行)。
linux>gcc -o foobar5 foo5.c bar5.c
linux>./foobar
x = 0x0 y = 0x80000000
这是一个微细并且很坏(subtle and nasty)的错误,特别是因为它是静静发生的,没有编译系统的任何的警告信息(no warning),(我用的suse11 gcc i-586 4.3还是给出了warning的:Warning alignment 4 of symbol 'x' in /tmp/cc2pZDnF.o is smaller than 8 in /tmp/cc4hGlKZ.o),并且在这个程序过程中一般显示出来很晚,原理发生错误的地方(it typically manifests itself much later in the execution of the program, far away from where the error occurred.)。在一个拥有上百个模块的大的系统中,这种类型的错误是极其难以修复的(fix),尤其是在很多程序员不明白链接器是如何工作的情况下。如果有怀疑的话,用这样一个标志-warn-common的标志(flag)添加到像GCC这样的链接器的选项中(flag)。
原文参考:Computer.Systems.-.A.Programmers.Perspective.-.Randal.Bryant,.David.O.Hallaron

原创粉丝点击