Section 与 Segment:从 链接器 到 Runtime 的角度出发

⚠ 转载请注明出处:作者:ZobinHuang,更新日期:Oct.4 2021


知识共享许可协议

    本作品ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。

1. 从 链接器 和 Runtime 的角度出发

    我们在 linux 下编译后、链接后的形成程序是 ELF (Executable and Linkable Format) 目标文件。注意!此处区别于我们平时指代的目标文件,在以前的语义中我们常把编译后未经链接的文件称为目标文件,而 ELF 目标文件大概可以分为以下几种:

ELF 目标文件类型
描述
待重定位文件 (Relocatable File)
[平时语义下的目标文件]
待重定位文件就是平时常说的目标文件,属于源文件编译后但未完成链接的半成品,它被用于与其他目标文件合并连接,以构建出二进制可执行文件或动态链接库。称其为 "待重定位文件" 的原因是因为在该目标文件中,如果引用了其它外部文件 (其它目标文件或者库文件) 中定义的符号 (i.e. 变量、函数等),在编译阶段只能先标示出一个符号名,该符号具体的地址还不能确定,因为不知道该符号是在哪个外部文件中,而该外部文件需要被重定位后才能确定文件内的符号地址,这些重定位的工作是需要在链接的过程中完成的
共享目标文件 (Shared Object File) 这就是我们常说的动态链接库,在可执行文件被加载的过程中需要被 动态链接,称为程序代码的一部分
可执行文件 (Executable File) 经过编译链接后的、可以直接运行的程序文件

    在理解了 ELF 的实质后,我们下面来看出现在 ELF 中最重要的两个概念:Section (节)Segment (段) 的区别。由于它们常常被混为一谈,以至于读者朋友可能会有所混淆。

1.1 Section (节)

    Section 是给链接器看的。Section 中包含了各种用于链接和重定位的内容。我们平时在 NASM 风格下编写汇编代码的时候,使用的 "SECTION" 关键字定义的一段内存就是一个 Section,在其中我们可能会使用到大量的 Readable 的变量名、函数名等。Section 的理解可以如上图左侧所示。

    如上左图所示,是一个典型的 ELF 文件的 Section 布局情况,各个经典 Section 的作用如下所示:

Section 描述
.text 包含了程序中可执行的代码段
.rodata 用于维护只读数据,比如:常量字符串、带 const 修饰的全局变量和静态局部变量等
.data 用于维护初始化的且初始值非 0 的全局变量和静态局部变量 (不带 const 修饰)
.bss 用于维护未初始化的或初始值为 0 的全局变量和静态局部变量 (不带 const 修饰)

1.2 Segment (段)

    Segment 是给 Runtime 看的。我们在汇编系列的博客中曾经详细地分析了保护模式下 CPU 对内存访问的保护,在当时我们知道了我们可以把段区分成代码段、数据段,并且赋予它们不同的特权级。由于在一个任务内部的各个段特权级都是一致的,所以我们在这里只考虑段是代码段还是数据段这一特性。实质上,基于上面我们对 Section 的理解,链接器的工作可以分为以下三点:

  • 将性质相同的 Sections 进行归类合并成为一个 Segment (p.s. 有些 Section 在这个阶段可能会被丢弃)
  • 将各个 Segment 在 4GB 虚拟内存空间中展开
  • 重定位各个 Segment 中各个 Section 内部中的符号

    链接器在完成这一步之后,最终就形成了可执行文件。在这个可执行文件的内部包含了若干的 Segment,和相应的 ELF 头部。操作系统根据 ELF 中的 metadata,就知道如何将程序加载到 4GB 虚拟地址空间中,并且予以执行了。

2. ELF 文件分析示例

    我们现在尝试对下面这段非常简单的 C 程序使用不同的链接器进行链接 (p.s. 统一为 x86_64 架构):

1
2
3
4
5
// kernel.c
int main(void){
while(1);
return 0;
}

2.1 gold

    在下面的链接中,我们使得最后形成的包含 .text 这个 Section 的 Segment 的起始地址位于 0xc0001500,并且注意进行的是 32 位编译和链接。

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
# 编译
gcc -m32 -c -o kernel.o kernel.c

# 链接
zobin@ubuntu-devbox:~/projects/os_dev/kernel$ gold kernel.o -melf_i386 -Ttext 0xc0001500 -e main -o kernel.bin

# show ELF
zobin@ubuntu-devbox:~/projects/os_dev/kernel$ readelf -e ./kernel.bin
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2 s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0xc0001500
Start of program headers: 52 (bytes into file)
Start of section headers: 4480 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 4
Size of section headers: 40 (bytes)
Number of section headers: 12
Section header string table index: 11

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS c0001500 000500 000017 00 AX 0 0 1
[ 2] .eh_frame PROGBITS c0001518 000518 000048 00 A 0 0 4
[ 3] .got PROGBITS c0002ff4 000ff4 000000 00 WA 0 0 4
[ 4] .got.plt PROGBITS c0002ff4 000ff4 00000c 00 WA 0 0 4
[ 5] .data PROGBITS c0003000 001000 000000 00 WA 0 0 1
[ 6] .bss NOBITS c0003000 001000 000000 00 WA 0 0 1
[ 7] .comment PROGBITS 00000000 001000 00002b 01 MS 0 0 1
[ 8] .note.gnu.gold-ve NOTE 00000000 00102c 00001c 00 0 0 4
[ 9] .symtab SYMTAB 00000000 001048 000080 10 10 4 4
[10] .strtab STRTAB 00000000 0010c8 000051 00 0 0 1
[11] .shstrtab STRTAB 00000000 001119 000064 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),
p (processor specific)

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000500 0xc0001500 0xc0001500 0x00060 0x00060 R E 0x1000
LOAD 0x000ff4 0xc0002ff4 0xc0002ff4 0x0000c 0x0000c RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x000ff4 0xc0002ff4 0xc0002ff4 0x0000c 0x0000c RW 0x4

Section to Segment mapping:
Segment Sections...
00 .text .eh_frame
01 .got .got.plt
02
03 .got .got.plt

    可以看到,在使用 gold 链接后形成的 ELF 文件中,.text 和 .rodata Section 被 Merged 到第一个 RE Segment 中去了;而 .data 和 .bss Section 则被 Merged 到第二个 RW Segment 中去了。

2.2 ld

    同样地,在下面的链接中,我们使得最后形成的包含 .text 这个 Section 的 Segment 的起始地址位于 0xc0001500,并且注意进行的是 32 位编译和链接。

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
zobin@ubuntu-devbox:~/projects/os_dev/kernel$ ld kernel.o -melf_i386 -Ttext 0xc0001500 -e main -o kernel.bin
zobin@ubuntu-devbox:~/projects/os_dev/kernel$ readelf -e ./kernel.bin
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0xc0001500
Start of program headers: 52 (bytes into file)
Start of section headers: 8636 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 7
Size of section headers: 40 (bytes)
Number of section headers: 9
Section header string table index: 8

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .note.gnu.propert NOTE 08048114 000114 00001c 00 A 0 0 4
[ 2] .text PROGBITS c0001500 000500 000017 00 AX 0 0 1
[ 3] .eh_frame PROGBITS c0002000 001000 000048 00 A 0 0 4
[ 4] .got.plt PROGBITS c0004000 002000 00000c 04 WA 0 0 4
[ 5] .comment PROGBITS 00000000 00200c 00002a 01 MS 0 0 1
[ 6] .symtab SYMTAB 00000000 002038 0000e0 10 7 9 4
[ 7] .strtab STRTAB 00000000 002118 000051 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 002169 000050 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),
p (processor specific)

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x00130 0x00130 R 0x1000
LOAD 0x000500 0xc0001500 0xc0001500 0x00017 0x00017 R E 0x1000
LOAD 0x001000 0xc0002000 0xc0002000 0x00048 0x00048 R 0x1000
LOAD 0x002000 0xc0004000 0xc0004000 0x0000c 0x0000c RW 0x1000
NOTE 0x000114 0x08048114 0x08048114 0x0001c 0x0001c R 0x4
GNU_PROPERTY 0x000114 0x08048114 0x08048114 0x0001c 0x0001c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10

Section to Segment mapping:
Segment Sections...
00 .note.gnu.property
01 .text
02 .eh_frame
03 .got.plt
04 .note.gnu.property
05 .note.gnu.property
06

    可以看到,在使用 ld 作为链接器生成的 ELF 文件中,生成了更多的 loadable segment。这是因为出于安全原因考虑,.rodata Section 不应该是 executable 的,所以上面使用 gold 链接器生成的 ELF 文件中把 .rodata Section 合进一个 executable 的 Segment 是不合理的。因此 .rodata 被独立成为一个 read-only 的 Segment。至于另一个 read-only 的 Segment,那是 ELF Header,至于为什么它也是一个 Loadable 的段,就以后再探究了。

附录:参考源

  1. StackOverflow, What’s the difference of section and segment in ELF file format
  2. StackOverflow, Why an ELF executable could have 4 LOAD segments?