WangYu::Space

Study, think, create, and grow. Teach yourself and teach others.

编译与链接 - 符号

分类:编译与链接创建时间:2023-07-11 16:19:55修改时间:2023-07-23 17:57:26

前一篇{% include link.html uid=20231552 %}中,主要介绍了 ELF 文件的结构,我们知道 ELF 文件中包含多个 section 和 segment,但这仅仅是一个开始。后续的文章中,我将深入到不同的 section 和 segment 中,介绍诸如符号表、静态链接、动态链接等概念。

符号

在编写程序时,我们会给变量、函数命令,并在代码中使用名称来访问它们,这些名称就是符号。程序编译后,程序中使用的变量会被转换为内存地址或者偏移量。但这不意味着这些变量名称没用了,编译器生成机器代码的同时还导出了符号。这些符号在链接与调试的时候会有大用途,符号则存储在 ELF 文件中的某些 section 中。

链接器用来整合多个目标文件,并构造可执行程序或者动态库,对其他目标文件中符号的引用使用的正是符号的名称。在链接阶段,链接器需要基于名称来定位到符号的地址或者偏移量。如果没有符号信息,链接器将无能为力。在使用调试器的时候,我们通过名称来查看一个变量的内容,如果没有符号,我们就需要知道变量在内存中的地址,这显然是很困难的。在使用动态库的时候,我们通过名称来获取变量或函数的地址。机器在运行编译后的机器指令时并不需要符号的名称信息,但符号的名称不可或缺。

符号定义

在 ELF 文件中,每个符号使用如下结构体表示:

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;

该结构的各个字段说明如下:

st_name

一个整数,指向字符串表中该符号的名称的起始位置

st_info

包含符号的 bind 类型和符号类型,可以使用如下两个宏来获取 bind 和 type 信息

ELF64_ST_BIND(info)    ((info) >> 4)
ELF64_ST_TYPE(info)    ((info) & 0xf)

常见的符号类型如下:

常见的符号绑定类型有如下几种:

st_other

包含符号的可见性(visibility)属性,当目标文件被链接为可执行文件或动态链接库后,符号的可见性用于控制该符号是否能被其他目标文件中访问到。常见的可见性类别如下:

符号的绑定类型和可见性有点让人迷惑。绑定类型用于在链接阶段控制符号能否被其他目标文件访问到,比如我们将变量使用 static 修饰,该变量的绑定类型就是 STB_LOCAL,它就只能在当前目标文件中被访问。

符号的可见性则是在运行时控制当前的动态库中的符号是否能被其他动态库看到。在实现动态库的时候,一般会有多个 .c 文件,编译后有多个 .o 目标文件,多个目标文件中会涉及到符号的相互访问,因此把符号都设置为 static 类型是不现实的,需要在其他目标文件中使用的符号的绑定类型需要被设置 STB_GLOBAL。当动态库构造完成后,希望该动态库中的符号可以隐藏起来,只能在本动态库中被访问到,这个时候就可以设置符号的可见性,控制符号是否能被其他动态库或者当前加载动态库的程序看到。 st_shndx

包含一个 index,可以通过 ELF 中的 section header table 定位到对应的 section,表示此符号定义在那个 section 中。

st_value

符号的值,解读方式与 ELF 文件类型有关,对于 REL 类型(即可可重定位文件)而言,这个字段中是该符号在 section 中的相对偏移量。对于 EXEC、DYN(可执行文件、动态库)类型,st_value 中保存的是虚拟地址,如果 section 类型为 SHT_UNDEFst_value 为 0,则说明这是一个需要在运行时重定位的符号。

st_size

符号的大小,如果此值为 0 则表示符号大小未知。

字符串表

符号的结构体 Elf64_Sym 中的 st_name 为符号的名称,这里 st_name 并不是一个字符串而是一个数字,它指向字符串表中某个字符串,该字符串为符号的名称。

下面我们实际动手来观察一下 ELF 文件的符号表。下面使用 hello world 程序 hello 来演示,首先使用 readelf -S 查看 section 表:

$ readelf -S hello
There are 37 section headers, starting at offset 0x4678:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
...
  [35] .strtab           STRTAB           0000000000000000  00004360
       00000000000001d2  0000000000000000           0     0     1

.strtab 这个 section 中存放的是字符串,我们重点关注它。

可以看到 .strtab 的偏移量为 0x4360 = 17248,大小为 0x1d2 = 466,可以使用如下命令把这部分内容从 ELF 文件截取出来:

$ dd if=hello of=str.txt bs=1 skip=17248 count=466
$ hexdump -C str.txt | head -n 10
00000000  00 63 72 74 73 74 75 66  66 2e 63 00 5f 5f 43 54  |.crtstuff.c.__CT|
00000010  4f 52 5f 4c 49 53 54 5f  5f 00 5f 5f 44 54 4f 52  |OR_LIST__.__DTOR|
00000020  5f 4c 49 53 54 5f 5f 00  64 65 72 65 67 69 73 74  |_LIST__.deregist|
00000030  65 72 5f 74 6d 5f 63 6c  6f 6e 65 73 00 5f 5f 64  |er_tm_clones.__d|
00000040  6f 5f 67 6c 6f 62 61 6c  5f 64 74 6f 72 73 5f 61  |o_global_dtors_a|
00000050  75 78 00 63 6f 6d 70 6c  65 74 65 64 2e 37 32 36  |ux.completed.726|
00000060  38 00 64 74 6f 72 5f 69  64 78 2e 37 32 37 30 00  |8.dtor_idx.7270.|
00000070  66 72 61 6d 65 5f 64 75  6d 6d 79 00 5f 5f 43 54  |frame_dummy.__CT|
00000080  4f 52 5f 45 4e 44 5f 5f  00 5f 5f 46 52 41 4d 45  |OR_END__.__FRAME|
00000090  5f 45 4e 44 5f 5f 00 5f  5f 64 6f 5f 67 6c 6f 62  |_END__.__do_glob|

.strtab 中包含的都是字符串,每个字符串都是以 \0 结尾,并一个挨着一个存储。前面提到表示符号的结构体中有 st_name 字段,该字段是一个整型,实际上就是字符串表中的一个 offset,通过这个 offset 可以查询在字符串表中查到对应的符号名称。因为所有的符号都是 \0 结尾的,所以没有必要记录符号名称的长度。

下面是字符串表的一个图例:

0123456789
0\0name\0stri
10ng\0print\0m
20ain\0

这里面包含如下符号:

index字符串
0null
1name
5string
13print
19main

符号表

一个 ELF 文件中最多包含两个符号表,它们放置在 .symtab.dynsym 这两个 section 中。

$ readelf -S hello
There are 37 section headers, starting at offset 0x4678:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  ...
  [ 5] .dynsym           DYNSYM           0000000000400348  00000348
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003a8  000003a8
       000000000000003f  0000000000000000   A       0     0     1
  ...
  [34] .symtab           SYMTAB           0000000000000000  00003c70
       00000000000006f0  0000000000000018          35    56     8
  [35] .strtab           STRTAB           0000000000000000  00004360
       00000000000001d2  0000000000000000           0     0     1
  ...

观察 hello 程序 section 列表,可以看到 .dynsym 的 link 字段指向 .dynstr.symtab 的 link 字段指向 .strtab。这是因为这两个符号表使用了不同的字符串表。

.dynsym 中存方的是动态加载的符号,可以使用如下命令观察它们:

$ readelf --dyn-syms hello

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

直接读取 .dynstr 中的内容:

$ dd if=hello of=str.txt bs=1 skip=936 count=63
$ hexdump -C str.txt 
00000000  00 6c 69 62 63 2e 73 6f  2e 36 00 70 72 69 6e 74  |.libc.so.6.print|
00000010  66 00 5f 5f 6c 69 62 63  5f 73 74 61 72 74 5f 6d  |f.__libc_start_m|
00000020  61 69 6e 00 47 4c 49 42  43 5f 32 2e 32 2e 35 00  |ain.GLIBC_2.2.5.|
00000030  5f 5f 67 6d 6f 6e 5f 73  74 61 72 74 5f 5f 00     |__gmon_start__.|

为什么需要两个符号表呢?

符号表 .dynsym 中的符号会被装载器使用到,比如 hello 中的 __libc_start_main 则是程序的入口,装载器把 hello 程序载入到内存后,做好必要的初始化工作后,就会查找 __libc_start_main 符号,并跳转到该函数。另外动态库中的符号表 .dynsym 会在运行时动态加载符号时被使用到,所以 .dynsym 这个符号表示不可或缺的。

.symtab 只是在调试和静态连接的时候会被使用到,对可执行程序和动态库而言,如果不需要被调试,完全可以不需要 .dynsym.strtab 这两个 section。

当你的程序比较大时候,符号表中就会存在很多的符号,可执行文件的大小相应的也会大不少。这个时候可以使用 strip 命令将符号表和字符串表全部删除掉。

$ gcc hello.c -o hello

$ ll hello
-rwxrwxr-x 1 work work 20408 Aug 11 15:12 hello

$ readelf -s hello  # 查看符号

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

Symbol table '.symtab' contains 74 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000004002a8     0 SECTION LOCAL  DEFAULT    1 
     2: 00000000004002dc     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000400300     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000400328     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000400348     0 SECTION LOCAL  DEFAULT    5 
     ...

$ strip -s hello  # 删除符号

$ readelf -s hello  # 再次查看符号

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

$ ll hello
-rwxrwxr-x 1 work work 14376 Aug 11 15:12 hello

$ ./hello   # 执行程序
hello world

使用 strip 命令删除掉符号表 .symtab 之后,不会影响程序的运行,同时程序的体积会减少不少,缺点自然是该程序调试起来会很难。

总结

本文描述了 ELF 文件中符号相关的细节,通过本文介绍,我们可以了符号在 ELF 文件中的存储格式,符号的绑定类型和可见性等属性。

参考资料:

评论 (评论内容仅博主可见,不会公开显示)