目标文件的 ELF 结构
实验环境:
Linux version 5.11.0-46-generic (buildd@lgw01-amd64-010)
Ubuntu 20.04.3 LTS
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34
Arch x86_64
ELF
ELF 为 可执行可链接文件格式 的缩写. 可重定位文件 (目标文件), 可执行文件, 共享目标文件 等文件类型都以 ELF 格式储存.
可重定位文件
就是目标文件, Linux 下后缀为 .o
(Object). 这一类文件是编译后链接前的中间文件. 其中, 代码里定义在其他模块(如其他 .c
文件)里的变量暂时没有地址, 在目标文件中用全 0 占位. 链接器链接文件时, 将其地址替换为变量真正的地址, 这个过程叫 重定位.
gcc
选项 -c
表示只编译不链接. 可以用如下命令把源代码编译为目标文件:
|
|
SimpleSection.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
可执行文件
不解释
共享目标文件
即动态链接库, Linux 下后缀为 .so
(Shared Object) 动态连接器可以与可执行文件结合, 作为程序进程的一部分来运行.
|
|
/lib/i386-linux-gnu/ld-2.31.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=c6d33dc0bb4a6e9fed4fa72af98b59b8d0fec3f1, stripped
目标文件的 ELF 结构
由于目标文件和可执行文件都是 ELF 结构, 而目标文件没有链接过程, 所以我们分析目标文件, 来窥探 ELF 的大致结构. ELF 由三个部分组成, 分别是 文件头, 一系列 节, 节表 和 程序段表.
节 Part I
在目标文件中, 不需要程序段这个东西. 而节, 是目标文件中最重要的内容.
目标文件包含编译后的机器代码和数据, 以及一些链接需要的信息. 这些信息按照不同的属性存在不同的节里. objdump
可以查看目标文件的各个节.
下面以程序 SimpleSection.c
编译成的 SimpleSecton.o
为例, 源代码如下:
|
|
命令 objdump -h
可以查看目标文件的结构和内容. (当然也可以查看可执行文件的结构和内容)
|
|
SimpleSection.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .group 00000008 00000000 00000000 00000034 2**2
CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
1 .text 00000087 00000000 00000000 0000003c 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data 00000008 00000000 00000000 000000c4 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000004 00000000 00000000 000000cc 2**2
ALLOC
4 .rodata 00000004 00000000 00000000 000000cc 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .text.__x86.get_pc_thunk.ax 00000004 00000000 00000000 000000d0 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
6 .comment 0000002b 00000000 00000000 000000d4 2**0
CONTENTS, READONLY
7 .note.GNU-stack 00000000 00000000 00000000 000000ff 2**0
CONTENTS, READONLY
8 .note.gnu.property 0000001c 00000000 00000000 00000100 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .eh_frame 0000007c 00000000 00000000 0000011c 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
(和书上的不同, 可能是编译器和机器版本不同的原因, 以及开没开一些选项?)
可以看到, 文件格式是 elf32-i386, 即 32 位 Intel 80386 处理器下的 ELF 文件. 一共有 9 个非辅助性的节 (一些辅助性质的节, 如 .symtab 等"表"被省略掉了. 要查看所有的节, 可以使用 readelf -S
). (其中有一堆我不知道是啥, 之后知识面广了再回来看QAQ.)
几个比较重要的节是 .text, 包含程序的 代码 (机器指令); .data 包含 已初始化的全局变量和静态局部变量; .bss 包含 未初始化的全局变量和静态局部变量, .rodata (read only data) 包含只读变量如 const
修饰的变量和字符串常量. 其他段如 .note 开头的段包含一些提示信息, .commment 段包含注释信息, (.group 书上没有网上查到的东西我暂时也看不懂, 先不理它.) .text.__x86.get_pc_thunk.ax 包含一个 (系统用于函数调用栈的?) 函数的代码, .eh_frame 包含一些调用栈信息的记录.
表格中一个节有两行, 第一行是一些数据, 比较重要的是 Size (大小[单位字节]) 和 File off (偏移量, 即开始虚拟地址), 可以发现, .group 从 0x34 开始, 大小为 0x08 字节, .text 从 0x3c 开始, 大小为 0x87 字节. 可以发现, .group 和 .text 是连着的. .group 之前 0x34 大小的数据是 ELF 文件头大小. 有些相邻节之间差了一两个字节, 是因为这个节需要对齐
第二行是各个节的属性, CONTENTS 表示是 ELF 的内容, 也即 存在 ELF 文件中; DATA 表示该节存放数据, CODE 表示该节存放代码, READONLY 表示该节只读. (ALLOC LOAD RELOC 啥的看不懂, 书上没讲, 留坑)
.text
objdump
参数 -s
将节内容以十六进制打印, -d
反汇编代码并打印. 所以我们可以用这两个参数来查看 .text 详细内容.
|
|
SimpleSection.o: file format elf32-i386
Contents of section .group:
0000 01000000 07000000 ........
Contents of section .text:
0000 f30f1efb 5589e553 83ec04e8 fcffffff ....U..S........
0010 05010000 0083ec08 ff75088d 90000000 .........u......
0020 005289c3 e8fcffff ff83c410 908b5dfc .R............].
0030 c9c3f30f 1efb8d4c 240483e4 f0ff71fc .......L$.....q.
0040 5589e551 83ec14e8 fcffffff 05010000 U..Q............
0050 00c745f0 01000000 8b900400 00008b80 ..E.............
0060 00000000 01c28b45 f001c28b 45f401d0 .......E....E...
0070 83ec0c50 e8fcffff ff83c410 8b45f08b ...P.........E..
0080 4dfcc98d 61fcc3 M...a..
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
Contents of section .text.__x86.get_pc_thunk.ax:
0000 8b0424c3 ..$.
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520392e .GCC: (Ubuntu 9.
0010 332e302d 31377562 756e7475 317e3230 3.0-17ubuntu1~20
0020 2e303429 20392e33 2e3000 .04) 9.3.0.
Contents of section .note.gnu.property:
0000 04000000 0c000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 ............
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 017c0801 .........zR..|..
0010 1b0c0404 88010000 20000000 1c000000 ........ .......
0020 00000000 32000000 00450e08 8502420d ....2....E....B.
0030 05448303 66c5c30c 04040000 28000000 .D..f.......(...
0040 40000000 32000000 55000000 00480c01 @...2...U....H..
0050 00471005 02750043 0f03757c 067e0c01 .G...u.C..u|.~..
0060 0041c543 0c040400 10000000 6c000000 .A.C........l...
0070 00000000 04000000 00000000 ............
Disassembly of section .text:
00000000 <func1>:
0: f3 0f 1e fb endbr32
4: 55 push %ebp
5: 89 e5 mov %esp,%ebp
7: 53 push %ebx
8: 83 ec 04 sub $0x4,%esp
b: e8 fc ff ff ff call c <func1+0xc>
10: 05 01 00 00 00 add $0x1,%eax
15: 83 ec 08 sub $0x8,%esp
18: ff 75 08 pushl 0x8(%ebp)
1b: 8d 90 00 00 00 00 lea 0x0(%eax),%edx
21: 52 push %edx
22: 89 c3 mov %eax,%ebx
24: e8 fc ff ff ff call 25 <func1+0x25>
29: 83 c4 10 add $0x10,%esp
2c: 90 nop
2d: 8b 5d fc mov -0x4(%ebp),%ebx
30: c9 leave
31: c3 ret
00000032 <main>:
32: f3 0f 1e fb endbr32
36: 8d 4c 24 04 lea 0x4(%esp),%ecx
3a: 83 e4 f0 and $0xfffffff0,%esp
3d: ff 71 fc pushl -0x4(%ecx)
40: 55 push %ebp
41: 89 e5 mov %esp,%ebp
43: 51 push %ecx
44: 83 ec 14 sub $0x14,%esp
47: e8 fc ff ff ff call 48 <main+0x16>
4c: 05 01 00 00 00 add $0x1,%eax
51: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp)
58: 8b 90 04 00 00 00 mov 0x4(%eax),%edx
5e: 8b 80 00 00 00 00 mov 0x0(%eax),%eax
64: 01 c2 add %eax,%edx
66: 8b 45 f0 mov -0x10(%ebp),%eax
69: 01 c2 add %eax,%edx
6b: 8b 45 f4 mov -0xc(%ebp),%eax
6e: 01 d0 add %edx,%eax
70: 83 ec 0c sub $0xc,%esp
73: 50 push %eax
74: e8 fc ff ff ff call 75 <main+0x43>
79: 83 c4 10 add $0x10,%esp
7c: 8b 45 f0 mov -0x10(%ebp),%eax
7f: 8b 4d fc mov -0x4(%ebp),%ecx
82: c9 leave
83: 8d 61 fc lea -0x4(%ecx),%esp
86: c3 ret
Disassembly of section .text.__x86.get_pc_thunk.ax:
00000000 <__x86.get_pc_thunk.ax>:
0: 8b 04 24 mov (%esp),%eax
3: c3 ret
其中, Contents of section .text:
这一段中间部分是以十六进制打印的指令, 左边是偏移, 右边是十六进制的 ASCII 码, 对照下面 Disassembly of section .text:
, 左边是机器码, 右边是反汇编指令, 可以看到 .text 的指令和十六进制机器指令是一一对应的, 如开始四个字节 f30f1efb
就是反汇编后的第一行的机器码. 同理最后一行的 ret
(c3
), 也是 .text 最后一个字节. 并且可以看到, .text 一共 0x87 个字节, 这和 objdump -h
得到的是一样的.
.data 和 .rodata
.data 包含 已初始化的全局变量和静态局部变量, 而 .rodata 里存放的是只读变量, 如 const
修饰的变量, 字符串常量等. .data 里的变量的值可以被改变, 而 .rodata 是只读数据, 里面的变量不能被改变.
依旧使用 objdump -s -d
查看各个节的详细信息, 可以看到:
Contents of section .data:
0000 54000000 55000000 T...U...
Contents of section .rodata:
0000 25640a00 %d..
.data 有 8 个字节, 这正好是全局变量 global_init_var
和 静态局部变量 static_var
的大小(一个 int 4 个字节, 两个 int 8 个字节), 同时, 0x54000000 是十进制数 84 小端序的存储方式, 0x55000000 是十进制数 85 小端序的存储方式.
.rodata 一共有 4 个字节, 这里存放的是字符串常量 "%d\n"
(就是 printf
的第一个参数), 数据 0x25 是字符 %
的 ASCII 码, 0x64 是字符 d
的 ASCII 码, 而 0x0a 是 \n
的 ASCII 码. 末尾的 0x00 是字符串结尾符号 \0
.
.bss
bss 全称 Block Started By Symbol, 是为符号预留的空间. 也就是说, .bss 并不占用磁盘的空间, 而是在 程序运行后再分配空间. 但是根据 objdump -h
的结果可以看到, .bss 段只有 4 个字节, 而程序里有两个 int 未初始化的全局变量和局部静态变量: global_uninit_varabal
和 static_var2
.
Sections:
Idx Name Size VMA LMA File off Algn
3 .bss 00000004 00000000 00000000 000000cc 2**2
ALLOC
这是因为有些编译器会将目标文件中的未初始化的全局变量放在 COMMON 块, 链接成可执行文件后才会放在可执行文件的 .bss 节.
命令 objdump -x
可以查看符号表, 看到各个变量的存放位置.
查看目标文件的符号表, 可以看到如下内容:
SYMBOL TABLE:
00000000 l df *ABS* 00000000 SimpleSection.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .rodata 00000000 .rodata
00000004 l O .data 00000004 static_var.1512
00000000 l O .bss 00000004 static_var2.1513
00000000 l d .text.__x86.get_pc_thunk.ax 00000000 .text.__x86.get_pc_thunk.ax
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .note.gnu.property 00000000 .note.gnu.property
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 l d .group 00000000 .group
00000000 g O .data 00000004 global_init_var
00000004 O *COM* 00000004 global_uninit_var
00000000 g F .text 00000032 func1
00000000 g F .text.__x86.get_pc_thunk.ax 00000000 .hidden __x86.get_pc_thunk.ax
00000000 *UND* 00000000 _GLOBAL_OFFSET_TABLE_
00000000 *UND* 00000000 printf
00000032 g F .text 00000055 main
和变量有关的符号是这四行:
00000004 l O .data 00000004 static_var.1512
00000000 l O .bss 00000004 static_var2.1513
00000000 g O .data 00000004 global_init_var
00000004 O *COM* 00000004 global_uninit_var
可以看到, static_var
和 global_init_var
被放在了 .data 节, static_var2
被放在了 .bss 节, 而 global_uninit_var
在 COMMON 里.
如果将程序链接, 得到可执行文件, 再用 objdump
查看 global_uninit_var
符号表, 可以发现确实是被放在了 .bss 节里:
|
|
00004018 g O .bss 00000004 global_uninit_var
有一个值得注意的点是, 如果已初始化的全局变量或者局部静态变量的初始值是 0, 那么他将被放在 .bss 节里. 因为 .bss 节里存放的未初始化的全局变量和局部静态变量的默认值就是 0, 这并不会改变内容, 而 .bss 节是不占用磁盘空间的.
我们可以写一个程序验证一下:
|
|
还是编译到目标文件不链接, 然后查看符号表:
|
|
test.o: file format elf32-i386
test.o
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .group 00000008 00000000 00000000 00000034 2**2
CONTENTS, READONLY, GROUP, LINK_ONCE_DISCARD
1 .text 00000018 00000000 00000000 0000003c 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 .data 00000004 00000000 00000000 00000054 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000004 00000000 00000000 00000058 2**2
ALLOC
4 .text.__x86.get_pc_thunk.ax 00000004 00000000 00000000 00000058 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
5 .comment 0000002b 00000000 00000000 0000005c 2**0
CONTENTS, READONLY
6 .note.GNU-stack 00000000 00000000 00000000 00000087 2**0
CONTENTS, READONLY
7 .note.gnu.property 0000001c 00000000 00000000 00000088 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .eh_frame 0000004c 00000000 00000000 000000a4 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SYMBOL TABLE:
00000000 l df *ABS* 00000000 test.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l O .data 00000004 y.1505
00000000 l O .bss 00000004 x.1504
00000000 l d .text.__x86.get_pc_thunk.ax 00000000 .text.__x86.get_pc_thunk.ax
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .note.gnu.property 00000000 .note.gnu.property
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 l d .group 00000000 .group
00000000 g F .text 00000018 main
00000000 g F .text.__x86.get_pc_thunk.ax 00000000 .hidden __x86.get_pc_thunk.ax
00000000 *UND* 00000000 _GLOBAL_OFFSET_TABLE_
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000008 R_386_PC32 __x86.get_pc_thunk.ax
0000000d R_386_GOTPC _GLOBAL_OFFSET_TABLE_
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
00000020 R_386_PC32 .text
00000040 R_386_PC32 .text.__x86.get_pc_thunk.ax
可以看到这两行:
00000000 l O .data 00000004 y.1505
00000000 l O .bss 00000004 x.1504
确实 变量 x
放在了 .bss 节, 而变量 y
放在了 .data 节.
文件头
ELF 文件头共 52 个字节, 使用十六进制编辑器可以看到, SimpleSection.o
的文件头如下:
00000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 0100 0300 0100 0000 :.ELF....................
00000018: 0000 0000 0000 0000 7404 0000 0000 0000 3400 0000 0000 2800 :........t.......4.....(.
00000030: 1000 0f00 :....
使用 readelf -h
可以查看 ELF 文件头信息:
|
|
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: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1140 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 16
Section header string table index: 15
我们可以对照 /usr/include/elf.h
中的定义来看这些数据的含义:
|
|
并且可以看到 elf.h
中有很多宏定义, 并且带了注释, 可以很方便了解每个变量值的含义.
魔数
文件头的前 16 个字节是 魔数, 其中, 1-4 字节是 ELF 文件的标识码, 0x454c46 是 ‘ELF’ 的 ASCII 码. 第五个字节是文件类型, 0x01 表示 32 位, 0x02 表示 64 位. 第六个字节是大小端序, 0x01 是小端序, 0x02 是大端序. 第七个字节是 ELF 版本号, 一般为 0x01, 后面 9 个字节没有定义, 一般为 0. 有些平台会使用其作为拓展标志.
关于这些定义, 都可以在 elf.h
中找到:
|
|
文件类型
可以看到, 16 个字节之后, 接下来两个字节 (Elf32_Half
是 2 个字节, 可以在 elf.h
中找到定义) 表示文件类型, 并且可以找到变量值的含义:
|
|
很容易发现, 0x0100 表示可重定位文件, 0x0200 是可执行文件, 0x0300 是共享目标文件, 0x0400 是内核文件. (注意这里是小端序).
对照上方十六进制内容, 这两个位置是 0x0100, 也就是说, SimpleSection.o
是可重定位文件. 当然这些信息都在 readelf -h
中可以很方便地查看.
ELF 文件头的其他内容都可以用如上的方法来查看了解信息.
节表
目标文件中有很多个节, 这些节的信息由 节表 描述. 节表也被存储在 ELF 文件中, 且文件头中的 e_shoff
表述了节表的偏移量, 也就是节表从哪个地方开始. 节表用一种顺序的结构来存储, 就像是数组一样, 但是没有明着写成数组的形式, 因为节的个数是可变的, 而每个节的信息 (包括节名, 类型, 大小等) 的大小是固定的. 这样只需要用一个 “指针”, 从节表开始地址出发, 跳相应的倍数大小, 就可以找到各个表的信息了. (暂时还不清楚由于对齐导致的偏移, 这个 “指针” 会如何处理. 留个坑.)
我们可以根据上述方法, 找到 SimpleSection.o
的节表偏移, 第 32 个字节开始往后 4 个字节, 是 e_shoff
的值, 查看十六进制, 为 7404 0000 0000 0000
, 也就是开始地址为 0x0474 (还是注意小端序), 这和 readelf -h
得到的十进制数 1140 是一样的.
使用命令 readelf -S
来查看节表的内容, 也就是各个节的信息. objdump -h
也能查看各个节, 不过 readelf -S
看到的才是所有节, 比 objdump -h
多了一些信息表, 如重定向表 .rel.text
, .rel.eh_frame
, 符号表 .symtab, 字符串表 .strtab, 节名字符串表 .shstrtab.
|
|
There are 16 section headers, starting at offset 0x474:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .group GROUP 00000000 000034 000008 04 13 17 4
[ 2] .text PROGBITS 00000000 00003c 000087 00 AX 0 0 1
[ 3] .rel.text REL 00000000 00037c 000048 08 I 13 2 4
[ 4] .data PROGBITS 00000000 0000c4 000008 00 WA 0 0 4
[ 5] .bss NOBITS 00000000 0000cc 000004 00 WA 0 0 4
[ 6] .rodata PROGBITS 00000000 0000cc 000004 00 A 0 0 1
[ 7] .text.__x86.get_p PROGBITS 00000000 0000d0 000004 00 AXG 0 0 1
[ 8] .comment PROGBITS 00000000 0000d4 00002b 01 MS 0 0 1
[ 9] .note.GNU-stack PROGBITS 00000000 0000ff 000000 00 0 0 1
[10] .note.gnu.propert NOTE 00000000 000100 00001c 00 A 0 0 4
[11] .eh_frame PROGBITS 00000000 00011c 00007c 00 A 0 0 4
[12] .rel.eh_frame REL 00000000 0003c4 000018 08 I 13 11 4
[13] .symtab SYMTAB 00000000 000198 000150 10 14 14 4
[14] .strtab STRTAB 00000000 0002e8 000092 00 0 0 1
[15] .shstrtab STRTAB 00000000 0003dc 000095 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)
当然这里也可以用之前的人工查看十六进制的方法, 找到各个表的所有信息 — 不过这样很蠢.
至此, 我们根据文件头信息, 各个节的信息, 节表的信息, 就可以把目标文件 SimpleSection.o
的整个结构理清了 (有点长 QAQ):
这里就是整个文件的所有数据了. 整个文件大小 1780 个字节, 也就是上图所示的 0x000006f4.
仔细观察, 可以发现, 有一些内容中间有 1 个字节或者 3 个字节的空隙 (也可能是 2 个字节), 这是因为, 有一些变量需要对齐.
对齐
CPU 在读写数据的时候, 是以字为单位的. 也就是说, 在 32 位机器下, CPU 一次读写的数据大小是 4 个字节. 举个例子, 现在有一个 8 个字节的 double
存在了 0x00 - 0x08 处, 那么 CPU 可以取 0x00 - 0x04 和 0x04 - 0x08, 得到 double 的值. 但是, 如果这个 double
存在了 0x02 - 0x0a 处, 那么 CPU 就需要读取 0x00 - 0x04, 0x04 - 0x08, 0x08 - 0x0c 的数据, 这需要多一次的数据交换! 这样显然不利于 CPU 高效处理数据. 于是设计了对齐这种 用空间换时间 的方法. 简单来说, 就是强制要求 double
或者其他类型的数据, 存在能够用最少次数取出来的地址上. 用数学语言描述, 就是存放的地址必须是 4 的整数倍. 如果是 16 位机器, 那么就是 2 的整数倍, 64 位机器就是 8 的整数倍.
节的定义
阅读 elf.h
, 能够找到节的定义:
|
|
这里先解释一下一些简单的变量含义. sh_name
是节的名字, 所有节名以字符串的形式被存在 .shstrtab 中, sh_name
的值是这个节名在 .shstrtab 中的偏移. sh_addr
留坑. sh_offset
是该节在文件中的偏移 (不在文件中的节如 .bss 这个值就没有意义了). sh_entsize
留坑. sh_addralign
是地址是否需要对齐标志, 为 0 或 1 的时候表示不需要对齐, 其他则表示地址需要对齐到 $2^{sh\_addralign}$.
接下来再解释几个比较重要的变量.
sh_type
节的类型, 可以在 elf.h
中查看详细信息:
|
|
对照这些定义, 再回去看 readelf -S
查看到的内容, 可以发现, .data, .text, .rodata, .comment, .eh_frame 等节, 都是 PROGBITS
程序数据类型. .rel.text
和 .rel.eh_frame
是 REL
重定位表类型. 重定位表是链接时一个重要的内容. .symtab 是 SYMTAB
符号类型, .strtab 和 .shstrtab 是 STRTAB
字符串表类型.
其中, 节表第 0 个位置, 有一个 NULL
型, 解释是没有使用的项. 这里的数据全是 0. 查看其位置 (即节表偏移) 十六进制以验证:
|
|
0000474 0000 0000 0000 0000 0000 0000 0000 0000
*
000049c
-s 0x474 表示从偏移 0x474 开始输出, -n 40 表示输出 40 个字节的内容 (一个节占用空间是 40 个字节), 第二行的 * 表示都是 0, 直到 0x00049c
当我看到这个节的时候, 感觉这就是在浪费空间! 书上没有讲为什么会有一个空的节, 网上能够查到的也不多 (没有深入去搜索), 在 Oracle 关于 “链接与库 (Linker and Libraries Guide)” 的文档 里找到了这样的一句话:
大致意思就是说, 这里的空间可以作拓展的一个节, 或者 ELF 文件头的拓展数据. 具体什么情况会使用到暂时不清楚, 留坑.
sh_flag
节的标志, 表示节在虚拟地址空间中的属性, 比如可写, 可执行等. 同样在 elf.h
中可以看到所有属性:
|
|
对照 readelf -S
, 就可以看哪些节有哪些标志了.
其中, 执行过程中需要占用空间的节会有一个标志叫 ALLOC. 代码节, 数据节, .bss 等节, 程序运行时会载入到内存中, 所以他们有 ALLOC 标志. 而一些信息, 如 .comment, 其中的内容不需要载入内存, 所以没有 ALLOC 这个标志
sh_link 和 sh_info
这两个变量是链接相关的, 暂时跳过.
节 Part II
.strtab & .shstrtab
ELF 文件中的字符串用单独的表存起来, 使用的时候只需要一个下标就能找到字符串了.
.shstrtab 是 节表字符串表, 用来保存节表中用到的字符串, 如节名 sh_name
. 而 .strtab 是 字符串表, 保存其他地方用到的字符串.
ELF 文件头中有一个 e_shstrndx
, 表示节表字符串表在节表中的下标. readelf -h
可以查看:
|
|
Section header string table index: 15
可以看到, Section header string table index 的值为 15. 对比 readelf -S
:
|
|
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[15] .shstrtab STRTAB 00000000 0003dc 000095 00 0 0 1
调试信息
如果在编译的时候加上 -g
选项, 则会生成调试信息. 调试信息包含了代码及其对应的行号等等.
|
|
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 8] .debug_info PROGBITS 00000000 0000d4 0000ca 00 0 0 1
[ 9] .rel.debug_info REL 00000000 0006ec 000090 08 I 21 8 4
[10] .debug_abbrev PROGBITS 00000000 00019e 00009d 00 0 0 1
[11] .debug_aranges PROGBITS 00000000 00023b 000020 00 0 0 1
[12] .rel.debug_arange REL 00000000 00077c 000010 08 I 21 11 4
[13] .debug_line PROGBITS 00000000 00025b 00005e 00 0 0 1
[14] .rel.debug_line REL 00000000 00078c 000008 08 I 21 13 4
[15] .debug_str PROGBITS 00000000 0002b9 0000f4 01 MS 0 0 1
节 Part III
.rel
当用 readelf -S
查看目标文件时, 可以发现, 有两个打 .rel 开头的节:
|
|
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 3] .rel.text REL 00000000 00037c 000048 08 I 13 2 4
[12] .rel.eh_frame REL 00000000 0003c4 000018 08 I 13 11 4
可以看到, 他的类型是 REL. 翻阅 elf.h
可以找到:
|
|
REL 表示 重定位条目. 链接的时候, 有些节需要重定位, 而这里就存放了重定位的信息. 对于每个需要重定位的数据节或者代码节, 都会有相应的重定位条目. 具体什么时候需要重定位, 暂时没学到, 留坑.
当一个节是重定位条目时, 它会有一些附加信息. 首先它需要知道作用于哪个节, 比如 .rel.text 作用于 .text, 所以 .rel.text 这个节中的 sh_info
会保存 .text 节的下标. 而 sh_link
中会保存符号表的下标(因为链接的时候要用?).
对照 readelf -S
的结果来看, .rel.text 的 sh_info
是 2, 也就是 .text 的下标; .rel.eh_frame 的 sh_info
是 11, 也就是 .eh_frame 的下标. 二者的 sh_link
都是 13, 也就是符号表 .symtab 的下标.
.symtab
.symtab 节是符号表, 保存各个符号的信息. 所谓符号, 一般就是函数, 变量等, 也有一些特殊符号. 总之, 符号是为链接服务的, 可以看作是 “地址的标签”. 连接器正是根据符号才能进行正确的链接. 举个例子, 如果在某一个文件中定义了一个函数, 另一个文件中使用这个函数, 由于使用这个函数的文件没有该函数的代码, 目标文件中会创建一个函数的符号, 待链接时, 从其他文件中找到相同的符号, 然后链接起来.
查看目标文件的符号表可以用如下命令:
|
|
Symbol table '.symtab' contains 21 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c
2: 00000000 0 SECTION LOCAL DEFAULT 2
3: 00000000 0 SECTION LOCAL DEFAULT 4
4: 00000000 0 SECTION LOCAL DEFAULT 5
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000004 4 OBJECT LOCAL DEFAULT 4 static_var.1512
7: 00000000 4 OBJECT LOCAL DEFAULT 5 static_var2.1513
8: 00000000 0 SECTION LOCAL DEFAULT 7
9: 00000000 0 SECTION LOCAL DEFAULT 9
10: 00000000 0 SECTION LOCAL DEFAULT 10
11: 00000000 0 SECTION LOCAL DEFAULT 11
12: 00000000 0 SECTION LOCAL DEFAULT 8
13: 00000000 0 SECTION LOCAL DEFAULT 1
14: 00000000 4 OBJECT GLOBAL DEFAULT 4 global_init_var
15: 00000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
16: 00000000 50 FUNC GLOBAL DEFAULT 2 func1
17: 00000000 0 FUNC GLOBAL HIDDEN 7 __x86.get_pc_thunk.ax
18: 00000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
19: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
20: 00000032 85 FUNC GLOBAL DEFAULT 2 main
其中表中的第 0 个位置不存放信息(暂时不知道为啥), 并把符号类型设为 NOTYPE. readelf
没有显示节符号的名字, 这个符号名其实就是节名, 如 .text
查看 elf.h
, 可以找到符号表的定义:
|
|
其中, st_name
是符号名称, 实际上是一个下标, 通过这个下标去字符串表中索引真正的名称. st_info
包含了符号的信息, 低四位表示符号类型, 高 28 位表示符号绑定信息.
|
|
符号的绑定信息有如下主要的三种: 局部符号, 全局符号, 弱引用. 局部符号对外部文件不可见, 全局符号对外部文件可见. 弱引用在讲符号的时候再说. 观察 readelf
的输出可以看到, 局部变量, 节, 都是 LOCAL 的. 全局变量和函数都是 GLOBAL.
符号类型有对象(即变量), 函数, 节, 文件等. 表示该符号是什么样的符号. 根据符号类型的不同, st_value
也有不同的含义. 如果是函数或者对象, 并且不是 COMMON 块 (COMMON 块是啥还没学到), 这个值 在目标文件中 则表示该对象或函数的地址 相对包含该符号的节起始位置 的偏移. 而在可执行文件中, 则是符号的虚拟地址.
st_size
是符号的大小, 如一个 int 变量的大小是 4 个字节, 那么这个值就是 4. 一个函数的 st_size
也是该函数指令所占的字节数. st_shndx
是包含该符号的节在节表中的下标. 符号如果不在当前目标文件中定义, 则这个值有特殊的含义. 比如 COMMON 指的是 COMMON 的符号 (如未初始化的全局变量), ABS 指的是绝对符号, 如文件类型的符号就是一个绝对符号. UND 指的是未定义的符号, 也就是引用了这个符号, 需要后续链接器去找到其他文件中该符号的定义.