7.5 符号和符号表

每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

  • 由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局变量;
  • 由其它模块定义并被模块M引用的全局符号。这些符号称为外部符号,对应于在其他模块中对应的非静态C函数和全局变量。
  • 只被模块m定义和引用的局部符号。他们对应于带static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其他模块引用。
    认识到本地链接器符号和本地程序变量不同是很重要的。.symtab中的符号表不包括对应于本地非静态程序变量的任何符号。这些符号在运行时在栈中被管理,链接器对此类符号不感兴趣。
    有趣的是,定义为带Cstatic属性的本地过程变量是不在栈中管理的,相反的,编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。比如,假设在同一模块中的两个函数各自定义了一个静态局部变量x;
int f()
{
    static int x = 0;
    return x;
}

int g()
{
    static int x = 1;
    return x;
}

在这种情况中,编译器向汇编器输出两个不同的名字的局部链接器符号。比如,它可以用x.1表示函数f中的定义,而用x.2表示函数g中的定义。
利用static 属性隐藏变量和函数名字
C程序员使用static属性隐藏模块内部的变量和函数声明,就像你在Java和C++中使用public和private声明一样。在C中,源文件扮演模块角色。任何带有static属性声明的全局变量或者函数都是模块私有的。类似的,任何不带static属性声明的全局变量和函数都是公开的,可以被其他模块访问。尽可能用static属性来保护你的变量和函数是很好的编程习惯。
符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab节中包含ELF符号表。这张符号表包含一个条目的数组。下图展示了每个条目的格式

--------------------------------------------------------------------------code/link/elfstructs.c
typedef struct{
    int name; /*String table offset*/
    char type:4, /*Function or data (4 bits)*/
             binding:4; /*Local or global (4 bits)*/
        char reserved; /*Unused*/
        short section; /*Section header index*/
        long value; /*Section offset or absolute address*/
        long size; /*Object size in bytes*/
}Elf64_Symbol;
--------------------------------------------------------------ELF符号表条目,type和binding字段每个都是4

name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小(以字节位单位)。type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding字段表示符号是本地的还是全局的。
每个符号都被分配到目标文件的某个节,由section字段表示,该字段也是一个到节头部表的索引。有三个特殊的伪节(pseudosection),它们在节头部表中是没有条目的:ABS代表不该被重定位的符号;UNDEF代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号;COMMON表示还未被分配位置的未初始化的数据目标。对于COMMON符号,value字段给出对齐要求,而size给出最小的大小。注意,只有可重定位目标文件中才有这些伪节,可执行目标文件中没有。
COMMON和.bss的区别很细微。现代的GCC版本根据以下规则来将可重定位目标文件中的符号分配到COMMON和.bss中:
COMMON 未初始化的全局变量
.bss 未初始化的静态变量,以及初始化为0的全局变量或静态变量
采用这种看上去很绝对的区分方式的原因来自于链接器执行符号解析的方式,我们会在7.6节中加以解释。
GUN READELF程序是一个查看目标文件内容的很方便的工具。比如下面的代码的程序的可重定位目标文件main.o的符号表中的最后三个条目。开始的8个条目没有显示出来,它们是链接器内部的局部符号。

int sum(int *a, int t)
{
    int array[2] = {1,2};
    int main()
    {
        int val = sum(array,2);
        return val;
    }
}

---------------------------------------------------
int sum(int *a, int t)
{
    int i, s= 0;
    for (i = 0; i < n; i++) s+=a[i];
    return a
}
Num:    Value           Size    type    Bind    Vis Ndx Name
8:  0000000000000000000 24  FUNC    GLOBAL  DEFAULT 1   main
9:  0000000000000000000 8   OBJECT  GLOBAL  DEFAULT 3   array
10: 0000000000000000000 0   NOTYPE  GLOBAL  DEFAULT UND sum

在这个例子中,我们看到全局符号main定义的条目,它是一个位于.text节中偏移量为0(即value值)处的24字节函数。其后跟随着的是全局符号array的定义,它是一个位于.data节中偏移量为0处的8字节目标。最后一个条目来自对外部符号sum的引用,READELF用一个整数索引来标识每个节。Ndx=1表示.text节,而Ndx=表示.data节。

你可能感兴趣的:(深入理解计算机原理)