Linux ELF 详解3 -- Symbol Table & Symbol【转】

转自:https://blog.csdn.net/helowken2/article/details/113782851

ELF Symbol Table
Symbol Table 包含了一组 Symbol。这些 Symbol 在程序中,要么表示定义,要么表示引用,它们的作用是在编译和链接的过程中,进行定位或者重定位(后面会讲到)。

先查看它在 Section Header 列表中的信息

$ readelf -SW program.o
...
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[12] .symtab SYMTAB 0000000000000000 000130 000180 18 13 10 8
...

由上可知 Symbol Table Section:

名字叫 “.symtab”
类型是 SYMTAB
offset = 0x130 = (1 * 16 + 3) * 16 = 304 bytes
size = 0x180 = (1 * 16 + 8) * 16 = 386 bytes
每个 Symbol 的 size = 0x18 = 1 * 16 + 8 = 24
Symbol 数为: 386 / 24 = 16
ELF Symbol
看一下 Symbol 的定义

typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;

typedef struct {
Elf64_Word st_name; // 4 B (B for bytes)
unsigned char st_info; // 1 B
unsigned char st_other; // 1 B
Elf64_Half st_shndx; // 2 B
Elf64_Addr st_value; // 8 B
Elf64_Xword st_size; // 8 B
} Elf64_Sym; // total size = 24 B

我们只关注 Elf64_Sym (64位系统的定义)。
可以看到 Elf64_Sym 的大小为24字节。

用 readelf 查看一下 Symbol Table

# 注意这里是小写的 s,代表 Symbols,大写的 S,代表 Sections/Section Headers
$ readelf -sW program.o

Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS program.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000000 1 OBJECT LOCAL DEFAULT 3 d
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 7
10: 0000000000000008 10 OBJECT GLOBAL DEFAULT COM c
11: 0000000000000008 8 OBJECT GLOBAL DEFAULT 3 f
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND function
13: 0000000000000000 81 FUNC GLOBAL DEFAULT 1 main
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND a
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf

这里包含了 program.o 中出现的所有 Symbol。其中 Num=0(index=0) 的 Symbol 用作 undefined Symbol,它的 index 有个特别的名字,叫做 STN_UNDEF。

接下来,我们将选取 Num=5,也就是 Name=d 的 Symbol,从字节级别解读 Symbol 的定义。

上面已经知道了 Symbol Table 在对象文件中的 offset=304,每个 Symbol 的 size=24,那么 Num=5 的 Symbol(Num 从0开始计算,Num=5表示前面有5个 Symbol),它的 offset = 304 + 24 * 5 = 424。

$ hexdump -C -s424 -n24 program.o
000001a8 0b 00 00 00 01 00 03 00 00 00 00 00 00 00 00 00 |................|
000001b8 01 00 00 00 00 00 00 00 |........|
000001c0

st_name
Symbol 名字的索引,类似于 Section Header 的 sh_name,只不过这个索引使用的 String table 跟 Section Header sh_name 使用的 String table 不是同一个。后面会讲到。

数据类型:Elf64_Word (4 bytes)

$ hexdump -C -s424 -n4 program.o
000001a8 0b 00 00 00 |....|
000001ac

值为:“0b 00 00 00” + little endian = 0xb = 11

st_info
这是个组合字段。它的高4位表示 Symbol Binding,低4位表示 Symbol Type。

数据类型:unsigned char (1 byte)

$ hexdump -C -s428 -n1 program.o
000001ac 01 |.|
000001ad

值为:1,二进制表示为 “0000 0001”
Symbol Binding: 1 >> 4 = 0
Symbol Type: 1 & 0xf = 1

Symbol Binding 值的含义:(只需要关注红框部分)

STB_LOCAL: 对象文件外部不可见。
STB_GLOBAL:所有合并到一起的对象文件都可见。对于同一个 Global Symbol,它的定义可以满足其他地方对它的引用。
STB_WEAK:类似于 STB_GLOBAL,但匹配时优先级较低。
(后面会详细讲到)

Symbol Type 值的含义:(只需要关注红框部分)

STT_NOTYPE:类型未指定,也可以认为这个类型的 Symbol,在当前的对象文件中没找到其定义,需要外部其他对象文件来提供它的定义。
STT_OBJECT:关联到一个数据对象,比如数组,变量等。
STT_FUNC:关联到一个函数或者其他可执行的代码。
STT_SECTION:关联到可以重定位的 Section。
STT_FILE:给出了这个对象文件的源文件名,譬如 program.o 的源文件就是 program.c。它的 Section index 为 SHN_ABS。(后面会讲到)
STT_COMMON:标识了未初始化的公共块。后面会详细讲到。
STT_TLS:指定了线程本地存储的实体。(本章不讨论多线程,所以先跳过)

st_other
指定了 Symbol 的可见性。当前只有最低2位被使用。

数据类型:unsigned char (1 byte)

$ hexdump -C -s429 -n1 program.o
000001ad 00 |.|
000001ae

值为:0
可见性:0 & 0x3 = 0

通常情况下,可见性都为 STV_DEFAULT,也就是使用默认规则(后面会讲到)。
其他可见性先跳过。

st_shndx
不同的上下文有不同的含义:

当 st_shndx = Section Header index: st_value(Symbol 的值)指向某个 Section 中的某个位置。
当 st_shndx = SHN_ABS:表示该 Symbol 不应该被重定位。比如类型为 STT_FILE 的 Symbol。
当 st_shndx = SHN_COMMON:该 Symbol 标记的是一个还没分配的公共块,st_value 表示对齐约束,即(连接编辑器为这个 Symbol 分配的地址 % st_value = 0),而 st_size(Symbol 的大小)则表示该 Symbol 至少需要多少字节。
当 st_shndx = SHN_UNDEF:表示当前对象文件引用了该 Symbol,但是它的定义存在于其他对象文件中。比如对象文件 A 引用了 Symbol b(没有 b 的定义),对象文件 B 有 b 的定义,那么链接编辑器将 A 和 B 进行合并时,A 的 Symbol b 就会链接到 B 中 b 的定义。
当 st_shndx = SHN_XINDEX:Section Header index 过大时需要做特殊解析,当前不考虑这种情况。
数据类型:Elf64_Half (2 bytes)

$ hexdump -C -s430 -n2 program.o
000001ae 03 00 |..|
000001b0

值为:“03 00” + little endian = 0x3 = 3,即关联到的 Section Header index 为3。

st_value
根据不同的上下文,有不同的定义

对于 Relocatable file,如果 st_shndx 的值为 SHN_COMMON,那么 st_value 表示对齐约束。(上面 st_shndx = SHN_COMMON 已经提及)
对于 Relocatable file,如果 st_shndx 的值为关联的 Section Header index,那么 st_value 表示从该 Section 起始位置开始的 offset。(上面 st_shndx = Section Header index 已经提及)
对于 Executable file 和 Shared object file,st_value 则是已经计算好的虚存地址,这是为了方便 dynamic linker(动态链接器)。
数据类型:Elf64_Addr (8 bytes)

$ hexdump -C -s432 -n8 program.o
000001b0 00 00 00 00 00 00 00 00 |........|
000001b8

值为:0

st_size
表示 Symbol 的大小,例如数据对象占据多少字节。如果 Symbol 没有大小或者大小未知,则值为0。

数据类型:Elf64_Xword (8 bytes)

$ hexdump -C -s440 -n8 program.o
000001b8 01 00 00 00 00 00 00 00 |........|
000001c0

值为:“01 00 00 00 00 00 00 00” + little endian = 0x1,即占据1个字节。

可以把各字段的信息对照 Symbol Table 中 Num=5 的输出,从而加深理解。

“.symtab” 的 sh_link & sh_info
首先回顾一下 “.symtab” 的 Section Header 输出:

$ readelf -SW program.o
...
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[12] .symtab SYMTAB 0000000000000000 000130 000180 18 13 10 8
[13] .strtab STRTAB 0000000000000000 0002b0 000024 00 0 0 1
...

由上可知,".symtab" 的 sh_link = 13,sh_info = 10。
前面 Section Header 的章节已经提到过,sh_link 和 sh_info 在不同的上下文有不同的含义。

对于 “.symtab”,
sh_link:表示关联到的 String table 的 Section Header index。在这里,这个 String table 就是 “.strtab”。Symbol 的 st_name 就是对应到 “.strtab” 的 index。
sh_info:1 + 最后一个 local Symbol (Binding = STB_LOCAL) 在 Symbol Table 中的 index,也就是说从这个 index 开始的 Symbol 不再是 local。

查看一下 “.strtab” 的内容

$ hexdump -C -s0x2b0 -n36 program.o
000002b0 00 70 72 6f 67 72 61 6d 2e 63 00 64 00 66 75 6e |.program.c.d.fun|
000002c0 63 74 69 6f 6e 00 6d 61 69 6e 00 61 00 70 72 69 |ction.main.a.pri|
000002d0 6e 74 66 00 |ntf.|
000002d4
1
2
3
4
5
从上面查看过的字节信息可知,Symbol d 的 st_name = 11,在这里对应的就是 “d”。

以下是打印所有 Symbol 名字的程序,类似于前面打印所有 Section Header 名字的程序(为了方便理解,其实就是换了变量名和对应的数值)。

// print_symbol_names.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
off_t symbolTableOffset = 0x130;
size_t symbolTableSize = 0x180;
size_t symbolSize = 0x18;
off_t stringTableOffset = 0x2b0;
size_t stringTableSize = 0x24;
size_t nameSize = 4;
int symbolNum = symbolTableSize / symbolSize;
char content[stringTableSize];
union {
char b[4];
int off;
} symbolName;

int fd = open("program.o", O_RDONLY);
if (fd == -1)
exit(EXIT_FAILURE);

if (lseek(fd, stringTableOffset, SEEK_SET) == -1)
exit(EXIT_FAILURE);

if (read(fd, content, stringTableSize) != stringTableSize)
exit(EXIT_FAILURE);


int i;
int currSymbolOffset = symbolTableOffset;
char *start;
for (i = 0; i < symbolNum; ++i) {
if (lseek(fd, currSymbolOffset, SEEK_SET) == -1)
exit(EXIT_FAILURE);
if (read(fd, symbolName.b, 4) != 4)
exit(EXIT_FAILURE);

start = content + symbolName.off;
printf("[%2d]: off: 0x%02x, name: %s\n", i, symbolName.off, start);

currSymbolOffset += symbolSize;
}

if (close(fd) == -1)
exit(EXIT_FAILURE);

return 0;
}


输出的结果和 “readelf -sW program.o” 是一致的。

$ gcc -o print_symbol_names print_symbol_names.c
$ ./print_symbol_names
[ 0]: off: 0x00, name:
[ 1]: off: 0x01, name: program.c
[ 2]: off: 0x00, name:
[ 3]: off: 0x00, name:
[ 4]: off: 0x00, name:
[ 5]: off: 0x0b, name: d
[ 6]: off: 0x00, name:
[ 7]: off: 0x00, name:
[ 8]: off: 0x00, name:
[ 9]: off: 0x00, name:
[10]: off: 0x09, name: c
[11]: off: 0x22, name: f
[12]: off: 0x0d, name: function
[13]: off: 0x16, name: main
[14]: off: 0x1b, name: a
[15]: off: 0x1d, name: printf

再来看 sh_info 的含义。

$ readelf -sW program.o

Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS program.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000000 1 OBJECT LOCAL DEFAULT 3 d
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 7
# --------------------------------------------------------------------
10: 0000000000000008 10 OBJECT GLOBAL DEFAULT COM c
11: 0000000000000008 8 OBJECT GLOBAL DEFAULT 3 f
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND function
13: 0000000000000000 81 FUNC GLOBAL DEFAULT 1 main
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND a
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf

由上可知,Num = 10 的 Symbol 是 c,在它之前的 Symbol,Binding 都是 LOCAL,从它开始往后的都是 GLOBAL。

接下来将查看 program.c 中出现的各个 Symbol 在对象文件中是如何定义的。

Symbol 示例
Num=5, Name=d
// program.c
...
static char d = 'd';
...

$ readelf -sW program.o
...
Num: Value Size Type Bind Vis Ndx Name
...
5: 0000000000000000 1 OBJECT LOCAL DEFAULT 3 d
...

# 查看关联到的 Section Header (Ndx=3)
$ readelf -SW program.o
...
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[ 3] .data PROGBITS 0000000000000000 000098 000010 00 WA 0 0 8
...

# 打印 ".data" 的内容
$ hexdump -C -s0x98 -n16 program.o
00000098 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |d...............|
000000a8

对于 Symbol d:

在程序中声明为 static,所以它的 Symbol Binding 是 LOCAL
用于储存数据,所以它的 Symbol Type 是 STT_OBJECT
拥有初始值 ‘d’,所以它被分配在 “.data” Section 中
从上面 st_value 的定义可知,因为 d 关联到 “.data” Section,所以它的 st_value 表示它在 “.data” 中的 offset,在这里 offset = 0
数据类型为 char,因此占据1个字节,所以它的 Symbol st_value = 1;从 “.data” 中 offset = 0 的地方提取到的1字节为 0x64,其 ascii 就是 ‘d’。
Num=10,Name=c
// program.c
...
char c[10];
...

$ readelf -sW program.o
...
Num: Value Size Type Bind Vis Ndx Name
...
10: 0000000000000008 10 OBJECT GLOBAL DEFAULT COM c
...

对于 Symbol c:

在程序中是个全局变量(没有声明 static),所以它的 Symbol Binding 是 GLOBAL
用于储存数据,所以它的 Symbol Type 是 STT_OBJECT
没有初始化,因此它的 Symbol st_shndx 为 SHN_COMMON(注意,这里并没有把它分配到 “.bss”,后面会详细讲到)
从上面 st_value 的定义可知,当 st_shndx = SHN_COMMON 时,st_value 表示对齐约束;这里 st_value = 8,表示它被分配到的地址会是8的整数倍
它是个长度为10的 char 数组,所以它的 Symbol st_size = 10
Num=11,Name=f
// library.h
int function(int);

// program.c
#include <stdio.h>
#include "library.h"
...
char* f = (char*) function;
...

$ readelf -sW program.o
...
Num: Value Size Type Bind Vis Ndx Name
...
11: 0000000000000008 8 OBJECT GLOBAL DEFAULT 3 f
...

# 打印 ".data" 的内容
$ hexdump -C -s0x98 -n16 program.o
00000098 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |d...............|
000000a8

对于 Symbol f:

在程序中是个全局变量(没有声明 static),所以它的 Symbol Binding 是 GLOBAL
用于储存数据(存储的是地址),所以它的 Symbol Type 是 STT_OBJECT
初始值为函数 function 的地址,所以它被分配在 “.data” Section 中
从上面 st_value 的定义可知,因为 f 关联到 “.data” Section,所以它的 st_value 表示它在 “.data” 中的 offset,在这里 offset = 8
数据类型为 char*,因此占据8个字节,所以它的 Symbol st_value = 8;从 “.data” 中 offset = 8 的地方提取到的8字节为 “00 00 00 00 00 00 00 00”,这是因为当前无法确定 function 的地址
Undefined Symbol
Num=12, Name=function
Num=14, Name=a (声明为 extern)
Num=15, Name=printf
它们都是外部引用(对外部 Symbol 的引用)

// program.c
...
extern int a;
...
char* f = (char*) function;

int main() {
...
printf("a: %d\n", a);
printf("result: %d\n", d);
}

$ readelf -sW program.o
...
Num: Value Size Type Bind Vis Ndx Name
...
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND function
...
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND a
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
...

对于这几个 Symbol:

在程序中是外部引用,所以它们的 Symbol Binding 是 GLOBAL
没有找到它们的定义,所以 Section Header index 为 SHN_UNDEF, Symbol Type 为 STT_NOTYPE,st_size = 0, st_value = 0
Ndx = SHN_UNDEF 的 Symbol 都需要在链接期间进行 Symbol 解析。

Num=13, name=main
...
int main() {
int d = function(100) + a;
printf("a: %d\n", a);
printf("result: %d\n", d);
}

$ readelf -sW program.o
...
Num: Value Size Type Bind Vis Ndx Name
...
13: 0000000000000000 81 FUNC GLOBAL DEFAULT 1 main
...

# 查看关联到的 Section Header(Ndx=1)
$ readelf -SW program.o
...
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
...
[ 1] .text PROGBITS 0000000000000000 000040 000051 00 AX 0 0 1
...

# 查看 ".text" 的内容
$ hexdump -C -s0x40 -n81 program.o
00000040 55 48 89 e5 48 83 ec 10 bf 64 00 00 00 e8 00 00 |UH..H....d......|
00000050 00 00 89 c2 8b 05 00 00 00 00 01 d0 89 45 fc 8b |.............E..|
00000060 05 00 00 00 00 89 c6 bf 00 00 00 00 b8 00 00 00 |................|
00000070 00 e8 00 00 00 00 8b 45 fc 89 c6 bf 00 00 00 00 |.......E........|
00000080 b8 00 00 00 00 e8 00 00 00 00 b8 00 00 00 00 c9 |................|
00000090 c3 |.|
00000091

# 查看 ".text" 的反汇编内容
$ objdump -d program.o
...
Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
...
4f: c9 leaveq
50: c3 retq

对于 Symbol main:

在程序中是个全局函数(没有声明 static),所以它的 Symbol Binding 是 GLOBAL,Symbol Type 为 STT_FUNC
包含了可执行的代码,所以被分配在 “.text” Section 中
从上面 st_value 的定义可知,因为 main 关联到 “.text” Section,所以它的 st_value 表示它在 “.text” 中的 offset,在这里 offset = 0;而 st_size 表示代码占据多少字节,在这里 st_size = 81(注意,这里的81是10进制,而 “.text” 的 size 是16进制: 0x51 => 5 * 16 + 1 = 81)
————————————————
版权声明:本文为CSDN博主「懒惰的劳模」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/helowken2/article/details/113782851

posted @ 2022-01-19 00:00  Sky&Zhang  阅读(757)  评论(0编辑  收藏  举报