程序员的自我修养之链接的符号

275 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

这一系列是《程序员的自我修养》的阅读笔记:

程序员的自我修养之程序的编译和链接

程序员的自我修养之目标文件里有什么

程序员的自我修养之ELF文件格式

程序员的自我修养之链接的符号

前言

之前我们聊到一个C语言程序到一个可执行文件会经过预编译、编译、汇编、链接四个过程,链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起,为了使不同目标文件之间能够相互粘合,这些目标文件之间必须有固定的规则才行。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。

在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。

链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(SymbolTable),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(SymbolValue)。

符号的类型

  1. 函数和变量

  2. 全局符号

  3. 段名

  4. 行号信息

  5. 局部符号

$ nm hello.o
0000000000000000 T main
                 U puts
0000000000000000 b static_var2.2361
0000000000000000 d static_var.2360

其中,符号类型的表示的对应关系如下:

A    Global absolute 符号。
a    Local absolute 符号。
B    Global bss 符号。
b    Local bss 符号。
D    Global data 符号。
d    Local data 符号。
f    源文件名称符号。
T    Global text 符号。
t    Local text 符号。
U    未定义符号。

ELF符号表结构

ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个Elf64_Sym结构(64位ELF文件)的数组,每个Elf64_Sym结构对应一个符号。这个数组的第一个元素,也就是下标0的元素为无效的“未定义”符号。Elf64_Sym的结构定义如下:

typedef struct
{
  Elf64_Word    st_name;                /* Symbol name (string tbl index) */
  unsigned char st_info;                /* Symbol type and binding */
  unsigned char st_other;               /* Symbol visibility */
  Elf64_Section st_shndx;               /* Section index */
  Elf64_Addr    st_value;               /* Symbol value */
  Elf64_Xword   st_size;                /* Symbol size */
} Elf64_Sym;

image.png

  1. 符号所在段

如果符号定义在本目标文件中,那么这个成员表示符号所在的段在段表中的下标;但是如果符号不是定义在本目标文件中,或者对于有些特殊符号,sh_shndx的值有些特殊,如下表所示:

宏定义名说明
SHN_ABS0xfff1表示该符号包含了一个绝对的值。比如表示文件名的符号就属于这种类型的
SHN_COMMON0xfff2表示该符号是一个"common块"类型的符号,一般来说,未初始化的全局符号定义就是这种类型的
SHN_UNDFF0表示该符号未定义。表示该符号在本目标文件被引用到,但是定义在其他目标文件中
  1. 符号值

在目标文件中,如果是符号的定义并且该符号不是“COMMON块”类型的(即st_shndx不为SHN_COMMON,“COMMON块”之后会讲),则st_value表示该符号在段中的偏移。即符号所对应的函数或变量位于由st_shndx指定的段,偏移st_value的位置。

如果符号是“COMMON块”类型的(即st_shndx为SHN_COMMON),则st_value表示该符号的对齐属性。

在可执行文件中,st_value表示符号的虚拟地址。这个虚拟地址对于动态链接器来说十分有用。

  1. 符号类型与绑定信息

该成员低4位表示符号的类型(Symbol Type),高28位表示符号绑定信息(Symbol Binding)。

image.png

image.png

ELF符号表结构实例

根据上面的介绍,我们对ELF文件的符号表有了大致的了解,接着将以hello.o里面的符号为例子,分析各个符号在符号表中的状态。使用readelf工具来查看ELF文件的符号。

$ readelf -s hello.o

Symbol table '.symtab' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.2361
     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 static_var.2360
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    11: 0000000000000000    32 FUNC    GLOBAL DEFAULT    1 main
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

readelf 的输出格式与上面描述的 Elf64_Sym 的各个成员几乎一一对应,第一列 Num 表示符号表数组的下标,从0开始,共13个符号;第二列 Value 就是符号值,即 st_value ;第三列 Size 为符号大小,即 st_size ;第四列和第五列分别为符号类型和绑定信息,即对应 st_info 的低4位和高28位;第六列 Vis 目前在C/C++语言中未使用,我们可以暂时忽略它;第七列 Ndx 即 st_shndx ,表示该符号所属的段;当然最后一列也最明显,即符号名称。

main 函数是定义在 hello.c 里面的,所在的位置都为代码段,所以 Ndx 为1,即hello.o里面,.text段的下标为1。这一点可以通过 readelf –a 或 objdump–x 得到验证,也可以回顾下 程序员的自我修养之ELF文件格式 。因为是函数,所以类型是 STT_FUNC;全局可见的,所以是 STB_GLOBAL ;Size 表示函数指令所占的字节数;Value表示函数相对于代码段起始位置的偏移量。

static_var.2360和 static_var2.2361 是两个静态变量,它们的绑定属性是 STB_LOCAL ,即只是编译单元内部可见。

对于那些 STT_SECTION 类型的符号,它们表示下标为 Ndx 的段的段名。它们的符号名没有显示,其实它们的符号名即它们的段名。比如2号符号的 Ndx 为1,那么它即表示 .text 段的段名,该符号的符号名应该就是“.text”。

“hello.c”这个符号表示编译单元的源文件名。

“puts” 符号是导入的 stdio.h 库中定义的。该符号在SimpleSection.c里面被引用,但是没有被定义。所以它的Ndx是SHN_UNDEF。