上一篇:ELF 详解1 – ELF Header

ELF Section Header & Section

先看 Section Header 的定义

typedef struct {
	Elf32_Word	sh_name;
	Elf32_Word	sh_type;
	Elf32_Word	sh_flags;
	Elf32_Addr	sh_addr;
	Elf32_Off	sh_offset;
	Elf32_Word	sh_size;
	Elf32_Word	sh_link;
	Elf32_Word	sh_info;
	Elf32_Word	sh_addralign;
	Elf32_Word	sh_entsize;
} Elf32_Shdr;

typedef struct {
	Elf64_Word	sh_name; // 4 B (B for bytes)
	Elf64_Word	sh_type; // 4 B
	Elf64_Xword	sh_flags; // 8 B
	Elf64_Addr	sh_addr; // 8 B
	Elf64_Off	sh_offset; // 8 B
	Elf64_Xword	sh_size; // 8 B
	Elf64_Word	sh_link; // 4 B
	Elf64_Word	sh_info; // 4 B
	Elf64_Xword	sh_addralign; // 8 B
	Elf64_Xword	sh_entsize; // 8 B
} Elf64_Shdr; // total size: 64 B

我们只关注 Elf64_Shdr(64位系统的定义)。

用 readelf 查看 program.o 的 Section Header 列表。

# -S是查看Section Header,-W是拓展显示的宽度
$ readelf -SW program.o
There are 14 section headers, starting at offset 0x418:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        0000000000000000 000040 000051 00  AX  0   0  1
  [ 2] .rela.text        RELA            0000000000000000 0002d8 0000a8 18   I 12   1  8
  [ 3] .data             PROGBITS        0000000000000000 000098 000010 00  WA  0   0  8
  [ 4] .rela.data        RELA            0000000000000000 000380 000018 18   I 12   3  8
  [ 5] .bss              NOBITS          0000000000000000 0000a8 000000 00  WA  0   0  1
  [ 6] .rodata           PROGBITS        0000000000000000 0000a8 000013 00   A  0   0  1
  [ 7] .comment          PROGBITS        0000000000000000 0000bb 000036 01  MS  0   0  1
  [ 8] .note.GNU-stack   PROGBITS        0000000000000000 0000f1 000000 00      0   0  1
  [ 9] .eh_frame         PROGBITS        0000000000000000 0000f8 000038 00   A  0   0  8
  [10] .rela.eh_frame    RELA            0000000000000000 000398 000018 18   I 12   9  8
  [11] .shstrtab         STRTAB          0000000000000000 0003b0 000066 00      0   0  1
  [12] .symtab           SYMTAB          0000000000000000 000130 000180 18     13  10  8
  [13] .strtab           STRTAB          0000000000000000 0002b0 000024 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

重新看一下 ELF Header

$ readelf -h program.o
ELF Header:
...
  Start of section headers:          1048 (bytes into file)
...
  Size of section headers:           64 (bytes)  #跟 Section Header 定义中的 total size 一致
  Number of section headers:         14  #跟 Section Header 列表中的 Header 的个数一致
  Section header string table index: 11

接下来从字节级别查看一下这些 Section Header 的内容。
从 ELF Header 中可以看出 Section header table 的 offset 是1048字节,每个 Section Header 大小为64 bytes,一共有14个。

Section Header undefined
$ hexdump -C -s1048 -n64 program.o
00000418  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000458

可以看到 index=0 的 Section Header,内容全部为0。(在某些情况下,它的某部分字段不为0,这些特殊情况当前先跳过)
这是一个非常特别的 Section Header,它的作用是表示 undefined 。它的 index(=0) 也有一个特别的名字,叫 SHN_UNDEF。后面会讲到它是如何发挥作用。

Section Header .text

index=1 的 Section Header,offset = 1048 + 64 = 1112

$ hexdump -C -s1112 -n64 program.o
00000458  20 00 00 00 01 00 00 00  06 00 00 00 00 00 00 00  | ...............|
00000468  00 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000478  51 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |Q...............|
00000488  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000498

我们以这个 Section Header 为例,来查看各字段的内容。

sh_name

虽然这个字段叫 sh_name,但实际上它包含的是一个索引,这个索引是从一个叫 Section header string table 的地方开始偏移,后面会讲到,现在只需要知道它相当于一个字符数组下标就好。

数据类型:Elf64_Word (4 bytes)

$ hexdump -C -s1112 -n4 program.o
00000458  20 00 00 00                                       | ...|
0000045c

值为:“20 00 00 00” + little endian = 0x20 = 32

sh_type

Section Header 的类型。因为 Section Header 描述了 Section,所以它的类型也可以看做是 Section 的类型。只关注红框内的就好,其他的是扩展部分,可以先跳过。
在这里插入图片描述
这里有很多类型,每个类型都有不同的含义,譬如:

  1. SHT_NULL 表示这个 Section Header inactive,没有对应的 Section,其他字段值是 undefined。index=0 的 Section Header,它的类型就是这个。
  2. SHT_STRTAB 表示 Section 的内容是个 string table。string table 其实就是一个字符数组,存放了多个 string,每个 string 都以 ‘\0’ 作为终结符。一个对象文件可以有多个 string table。后面会讲到不同的 string table 发挥不同的作用。index=11(.shstrtab) 的 Section Header,类型就是这个。

数据类型:Elf64_Word (4 bytes)

$ hexdump -C -s1116 -n4 program.o
0000045c  01 00 00 00                                       |....|
00000460

值为:“01 00 00 00” + little endian = 0x1
由上图可知,1对应的类型是 SHT_PROGBITS,表示这部分的信息,格式和意义完全由程序来定义。内容可以是已初始化的数据,未初始化的数据,comment 或者是程序代码等。

sh_flags

表示 Section 的各种属性,每一位代表不同的含义,可以多个位进行组合。
在这里插入图片描述
比较常见的有:

  1. SHF_WRITE 表示 Section 的数据在进程运行期间可写
  2. SHF_ALLOC 表示 Section 在进程运行期间需要占据内存
  3. SHF_EXECINSTR 表示 Section 包含了可执行的机器指令
  4. SHF_INFO_LINK 表示这个 Section Header 的 sh_info 字段包含了另外一个 Section Header 的 index。(这个后面会详细讲到)

数据类型:Elf64_Xword (8 bytes)

$ hexdump -C -s1120 -n8 program.o
00000460  06 00 00 00 00 00 00 00                           |........|
00000468

值为:“06 00 00 00 00 00 00 00” + little endian = 0x6 = SHF_ALLOC | SHF_EXECINSTR => 这部分是可执行代码,进程运行时需要放置在内存中。
再对照回 Section Header 列表中 “.text” 那一行

$ 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
...  
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

从下面的 “Key to Flags” 可以看出 “.text” 对应的就是 A (alloc), X (execute)。

sh_addr

Section 存放于进程内存映像中的虚存地址,如果 Section 不需要出现在内存中,则为0。Relocatable file 的虚存地址都为0。Executable file 和 Shared object file 才会为有需要的 Section 计算虚存地址。后面会讲到。

数据类型:Elf64_Addr (8 bytes)

$ hexdump -C -s1128 -n8 program.o
00000468  00 00 00 00 00 00 00 00                           |........|
00000470
sh_offset

表示 Section 在文件中的字节偏移量(offset)。
类型为 NOBITS (SHT_NOBITS = 8) 的 Section Header 比较特别,它本身在文件中不占据空间,但还是拥有 offset,下图显示 “.bss” 和 “.rodata” 的 offset 是一样的,但 “.bss” 的 size 为0。

$ readelf -SW program.o
...
Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
...
  [ 5] .bss              NOBITS          0000000000000000 0000a8 000000 00  WA  0   0  1
  [ 6] .rodata           PROGBITS        0000000000000000 0000a8 000013 00   A  0   0  1
...

数据类型:Elf64_Off (8 bytes)

$ hexdump -C -s1136 -n8 program.o
00000470  40 00 00 00 00 00 00 00                           |@.......|
00000478

值为:“40 00 00 00 00 00 00 00” + little endian = 0x40 = 4 * 16 = 64
因为 program.o 没有 Program header table(从 ELF Header 中可以得知),所以 “.text” 的 Section 在文件中是紧贴着 ELF Header 存放的。

sh_size

表示 Section 在文件中占据的字节数。
类型为 NOBITS 的 Section,即使这个值不为0,它在文件中也不占据空间。".bss" Section 就是这个类型。bss 全称为 “block started by symbol”(更简单的记法是:Better Save Space),存放的是未初始化的数据,因为数据无初始值,所以只需要记录它在内存中占据的空间即可,在文件中不需要额外的存储。相反,".data" Section 存储的则是已经初始化的数据,所以需要在文件中记录下初始值,才能在进程内存映像中把这些值带进去。

数据类型:Elf64_Xword (8 bytes)

$ hexdump -C -s1144 -n8 program.o
00000478  51 00 00 00 00 00 00 00                           |Q.......|
00000480

值为:“51 00 00 00 00 00 00 00” + little endian = 0x51 = 5 * 16 + 1 = 81

sh_link

包含另外一个 Section Header 的 index,具体的含义取决于 Section 的类型。后面会详细讲到。

数据类型:Elf64_Word (4 bytes)

$ hexdump -C -s1152 -n4 program.o
00000480  00 00 00 00                                       |....|
00000484
sh_info

包含了额外的信息,具体的含义取决于 Section 的类型。
需要把 sh_typesh_linksh_info 联合在一起进行解析。
另外,当 sh_flags=SHF_INFO_LINK 时,sh_info 则表示另外一个 Section Header 的 index。后面会讲到。

数据类型:Elf64_Word (4 bytes)

$ hexdump -C -s1156 -n4 program.o
00000484  00 00 00 00                                       |....|
00000488
sh_addralign

表示对齐约束。0或1表示无约束。sh_addr % sh_addralign 必须等于0,后面会讲到。

数据类型:Elf64_Xword (8 bytes)

$ hexdump -C -s1160 -n8 program.o
00000488  01 00 00 00 00 00 00 00                           |........|
00000490

值为:“01 00 00 00 00 00 00 00” + little endian = 0x1 => 无约束

sh_entsize

有些 Section 会包含一组大小固定的记录,这时 sh_entsize 就表示记录的大小,通过 sh_size / sh_entsize 就能得到记录的个数。如果 Section 没有包含这种类型的记录,则值为0。比如后面会看到的 “.shstrtab” Section,包含的是一组 string,但 string 的长度不一样,于是 sh_entsize 的值就为0。

数据类型:Elf64_Xword (8 bytes)

$ hexdump -C -s1168 -n8 program.o
00000490  00 00 00 00 00 00 00 00                           |........|
00000498

可以把上面各个字段的信息和 Section Header 列表中 “.text” 行的输出相对比来加深理解。

接下来将查看其中几个 Section 的内容。

Section .shstrtab

这个 Section 包含了各个 Section Header 的 string 名字。

$ readelf -SW program.o
...
Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
...
  [11] .shstrtab         STRTAB          0000000000000000 0003b0 000066 00      0   0  1
...

可以看出:
offset = 0x3b0
size = 0x66 = 6 * 16 + 6 = 102

# 注意,这里 -s 后面跟随的是16进制数字
$ hexdump -C -s0x3b0 -n96 program.o
000003b0  00 2e 73 79 6d 74 61 62  00 2e 73 74 72 74 61 62  |..symtab..strtab|
000003c0  00 2e 73 68 73 74 72 74  61 62 00 2e 72 65 6c 61  |..shstrtab..rela|
000003d0  2e 74 65 78 74 00 2e 72  65 6c 61 2e 64 61 74 61  |.text..rela.data|
000003e0  00 2e 62 73 73 00 2e 72  6f 64 61 74 61 00 2e 63  |..bss..rodata..c|
000003f0  6f 6d 6d 65 6e 74 00 2e  6e 6f 74 65 2e 47 4e 55  |omment..note.GNU|
00000400  2d 73 74 61 63 6b 00 2e  72 65 6c 61 2e 65 68 5f  |-stack..rela.eh_|
00000410

右边的列已经显示出所有 Section Header 的名字 string,这些 string 都是以 ‘\0’ 作为终结符。对于所有不可见或无法显示的字符,都以 ‘.’ 来显示,这样一来,反而把能显示出来的 ‘.’ 和无法显示的字符混在一起了。
比如 “..symtab.” 这个字符串,如果参照左边的编码表,就能发现,它的第一个和最后一个字符都是 “00”,也就是 ‘\0’,第二个字符是能显示的 ‘.’,因此不要混淆了。

接下来,我们把所有 Section Header 的 sh_name 都拿出来,对照上表看看结果。
从 ELF Header 和 Section Header 列表可以知道:

  1. Section header table offset 为1048 bytes
  2. 每个 Section Header 大小为 64 bytes
  3. 一共有14个 Section Header
  4. Section Header 的 sh_name 长度为4 bytes
  5. Section header string table offset 为 0x3b0
  6. Section header string table size 为 0x66

print_sh_names.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
	off_t headerTableOffset = 1048;
	size_t headerSize = 64;
	off_t stringSectionOffset = 0x3b0;
	size_t stringSectionSize = 0x66;
	size_t nameSize = 4;
	int sectionNum = 14;
	char content[stringSectionSize];
	union {
		char b[4];
		int off;
	} headerName;

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

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

	int i;
	int currHeaderOffset = headerTableOffset;
	char *start;
	for (i = 0; i < sectionNum; ++i) {
		if (lseek(fd, currHeaderOffset, SEEK_SET) == -1)
		  exit(EXIT_FAILURE);
		if (read(fd, headerName.b, 4) != 4)
		  exit(EXIT_FAILURE);

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

		currHeaderOffset += headerSize;
	}

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

	return 0;
}

$ ./print_sh_names 
[ 0]: off: 0x00,  name: 
[ 1]: off: 0x20,  name: .text
[ 2]: off: 0x1b,  name: .rela.text
[ 3]: off: 0x2b,  name: .data
[ 4]: off: 0x26,  name: .rela.data
[ 5]: off: 0x31,  name: .bss
[ 6]: off: 0x36,  name: .rodata
[ 7]: off: 0x3e,  name: .comment
[ 8]: off: 0x47,  name: .note.GNU-stack
[ 9]: off: 0x5c,  name: .eh_frame
[10]: off: 0x57,  name: .rela.eh_frame
[11]: off: 0x11,  name: .shstrtab
[12]: off: 0x01,  name: .symtab
[13]: off: 0x09,  name: .strtab

可以看到跟 “readelf -SW program.o” 的结果是一致的。

Section .text

这个 Section 包含了程序中可执行的指令。

$ 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
...

由上可知:

  1. offset = 0x40
  2. size = 0x51 = 5 * 16 + 1 = 81
  3. flag = AX => 进程运行期间占据内存 + 这部分是可执行的指令 => 可执行
$ hexdump -s0x40 -n81 -C 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

再用 objdump 查看其中的代码:

$ objdump -d program.o

program.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	bf 64 00 00 00       	mov    $0x64,%edi
   d:	e8 00 00 00 00       	callq  12 <main+0x12>
  12:	89 c2                	mov    %eax,%edx
  14:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 1a <main+0x1a>
  1a:	01 d0                	add    %edx,%eax
  1c:	89 45 fc             	mov    %eax,-0x4(%rbp)
  1f:	8b 05 00 00 00 00    	mov    0x0(%rip),%eax        # 25 <main+0x25>
  25:	89 c6                	mov    %eax,%esi
  27:	bf 00 00 00 00       	mov    $0x0,%edi
  2c:	b8 00 00 00 00       	mov    $0x0,%eax
  31:	e8 00 00 00 00       	callq  36 <main+0x36>
  36:	8b 45 fc             	mov    -0x4(%rbp),%eax
  39:	89 c6                	mov    %eax,%esi
  3b:	bf 00 00 00 00       	mov    $0x0,%edi
  40:	b8 00 00 00 00       	mov    $0x0,%eax
  45:	e8 00 00 00 00       	callq  4a <main+0x4a>
  4a:	b8 00 00 00 00       	mov    $0x0,%eax
  4f:	c9                   	leaveq 
  50:	c3                   	retq   

两者完全吻合,这其实就是我们 main 函数反汇编后的指令。

Section .data

这个 Section 存放的是已经初始化了的数据。

$ 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
...

由上可知:

  1. offset = 0x98
  2. size = 0x10 = 1 * 16 = 16
  3. flag = WA => 进程运行期间可写 + 进程运行期间占据内存 => 可读写
$ 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

结合 program.c 的代码:

$ cat program.c
...
static char d = 'd';
char* f = (char*) function;
...

这里只有2个变量是被初始化的:

  1. d 的值为 ‘d’,十六进制就是0x64。hexdump 结果中头4个字节 “64 00 00 00”,little endian 之后就是 0x64
  2. f 的值是函数 function 的地址,但是因为 function 来源于外部,当前无法确定它的地址,所以初始值为0。x86-64 中地址为8字节,所以 f 的值对应到 hexdump 结果中的最后8个字节,也就是 “00 00 00 00 00 00 00 00”。
  3. 在 d 和 f 中间还隔着4个0(“00 00 00 00”),这4个0就是用作对齐的 padding,后面 Symbol 篇章会讲到对齐约束。
Section .rodata

这个 Section 存放的是只读数据。

$ readelf -SW program.o
...
Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
...
  [ 6] .rodata           PROGBITS        0000000000000000 0000a8 000013 00   A  0   0  1
...

由上可知:

  1. offset = 0xa8
  2. size = 0x13 = 1 * 16 + 3 = 19
  3. flag = A => 进程运行期间占据内存 => 只读
$ hexdump -C -s0xa8 -n19 program.o
000000a8  61 3a 20 25 64 0a 00 72  65 73 75 6c 74 3a 20 25  |a: %d..result: %|
000000b8  64 0a 00                                          |d..|
000000bb

这里包含2个字符串:
“61 3a 20 25 64 0a 00” => “a: %d\n” (“0x0a” 对应的是 ‘\n’)
“72 65 73 75 6c 74 3a 20 25 64 0a” => “result: %d\n”

结合 program.c 的代码:

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

可以看出 printf 的 format string 就是作为只读数据存在。

Section .comment

这个 Section 包含了版本控制信息。

$ readelf -SW program.o
...
Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
...
  [ 7] .comment          PROGBITS        0000000000000000 0000bb 000036 01  MS  0   0  1
...

由此可知:

  1. offset = 0xbb
  2. size = 0x36 = 3 * 16 + 6 = 54
  3. flag = MS => 需要把重复的字符串进行合并,libfunc.so 也有同样的 comment,在最终生成的 program 中,comment 只有一份。
$ hexdump -C -s0xbb -n54 program.o
000000bb  00 47 43 43 3a 20 28 55  62 75 6e 74 75 20 35 2e  |.GCC: (Ubuntu 5.|
000000cb  34 2e 30 2d 36 75 62 75  6e 74 75 31 7e 31 36 2e  |4.0-6ubuntu1~16.|
000000db  30 34 2e 31 32 29 20 35  2e 34 2e 30 20 32 30 31  |04.12) 5.4.0 201|
000000eb  36 30 36 30 39 00                                 |60609.|
000000f1

虽然 .text, .data, .rodata, .comment 的类型都是 PROGBITS,但它们分别代表不同的含义。

下一篇:ELF 详解3 – Symbol Table & Symbol

Logo

更多推荐