Linux ELF 详解2 -- Section Header & Section
1. ELF2. Section Header3. Section4. hexdump5. objdump
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 的类型。只关注红框内的就好,其他的是扩展部分,可以先跳过。
这里有很多类型,每个类型都有不同的含义,譬如:
- SHT_NULL 表示这个 Section Header inactive,没有对应的 Section,其他字段值是 undefined。index=0 的 Section Header,它的类型就是这个。
- 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 的各种属性,每一位代表不同的含义,可以多个位进行组合。
比较常见的有:
- SHF_WRITE 表示 Section 的数据在进程运行期间可写
- SHF_ALLOC 表示 Section 在进程运行期间需要占据内存
- SHF_EXECINSTR 表示 Section 包含了可执行的机器指令
- 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_type,sh_link 和 sh_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 列表可以知道:
- Section header table offset 为1048 bytes
- 每个 Section Header 大小为 64 bytes
- 一共有14个 Section Header
- Section Header 的 sh_name 长度为4 bytes
- Section header string table offset 为 0x3b0
- 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
...
由上可知:
- offset = 0x40
- size = 0x51 = 5 * 16 + 1 = 81
- 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
...
由上可知:
- offset = 0x98
- size = 0x10 = 1 * 16 = 16
- 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个变量是被初始化的:
- d 的值为 ‘d’,十六进制就是0x64。hexdump 结果中头4个字节 “64 00 00 00”,little endian 之后就是 0x64
- f 的值是函数 function 的地址,但是因为 function 来源于外部,当前无法确定它的地址,所以初始值为0。x86-64 中地址为8字节,所以 f 的值对应到 hexdump 结果中的最后8个字节,也就是 “00 00 00 00 00 00 00 00”。
- 在 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
...
由上可知:
- offset = 0xa8
- size = 0x13 = 1 * 16 + 3 = 19
- 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
...
由此可知:
- offset = 0xbb
- size = 0x36 = 3 * 16 + 6 = 54
- 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,但它们分别代表不同的含义。
更多推荐
所有评论(0)