认识 ELF 文件格式(二)

认识 ELF 文件格式(二)

接下来,本篇文章我们来说一下 ELF 文件的节头、以及对应的各个节;他们所存储的数据内容。

一、节头

ELF 文件中的代码和数据在逻辑上被分为连续的非重叠块,称为节(Section)。节没有固定的结构体,节的结构取决于节的内容。每个节都有一个节头,这个节头中存储着这个节的属性、信息。节头的格式都是一致的。

需要注意的是,节只为链接器提供视图,而有些 ELF 文件不需要链接,也就不需要节,同时不需要节头。

如下,我们看看节头的格式,被定义在 /usr/include/elf.h 中,我们只看 64 位的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;

1. sh_name

还记得上节我们提到过 ELF 文件中有一个特殊字符串表,他是一个节。他里面存储的是所有节的名字。

好,那么当前 sh_name 这个字段表示的是这个节头所代表的节,他的名字在“特殊字符串表”中的位置索引。如果说这个字段为零,则说明这个节头所代表的节没有名字。

问题来了,如果这个节名字存储在特殊字符串表的第一个位置呢?特殊字符串表第一字节一般为 NULL,他是从第二个字节开始存储的。对于程序员的下标为 1。

2. sh_type

这个字段表示节的类型,如下,我们列举几个重要的类型进行介绍一下。

  • SHT_NULL:无效段
  • SHT_PROGBITS:表示这个节包含了程序数据,如机器指令或常量,这些节没有特定的结构供链接器解析
  • SHM_SYMTAB:表示这个节为符号表,并且是静态符号表
  • SHT_STRTAB:表示这个节为符号表,并且是字符串表
  • SHT_DYNSYM:表示这个节为符号表,并且是动态链接器使用的符号表
  • SHT_REL 和 SHT_RELA:表示这个节包含重定位项,对于链接器比较重要,链接器可以解析该重定位项,然后在其他节中进行重定位。每个重定位项都会告诉链接器 ELF 文件中需要重定位的位置,以及重定位需要解析的符号。需要注意的是,SHT_RELSHT_RELA 类型的节用于静态链接。
  • SHT_DYNAMIC:这种类型的节包含动态链接所需要的信息,和 SHT_REL、SHT_RELA 相对应
  • SHT_HASH:符号表的哈希表
  • SHT_NOBITS:表示该节在文件中没有内容,比如 .bss

3. sh_flags

这个字段表示节的标志,描述了节的一些信息。我们列举几个介绍一下

  • SHF_WRITE:表示该节在运行时可写
  • SHF_ALLOC:指示在执行二进制文件时将节的内容加载到虚拟内存
  • SHF_EXECINSTR:表示该节包含可执行指令

4. sh_addr 和 sh_offset 和 sh_size

sh_addr 这个字段表示节的虚拟地址;sh_offset 这个字段表示节的偏移(相对比 ELF 文件起始);sh_size 这个字段表示节的大小,单位是字节。

链接器有时候需要知道特定的代码和数据在运行时最终会在那个地址进行重定位,sh_addr 这个字段就要提供信息了。当设置此字段的值为零时,这个节不会被加载到虚拟内存中

这个字段提供了一个指针或者索引,他的含义取决于当前节的类型,如下例子:

  • 对于符号表(.symtab)和字符串表(.strtab)这类包含符号信息的节,sh_link 字段表示的是字符串表的节头索引,即该符号表所使用的字符串表在节头中的位置。
  • 对于重定位表(.rel.rela)这类包含重定位信息的节,sh_link 字段表示的是重定位表所依赖的符号表的节头索引

这个字段暂时不理解没关系,后面分享链接的时候会再次说明

6. sh_info

这个字段存放关于节的额外信息,这些额外信息依赖于节类型。例如对于 SHT_RELSHT_RELA 类型的重定位节,sh_info 存放的是应用重定位节的节头索引。

7. sh_addralign

某些节需要以特定方式在内存中对齐,来提供内存访问的效率。比如这个节可能需要在偏移量为 8 字节或者 16 字节倍数的地址进行加载,这些对齐的要求在这个字段指定。

如果这个字段设置为 16,意味着该节的基址必须为 16 的倍数(由链接器指定)。保留值 0 和 1 均指示无特殊对齐需要。

8. sh_entsize

某些节包含了固定大小的条目,或者说项。比如符号表中,他包含的每个符号所占的大小都是一样的,对于这种节,sh_entsize 表示每个条目的大小。如果为 0,则表示该节不包含固定大小的条目。

好,到这里我们介绍完了节头的结构,接下来介绍 ELF 文件中的各个节。上面的一些概念暂时不理解没关系,我们来看各个节的时候,就会豁然开朗。并且下面介绍节的时候,我们会发现很多有趣的知识技术。冲吧

二、节

我们先来看看一个 ELF 文件可能都有哪些节,使用 readelf 工具来查看。这个 a.out 还是我们之前的 “hello world” 程序编译出来的二进制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# readelf --sections --wide a.out
There are 32 section headers, starting at offset 0xcdd50:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400190 000190 000020 00 A 0 0 4
readelf: Warning: [ 2]: Link field (0) should index a symtab section.
[ 2] .rela.plt RELA 00000000004001b0 0001b0 000228 18 AI 0 19 8
[ 3] .init PROGBITS 00000000004003d8 0003d8 000017 00 AX 0 0 4
[ 4] .plt PROGBITS 00000000004003f0 0003f0 0000b8 00 AX 0 0 8
[ 5] .text PROGBITS 00000000004004b0 0004b0 08f640 00 AX 0 0 16
[ 6] __libc_freeres_fn PROGBITS 000000000048faf0 08faf0 001523 00 AX 0 0 16
[ 7] __libc_thread_freeres_fn PROGBITS 0000000000491020 091020 0010eb 00 AX 0 0 16
[ 8] .fini PROGBITS 000000000049210c 09210c 000009 00 AX 0 0 4
[ 9] .rodata PROGBITS 0000000000492120 092120 01926c 00 A 0 0 32
[10] .stapsdt.base PROGBITS 00000000004ab38c 0ab38c 000001 00 A 0 0 1
[11] .eh_frame PROGBITS 00000000004ab390 0ab390 00a5e8 00 A 0 0 8
[12] .gcc_except_table PROGBITS 00000000004b5978 0b5978 00009e 00 A 0 0 1
[13] .tdata PROGBITS 00000000006b6120 0b6120 000020 00 WAT 0 0 8
[14] .tbss NOBITS 00000000006b6140 0b6140 000040 00 WAT 0 0 8
[15] .init_array INIT_ARRAY 00000000006b6140 0b6140 000010 08 WA 0 0 8
[16] .fini_array FINI_ARRAY 00000000006b6150 0b6150 000010 08 WA 0 0 8
[17] .data.rel.ro PROGBITS 00000000006b6160 0b6160 002d94 00 WA 0 0 32
[18] .got PROGBITS 00000000006b8ef8 0b8ef8 0000f8 00 WA 0 0 8
[19] .got.plt PROGBITS 00000000006b9000 0b9000 0000d0 08 WA 0 0 8
[20] .data PROGBITS 00000000006b90e0 0b90e0 001af0 00 WA 0 0 32
[21] __libc_subfreeres PROGBITS 00000000006babd0 0babd0 000048 00 WA 0 0 8
[22] __libc_IO_vtables PROGBITS 00000000006bac20 0bac20 0006a8 00 WA 0 0 32
[23] __libc_atexit PROGBITS 00000000006bb2c8 0bb2c8 000008 00 WA 0 0 8
[24] __libc_thread_subfreeres PROGBITS 00000000006bb2d0 0bb2d0 000008 00 WA 0 0 8
[25] .bss NOBITS 00000000006bb2e0 0bb2d8 0016f8 00 WA 0 0 32
[26] __libc_freeres_ptrs NOBITS 00000000006bc9d8 0bb2d8 000028 00 WA 0 0 8
[27] .comment PROGBITS 0000000000000000 0bb2d8 000029 01 MS 0 0 1
[28] .note.stapsdt NOTE 0000000000000000 0bb304 001638 00 0 0 4
[29] .symtab SYMTAB 0000000000000000 0bc940 00a980 18 30 678 8
[30] .strtab STRTAB 0000000000000000 0c72c0 006927 00 0 0 1
[31] .shstrtab STRTAB 0000000000000000 0cdbe7 000163 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)

如上的展示,第一列是节头表里的索引,第二列是节的名字,第三列是节的类型,第四列是节的虚拟地址,第五列是节的文件偏移;接下来是节的大小。ES 列表示 sh_entsize,代表节中每个条目的大小(如果存在);Flg 列表示 sh_flags ,代表节的标志,描述节的一些信息;Lk 列表示 sh_link,代表链接节的索引(如果存在)。Inf 列表示 sh_info,代表节的额外信息。最后 Al 列表示节的对齐要求。

注意,我们会发现,节头表中第一项都是空值,该项的类型也为 NULL。这是 ELF 文件格式要求,规定每个 ELF 文件的节头表中的第一项都是 NULL 的,也就是说他是一个没有实际内容的节头。

好,我们接下来看各种不同的节

1. .init 节 和 .fini

.init 节包含可执行代码,用于执行初始化工作,并且在二进制文件执行其他代码之前运行。可执行代码有 SHF_EXECINSTR 标志,使用 readelf 查看 Flg 列,可以发现该值为 X。将控制转移到二进制文件的 main 入口点之前,系统会先执行 .init 节的代码。

.fini 节则是在主程序运行完之后执行,其他性质和 .init 一致。

2. .text

.text 节包含程序的主要代码,这个节的类型是 SHT_PROGBITS,这个节的标志为 AX,表示 alloc 和 execute,但是不可写入。

一般来说,可执行的节是不可写的;反之,可以写的节一般是不可执行的。因此既可写又可执行的节会让攻击者利用漏洞直接覆盖代码来修改程序,使得攻击变得容易。

我们来拿一个二进制来详细说明下。.text 节除了从源代码编译的某些特定应用程序之外,还有一些编译器帮我们添加的函数,GCC 编译器会帮我们的代码添加一些初始化和任务终止的标准函数。比如:_startregister_tm_clonesframe_dummy 等等。如下:

1
# objdump -M intel -d a.out

其中 -d 选项是展示可执行文件所有节的汇编内容。-M intel 表示以 nasm 格式的汇编展示。内容很多,我展示出 _start 汇编函数的代码和 main 函数的代码。还是那个 “hello world” 程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0000000000400a30 <_start>:
400a30: 31 ed xor ebp,ebp
400a32: 49 89 d1 mov r9,rdx
400a35: 5e pop rsi
400a36: 48 89 e2 mov rdx,rsp
400a39: 48 83 e4 f0 and rsp,0xfffffffffffffff0
400a3d: 50 push rax
400a3e: 54 push rsp
400a3f: 49 c7 c0 f0 18 40 00 mov r8,0x4018f0
400a46: 48 c7 c1 50 18 40 00 mov rcx,0x401850
400a4d: 48 c7 c7 4d 0b 40 00 mov rdi,0x400b4d
400a54: 67 e8 86 03 00 00 addr32 call 400de0 <__libc_start_main>
400a5a: f4 hlt
400a5b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]

0000000000400b4d <main>:
400b4d: 55 push rbp
400b4e: 48 89 e5 mov rbp,rsp
400b51: 48 8d 3d cc 15 09 00 lea rdi,[rip+0x915cc] # 492124 <_IO_stdin_used+0x4>
400b58: e8 a3 f5 00 00 call 410100 <_IO_puts>
400b5d: b8 00 00 00 00 mov eax,0x0
400b62: 5d pop rbp
400b63: c3 ret
400b64: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
400b6b: 00 00 00
400b6e: 66 90 xchg ax,ax

我们的用户的代码都需要从 main 函数开始,但是我们查看这个 ELF 二进制文件的文件头,我们之前说过字段 e_entry 表示二进制文件的入口点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - GNU
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400a30
Start of program headers: 64 (bytes into file)
Start of section headers: 843088 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 6
Size of section headers: 64 (bytes)
Number of section headers: 32
Section header string table index: 31

我们发现 e_entry 字段的值为 0x400a30。哦吼,_start 函数的地址为 0000000000400a30。而 main 函数的地址为 0000000000400b4d。这也就说明了站在机器层面,_start 函数才是一个二进制文件的入口点,而不是程序员角度的 main 函数。

我们再观察 _start 函数中有一行: 400a54: 67 e8 86 03 00 00 addr32 call 400de0 <__libc_start_main>。这一行其实最终会调用到 main 函数,并开始执行用户定义的代码。

3. .rodata节、.data节、.bss

我们已经知道,代码节不可写,所以变量会被保存在一个或多个可写的节中。

.rodata 节代表“只读数据”,用于存储常量,因此 .rodata 节是不可写的。此节的类型为 SHT_PROGBITS。

.data 节中存储的是初始化变量的默认值,因为变量的值可能会在运行中修改,所以该节是可写的。此节的类型为 SHT_PROGBITS。

.bss 节是未初始化的变量保留空间。此节的类型为 SHT_NOBITS。他只当二进制文件建立执行环境时为未初始化的变量分配适当大小的内存块。一般来说,.bss 节中的变量被初始化为零,并且该节被标记为可写。

4. .init_array

.init_array 节包含一个指向构造函数的指针数组,在二进制文件被初始化之后、main 函数被调用之前,这些构造函数会被依次调用。之前说的 .init 节包含可执行代码, 没有写权限。但是 .init_array 却是一个数据节,有可写权限,其中包含一定数量的函数指针,包含指向自定义构造函数的指针。

在 GCC 中,通过 __attribute__((constructor)) 修饰的函数,会将其标记为构造函数,并且他的函数指针会放在 .init_array 节中。我们尝试写一个这样的函数看看。如下:

1
2
3
__attribute__((constructor)) void func_01() {
printf("I am constructor func\n");
}

我们经过编译成二进制后,通过 objdump 命令来找到这个函数。并且找到这个函数的虚拟地址。objdump 输出的内容较多,可以使用重定向到文件中再来找。如下,我们找到了 func_01 的这个函数的地址为:0x065a

1
2
3
4
5
6
7
8
9
10
# objdump -d --section .text a.out

000000000000065a <_Z7func_01v>:
65a: 55 push %rbp
65b: 48 89 e5 mov %rsp,%rbp
65e: 48 8d 3d af 00 00 00 lea 0xaf(%rip),%rdi # 714 <_IO_stdin_used+0x4>
665: e8 c6 fe ff ff callq 530 <puts@plt>
66a: 90 nop
66b: 5d pop %rbp
66c: c3 retq

我们再来通过 objdump 查看 .init_array 节的内容,我们发现有两个函数指针(64 位操作系统,指针的长度为 8 个字节)。并且注意是小端字节序。

1
2
3
4
5
6
7
8
 # objdump -d --section .init_array a.out

a.out: file format elf64-x86-64

Disassembly of section .init_array:

0000000000200db0 <__frame_dummy_init_array_entry>:
200db0: 50 06 00 00 00 00 00 00 5a 06 00 00 00 00 00 00 P.......Z.......

因此我们得到了两个函数指针:

  • 一个是:0x0650
  • 另一个是 0x065a。这个地址就是我们刚才看到的 func_01 函数的地址。

好,我们在来看看 0x0650 对应的是那个函数

1
2
# objdump -d --section .text a.out | grep 0650
0000000000000650 <frame_dummy>:

原来是 frame_dummy,他是一个默认的初始化函数,我们在这里不深究了

5. .fini_array

.fini_array 节包含了指向析构函数的指针。他的性质与 .init_array 节相似。在这里不再累赘。

同样的,在 GCC 中,可以通过 __attribute__((destructor)) 修饰的函数,会将其标记为析构函数,并将这个函数的函数指针放入 .fini_array 节中,我们继续做一个实验。

1
2
3
__attribute__((destructor)) void func_02() {
printf("I am destructor func\n");
}

我们使用 objdump 来查看生成的汇编函数,以及汇编函数的地址。

1
2
3
4
5
6
7
8
9
10
# # objdump -d --section .text a.out 

000000000000067d <_Z7func_02v>:
67d: 55 push %rbp
67e: 48 89 e5 mov %rsp,%rbp
681: 48 8d 3d c2 00 00 00 lea 0xc2(%rip),%rdi # 74a <_IO_stdin_used+0x1a>
688: e8 b3 fe ff ff callq 540 <puts@plt>
68d: 90 nop
68e: 5d pop %rbp
68f: c3 retq

可以看到 func_02 汇编函数的虚拟地址为:0x067d。我们在看 .fini_array 节的内容

1
2
3
4
5
6
7
8
# objdump -d --section .fini_array a.out

a.out: file format elf64-x86-64

Disassembly of section .fini_array:

0000000000200db8 <__do_global_dtors_aux_fini_array_entry>:
200db8: 20 06 00 00 00 00 00 00 7d 06 00 00 00 00 00 00 .......}.......

我们看到 .fini_array 节中有两个函数指针,由于是小端字节序。所以是:

  • 一个是:0x0620
  • 另一个是:0x067d。正是我们 func_02 汇编函数的虚拟地址。

注意,.init_array 节和 .fini_array 节都是可写的,也就是说我们可以向这个节中添加初始化函数或结束函数的函数指针,来修改二进制的行为。

6. .shstrtab

此节是一个以 NULL 结尾的字符串数组,其中包含二进制文件中所有节的名称。

我们使用 readelf 工具来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# readelf -p .shstrtab a.out

String dump of section '.shstrtab':
[ 1] .symtab
[ 9] .strtab
[ 11] .shstrtab
[ 1b] .interp
[ 23] .note.ABI-tag
[ 31] .note.gnu.build-id
[ 44] .gnu.hash
[ 4e] .dynsym
[ 56] .dynstr
[ 5e] .gnu.version
[ 6b] .gnu.version_r
[ 7a] .rela.dyn
[ 84] .rela.plt
[ 8e] .init
[ 94] .plt.got
[ 9d] .text
[ a3] .fini
[ a9] .rodata
[ b1] .eh_frame_hdr
[ bf] .eh_frame
[ c9] .init_array
[ d5] .fini_array
[ e1] .dynamic
[ ea] .data
[ f0] .bss
[ f5] .comment
[ fe] .debug_aranges
[ 10d] .debug_info
[ 119] .debug_abbrev
[ 127] .debug_line
[ 133] .debug_str

可以看到 .shstrtab 节存储的是我们 ELF 文件中所有节的名称。

7. .strtab

来看 .strtab节,我们一般称其为字符串表,用来保存普通的字符串,比如符号的名字。

符号简单来说,就是函数或者变量。每个函数或者变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。

我们使用 readelf 工具来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# readelf -p .strtab a.out

String dump of section '.strtab':
[ 1] crtstuff.c
[ c] deregister_tm_clones
[ 21] __do_global_dtors_aux
[ 37] completed.7698
[ 46] __do_global_dtors_aux_fini_array_entry
[ 6d] frame_dummy
[ 79] __frame_dummy_init_array_entry
[ 98] main.c
[ 9f] __FRAME_END__
[ ad] __init_array_end
[ be] _DYNAMIC
[ c7] __init_array_start
[ da] __GNU_EH_FRAME_HDR
[ ed] _GLOBAL_OFFSET_TABLE_
[ 103] __libc_csu_fini
[ 113] _ITM_deregisterTMCloneTable
[ 12f] puts@@GLIBC_2.2.5
[ 141] _edata
[ 148] _Z7func_02v
[ 154] __libc_start_main@@GLIBC_2.2.5
[ 173] __data_start
[ 180] __gmon_start__
[ 18f] __dso_handle
[ 19c] _IO_stdin_used
[ 1ab] __libc_csu_init
[ 1bb] __bss_start
[ 1c7] main
[ 1cc] _Z7func_01v
[ 1d8] __TMC_END__
[ 1e4] _ITM_registerTMCloneTable
[ 1fe] __cxa_finalize@@GLIBC_2.2.5

我们可以看到,不仅仅有我们定义的函数名,比如 main、func_01、func_02 。而且一般情况下,符号名会经过一定的规则进行修饰的。比如:我们定义的函数名为:func_02,经过修饰后为:_Z7func_02v.strtab节还有三方库的函数,比如 puts 等。还有文件名。

8. .symtab

这个节是符号表,这个表是一个 Elf64_Sym 结构体数组,每个条目都将符号名与 ELF 文件中的代码和数据(如函数或变量)相关联。而包含符号名的实际字符串保存在 .strtab 节中。如下是 Elf64_Sym 结构

1
2
3
4
5
6
7
8
9
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:符号名索引,他表示这个符号名在 .strtab 节(字符串表)中的下标
  • st_info:符号类型和绑定信息,下面介绍
  • st_other:保留字段,未使用,默认为 0
  • st_shndx:符号所在节,节索引,下面详细介绍
  • st_value:符号所对应的值,这个值和符号有关,可能是一个绝对值,也可能是一个地址等,不同的符号,他所对应的值含义不同
  • st_size:符号大小,对于包含数据的符号,这个值是该数据类型的大小,比如一个 double 类型的符号,他占 8 个字节。

符号类型和绑定信息:该字段低 4 位表示符号的类型,高 4 位表示符号绑定信息。

如下表格是符号类型

宏定义 说明
STT_NOTYPE 0 未知类型符号
STT_OBJECT 1 该符号是一个数据对象,比如变量、数组等
STT_FUNC 2 该符号是个函数或者其他可执行代码
STT_SECTION 3 该符号表示一个段,这种符号必须是 STB_LOCAL 的
STT_FILE 4 该符号表示文件名,一般都是该目标文件所对应的源文件名,且一定是 STB_LOCAL 类型的

如下表格是符号绑定信息

宏定义 说明
STB_LOCAL 0 局部符号,对于目标文件的外部不可见
STB_GLOBAL 1 全局符号,外部可见
STB_WEAK 2 弱符号

符号所在节(st_shndx):如果符号定义在本目标文件中,那么这个字段表示符号所在的节在节头表的位置索引。但是如果符号不是定义在本目标文件中,或者对于有些特殊符号,st_shndx 的值有如下特殊值

宏定义 说明
SHN_ABS 0xfff1 表示该符号包含了一个绝对的值。比如表示文件名的符号就是属于这种类型
SHN_COMMON 0xfff2 表示该符号是一个“COMMON块”类型的符号,一般来说,未初始化的全局符号定义就是这种类型
SHN_UNDEF 0 表示该符号未定义,也就是说该符号在本目标文件被引用到,但定义在其他目标文件中

好的,我们接下来从实际例子出发来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# readelf -s a.out

Symbol table '.symtab' contains 70 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000238 0 SECTION LOCAL DEFAULT 1
2: 0000000000000254 0 SECTION LOCAL DEFAULT 2
3: 0000000000000274 0 SECTION LOCAL DEFAULT 3
4: 0000000000000298 0 SECTION LOCAL DEFAULT 4
5: 00000000000002b8 0 SECTION LOCAL DEFAULT 5
6: 0000000000000360 0 SECTION LOCAL DEFAULT 6
7: 00000000000003e2 0 SECTION LOCAL DEFAULT 7
8: 00000000000003f0 0 SECTION LOCAL DEFAULT 8
9: 0000000000000410 0 SECTION LOCAL DEFAULT 9
10: 0000000000000500 0 SECTION LOCAL DEFAULT 10
11: 0000000000000518 0 SECTION LOCAL DEFAULT 11
12: 0000000000000530 0 SECTION LOCAL DEFAULT 12
13: 0000000000000550 0 SECTION LOCAL DEFAULT 13
14: 0000000000000560 0 SECTION LOCAL DEFAULT 14
15: 0000000000000724 0 SECTION LOCAL DEFAULT 15
16: 0000000000000730 0 SECTION LOCAL DEFAULT 16
17: 000000000000076c 0 SECTION LOCAL DEFAULT 17
18: 00000000000007b8 0 SECTION LOCAL DEFAULT 18
19: 0000000000200da8 0 SECTION LOCAL DEFAULT 19
20: 0000000000200db8 0 SECTION LOCAL DEFAULT 20
21: 0000000000200dc8 0 SECTION LOCAL DEFAULT 21
22: 0000000000200fb8 0 SECTION LOCAL DEFAULT 22
23: 0000000000201000 0 SECTION LOCAL DEFAULT 23
24: 0000000000201010 0 SECTION LOCAL DEFAULT 24
25: 0000000000000000 0 SECTION LOCAL DEFAULT 25
26: 0000000000000000 0 SECTION LOCAL DEFAULT 26
27: 0000000000000000 0 SECTION LOCAL DEFAULT 27
28: 0000000000000000 0 SECTION LOCAL DEFAULT 28
29: 0000000000000000 0 SECTION LOCAL DEFAULT 29
30: 0000000000000000 0 SECTION LOCAL DEFAULT 30
31: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
32: 0000000000000590 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
33: 00000000000005d0 0 FUNC LOCAL DEFAULT 14 register_tm_clones
34: 0000000000000620 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
35: 0000000000201010 1 OBJECT LOCAL DEFAULT 24 completed.7698
36: 0000000000200db8 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fin
37: 0000000000000660 0 FUNC LOCAL DEFAULT 14 frame_dummy
38: 0000000000200da8 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_
39: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
40: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
41: 00000000000008fc 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
42: 0000000000000000 0 FILE LOCAL DEFAULT ABS
43: 0000000000200db8 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
44: 0000000000200dc8 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
45: 0000000000200da8 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
46: 000000000000076c 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
47: 0000000000200fb8 0 OBJECT LOCAL DEFAULT 22 _GLOBAL_OFFSET_TABLE_
48: 0000000000000720 2 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
49: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
50: 0000000000201000 0 NOTYPE WEAK DEFAULT 23 data_start
51: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
52: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 23 _edata
53: 0000000000000724 0 FUNC GLOBAL DEFAULT 15 _fini
54: 000000000000067d 19 FUNC GLOBAL DEFAULT 14 _Z7func_02v
55: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
56: 0000000000201000 0 NOTYPE GLOBAL DEFAULT 23 __data_start
57: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
58: 0000000000201008 0 OBJECT GLOBAL HIDDEN 23 __dso_handle
59: 0000000000000730 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
60: 00000000000006b0 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
61: 0000000000201018 0 NOTYPE GLOBAL DEFAULT 24 _end
62: 0000000000000560 43 FUNC GLOBAL DEFAULT 14 _start
63: 0000000000201010 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
64: 0000000000000690 23 FUNC GLOBAL DEFAULT 14 main
65: 000000000000066a 19 FUNC GLOBAL DEFAULT 14 _Z7func_01v
66: 0000000000201010 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
67: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
68: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
69: 0000000000000518 0 FUNC GLOBAL DEFAULT 11 _init

内容比较多,解释下每一列的含义

  • 第一列 Num 是 .symtab节(符号表)数组的下标;
  • 第二列 Value 是符号值,即 st_value
  • 第三列 Size 表示符号大小,即 st_size
  • 第四列 Type 表示符号类型,第五列 Bind 是绑定信息,第四列和第五列对应 st_info的低 4 位和高 4 位;
  • 第六列 Vis 目前没有使用
  • 第七列 Ndx 即 st_shndx,表示该符号所属的节
  • 最后一列 Name 是符号名称

从上图,我们可以看到第一个符号,即下标为 0 的符号,他的类型为 NOTYPE,表示永远是一个未定义的符号。

我们再来举几个例子,索引为 39 的行,他是文件类型,并且 Ndx 就是特殊值 ABS,一般是文件名特有的

1
39: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c

比如索引为 65 的行

1
65: 000000000000066a    19 FUNC    GLOBAL DEFAULT   14 _Z7func_01v

他的类型为 FUNC,即函数;绑定信息为 GLOBAL,即是一个全局符号,外部可见。Ndx 为 14,即 st_shndx 为 14,我们使用 readelf 工具来看看:

1
2
3
4
5
6
7
8
# objdump -d --section .text a.out | grep func_01
000000000000066a <_Z7func_01v>:

# readelf --sections --wide a.out

...
[14] .text PROGBITS 0000000000000560 000560 0001c2 00 AX 0 0 16
...

因为 func_01 函数在 .text 节,而 .text 节在节头表中的索引就是 14。同时我们函数 func_01 的符号值为 0x066a,正是 func_01 的虚拟内存地址。

9. .rel.*.rela.*

我们先来看看名为 .rela.* 节,还是只有输出 “hello world” 的二进制程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# readelf --relocs a.out            

Relocation section '.rela.dyn' at offset 0x410 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200db8 000000000008 R_X86_64_RELATIVE 630
000000200dc0 000000000008 R_X86_64_RELATIVE 5f0
000000201008 000000000008 R_X86_64_RELATIVE 201008
000000200fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fe0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000200fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200ff0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8 000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x4d0 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000200fd0 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0

节的类型为 SHT_RELA,意味着他们包含链接器用于执行重定位的信息。实际上,每个 SHT_RELA 类型的节都是一个重定位条目,每个条目都详细说明了需要应用重定位的特定地址,以及在该地址插入的特定值。

因为在静态链接过程中,所有在 ELF 文件中的静态重定位都已经被解析,所以这里只显示保留了由动态链接器执行的动态重定位节的信息。

三、小结

关于链接相关的内容,包括静态链接和动态链接,以及对应的节。我们下节专门分享。本节内容还是要多多敲一下命令,看看内部结构,然后做一下分析,这样才能吃透 ELF 文件。