前言
参考书籍第 3 章
笔记
ELF 中的段介绍:
.text: 代码段,存储二进制的机器指令,这些指令可以被机器直接执行
.rodata: 只读数据段,存储程序中使用的复杂常量,例如字符串等。
.data: 数据段,存储程序中已经被明确初始化的全局数据,包括C语言中的全局变量和静态变量,如果这些全局
数据被初始化为0,则不存储在数据段中,而是存储在块数据段中。C语言局部数据保存在栈中,不出现
在数据段中。
.bss: 块数据段,存储未被明确初始化的全局数据,在目标文件中,这个段并不占有实际空间,而仅仅是一个占
位符,以告知指定位置上应当预留全局数据的空间,块缓存段存在的原因是为了提高磁盘空间的利用率。
.rodata1: Read only Data, 这种段里存放的是只读数据,比如字符串常量,全局 const 变量。跟 .rodata 一样
.comment: 存放的是编译器版本信息,比如字符串: "GCC(GNU)4.2.0"
.debug: 调试信息
.dynamic: 动态链接信息
.hash: 符号哈希表
.line: 调试时的行号表,即源代码行号与编译后指令的对应表
.note: 额外的编译器信息,比如程序的公司名,发布版本号等
.strtab: String Table, 字符串表,用于存储 ELF 文件中用到的各种字符串
.symtab: Symbol Table.符号表
.shstrtab: Section Header String Table. 段表字符串表, 用来保存段表中用到的字符串,最常见的就是段名。
.rel.段名: 重定位段,如 .rel.text 根据名字他就是 .text 的重定位段
.plt
.got: 动态链接的跳转表和全局入口表
.init
.fini: 程序初始化与终结代码段
自定义段:
GCC 提供了一个扩展机制,使得程序员可以指定变量所处的段:
__attribute__((section(*FOO))) int global = 42;
__attribute__((section("BAR"))) void foo()
{
}
我们在全局变量或函数之前加上 __attribute__((section("name"))) 属性就可以把相应的变量
或函数放到以 "name" 作为段名的段中
测试程序如下:
// simplesection.c
int printf(const char *formt,...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n",i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 +a + b);
return a;
}
一. 使用 gcc 编译这个文件,生成中间目标文件, -c 只编译不链接
gcc -c simplesection.c
会生成对应的 simplesection.o 文件。
使用 objdump -h simplesection.o 查看编译出来的文件结构和内容
-x, --all-headers Display the contents of all headers
-h, --[section-]headers Display the contents of the section headers
$ objdump -h simplesection.o
simplesection.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000050 00000000 00000000 00000034 2**2 // 代码段
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE // CONTENTS: 表示该段在文件中存在
1 .data 00000008 00000000 00000000 00000084 2**2 // 数据段:初始化了的全局静态变量和局部静态变量
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 00000000 00000000 0000008c 2**2 // BSS(Block Started by Symbol)未初始化全局变量,局部静态变量段
ALLOC // ALLOC: 运行时分配的,文件中并不存存储的
3 .rodata 00000004 00000000 00000000 0000008c 2**0 // 只读数据段
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000024 00000000 00000000 00000090 2**0 // 注释信息段
CONTENTS, READONLY
5 .note.GNU-stack 00000000 00000000 00000000 000000b4 2**0 // 堆栈提示段
CONTENTS, READONLY
它们在 ELF 中布局如下:
------------------------------ 0x0000 0440(文件大小)
| Other data |
| |
-------------------------------------- 0x0000 00bc
| .comment |
0x24 | |
| |
-------------------------------------- 0x0000 0090
| .rodata |
0x04 | |
| |
-------------------------------------- 0x0000 008c
| .data |
0x08 | |
| |
-------------------------------------- 0x0000 0084
| .text |
0x50 | |
| |
-------------------------------------- 0x0000 0034
| ELF Header |
0x34 | |
| |
-------------------------------------- 0x0000 0000
Linux 中有一个专门的命令 size 可以用来查看 ELF 文件的代码段、数据段和 BSS 段的长度
其中 dec 表示 3 个段长度的和的 十进制,hex 表示长度和的十六进制
$ size simplesection.o
text data bss dec hex filename
84 8 4 96 60 simplesection.o
二、对各个段内容的解析:
objdump 的 -s 参数可以将所有段的内容以十六进制的方式打印出来,-d 参数可以将所有包含指令的段反汇编
$ objdump -s -d simplesection.o
simplesection.o: file format elf32-i386
// 代码段
Contents of section .text:
// 偏移量 十六进制内容 .text 段的 AScII 码形式
0000 5589e583 ec188b45 08894424 04c70424 U......E..D$...$
0010 00000000 e8fcffff ffc9c355 89e583e4 ...........U....
0020 f083ec20 c744241c 01000000 8b150400 ... .D$.........
0030 0000a100 0000008d 04020344 241c0344 ...........D$..D
0040 24188904 24e8fcff ffff8b44 241cc9c3 $...$......D$...
// 数据段
Contents of section .data:
0000 54000000 55000000 T...U... // 初始化了的全局静态变量和局部静态变量
// 这里对应程序的 global_uninit_var 和 static_var
// 每变量 4 字节,正好 8 字节
// 0x54 = 84 0x55 = 85
// 只读数据段
Contents of section .rodata:
0000 25640a00 %d.. // 调用 printf 时,用到的那个字符串常量 "%d\n"
// 这个段的 4 个字节正好是这个字符串常量的 ASCII
// 字节序,最后以 \0 结尾
// 注释信息段
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520342e .GCC: (Ubuntu 4.
0010 342e312d 34756275 6e747539 2920342e 4.1-4ubuntu9) 4.
0020 342e3100 4.1.
// 代码段反汇编内容
Disassembly of section .text:
00000000 <func1>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 18 sub $0x18,%esp
6: 8b 45 08 mov 0x8(%ebp),%eax
9: 89 44 24 04 mov %eax,0x4(%esp)
d: c7 04 24 00 00 00 00 movl $0x0,(%esp)
14: e8 fc ff ff ff call 15 <func1+0x15>
19: c9 leave
1a: c3 ret
0000001b <main>:
1b: 55 push %ebp
1c: 89 e5 mov %esp,%ebp
1e: 83 e4 f0 and $0xfffffff0,%esp
21: 83 ec 20 sub $0x20,%esp
24: c7 44 24 1c 01 00 00 movl $0x1,0x1c(%esp)
2b: 00
2c: 8b 15 04 00 00 00 mov 0x4,%edx
32: a1 00 00 00 00 mov 0x0,%eax
37: 8d 04 02 lea (%edx,%eax,1),%eax
3a: 03 44 24 1c add 0x1c(%esp),%eax
3e: 03 44 24 18 add 0x18(%esp),%eax
42: 89 04 24 mov %eax,(%esp)
45: e8 fc ff ff ff call 46 <main+0x2b>
4a: 8b 44 24 1c mov 0x1c(%esp),%eax
4e: c9 leave
4f: c3 ret
// 函数栈中的数据位图示意图
------------------------- -----
| 参数 | |
------------------------- |
| 返回地址 | |
ebp----->------------------------- |
| Old EBP | |
------------------------- 活动记录
| 保存的寄存器 | |
------------------------- |
| 局部变量 | |
------------------------- |
| 其他数据 | |
esp----->------------------------- ------
3.4 ELF 文件结构描述
ELF 文件结构:
------------------------------------
| ELF Header |
------------------------------------
| .text |
------------------------------------
| .data |
------------------------------------
| .bss |
------------------------------------
| … |
------------------------------------
| other sections |
------------------------------------
| Section Header table |
------------------------------------
| String Tables |
------------------------------------
| Symbol Tables |
------------------------------------
还是以上面的程序为例子:
1. 读取文件头
$ readelf -h simplesection.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 // ELF 魔数
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: 264 (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: 11
Section header string table index: 8
文件头定义在 /usr/include/elf.h
typedef struct
{
unsigned char e_ident[EI_NIDENT]; // Magic number and other info
// 16 个字节:
// 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
// ELF 文件魔数 ELF 文件类型 字节序 版本号 这 9 个字节未定义,可用于扩展
// 0x01: 32 位的 0x01: 小端 一般为 1
// 0x02: 64 位的 0x02: 大端 只出到 1.2
Elf32_Half e_type; // ELF 文件类型
// ET_REL: 1 可重定位文件,一般为 .o 文件
// ET_EXEC: 2 可执行文件
// ET_DYN: 3 共享目标文件,一般为 .so 文件
Elf32_Half e_machine; // ELF 文件的 CPU 平台属性,相关常量以 EM_ 开头
// EM_M32: 1 AT&T WE 32100
// EM_SPARC: 2 SPARC
// EM_386: 3 Intel x86
// EM68K: 4 Motorola 68000
// EM88K: 5 Motorola 88000
// EM_860: 6 Intel 80860
Elf32_Word e_version; // ELF 版本号,一般为常数 1
Elf32_Addr e_entry; // 入口地址,规定 ELF 程序的入口虚拟地址,操作系统在加载完该程序
// 后从这个地址开始执行进程的命令,可重定义文件一般没有入口地
// 址,则这个值为 0
Elf32_Off e_phoff; // Program header table file offset
Elf32_Off e_shoff; // 段表在文件中的偏移
Elf32_Word e_flags; // ELF 标志位,用来标识一些 ELF 文件平台相关属性。相关常量的格式,
// 一般为 EF_machine_flag, machine 为平台,flag 为标志
Elf32_Half e_ehsize; // ELF 文件头本身的大小
Elf32_Half e_phentsize; // Program header table entry size
Elf32_Half e_phnum; // Program header table entry count
Elf32_Half e_shentsize; // 段表描述符的大小,一般等于 sizeof(Elf32_Shdr)
Elf32_Half e_shnum; // 段表描述符数量,这个值等于 ELF 文件中拥有的段的数量
Elf32_Half e_shstrndx; // 段表字符串表所在的段在段表中的下标,段表字符串表即 .shstrtab
// 也是 ELF 文件中的一个普通段,这个值就是表示在在段表中
// 下标,例如 Section header string table index: 8
// 则表示在 .shstrtab 段表 8 中 [ 8] .shstrtab
} Elf32_Ehdr;
2. 段表
段表是 ELF 文件中除了文件头以外最重要的结构,它描述了 ELF 的各个段的信息,比如每个段的段名、段的长度、
在文件中的偏移、读写权限及段的其它属性。ELF 文件的段的结构就是由段表决定的,编译器、链接器和装载器都是依
靠段表来定位和访问各个段的属性的。段表在 ELF 文件中的位置由 ELF 文件头的 e_shoff 成员决定的。
前面我们使用了 objdump -h 来查看 ELF 文件中包含的段,实际上这个命令只是把 ELF 关键的段显示了出来,而
省略了其他辅助性的段,比如:符号表、字符串表、段名字符串表、重定位表等。我们可以使用 readelf 工具来查看
ELF 文件的段,它显示出来的结果才是真正的段表结构:
$ readelf -S simplesection.o
There are 11 section headers, starting at offset 0x108:
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 00000000 000034 000050 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000418 000028 08 9 1 4
[ 3] .data PROGBITS 00000000 000084 000008 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 00008c 000004 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 00008c 000004 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 000090 000024 01 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 0000b4 000000 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 0000b4 000051 00 0 0 1
[ 9] .symtab SYMTAB 00000000 0002c0 0000f0 10 10 10 4
[10] .strtab STRTAB 00000000 0003b0 000066 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
段表在 elf.h 中是以 Elf32_Shdr 数组形式保存的,对应的 Elf32_Shdr 结构体为:
typedef struct
{
Elf32_Word sh_name; // 段名是个字符串,它位于一个叫做 ".shstrtab" 的字符串表。
// sh_name 是段名字符串在 .shstrtab 中的偏移
Elf32_Word sh_type; // 段的类型
Elf32_Word sh_flags; // 段的标志位
Elf32_Addr sh_addr; // 如果该段可以被加载,则 sh_addr 为该段被加载后在进程地址空间的
// 虚拟地址,否则 sh_addr 为 0
Elf32_Off sh_offset; // 如果该存在于文件中,则表示该段在文件中的偏移,否则无意义,比如
// sh_offset 对于 BSS 段来说就没有意义
Elf32_Word sh_size; // 段的长度
Elf32_Word sh_link; // Link to another section
Elf32_Word sh_info; // Additional section information
Elf32_Word sh_addralign; // 段地址对齐,有些段对段地址对齐有要求,比如我们假设有个段刚开始
// 的位置包含了一个 double 变量。因为 Intel x86 系统要求浮点
// 数的存储地址必须是本身的整数倍,即 double 必须 8 字节对齐
// 这里写的是对齐的指数,即 2^sh_addralign ,0或1表示该没有
// 对齐要求
Elf32_Word sh_entsize; // 项的长度,有些段包含了一些固定大小的项,比如符号表,它包含的每
// 个符号所占的大小都是一样的。对于这种段,sh_entsize 表示
// 每个项的大小,如果为 0,则表示该段不包含固定大小的项
} Elf32_Shdr;
simplesection.o 的 Section Table 及所有段的位置和长度:
---------------------------- 0x0000 0000
0x34| ELF Header | ==>Start of section headers: 264 (bytes into file)
---------------------------- 0x0000 0034 |
0x50| .text | |
---------------------------- 0x0000 0084 |
0x84| .data | |
---------------------------- 0x0000 008c |
0x04| .bss | |
---------------------------- 0x0000 008c |
0x04| .rodata | |
---------------------------- 0x0000 0090 |
0x24| .comment | |
---------------------------- 0x0000 00b4 |
0x00| .note.GNU-stack | |
---------------------------- 0x0000 00b4 |
0x51| .shstrtab | |
---------------------------- 0x0000 0108 <------------------------------ ELF 头中指定段表位移
0x1b8| Section table | 段表长度 = 段表描述符的大小 * 段表描述符数量 = 40*11 = 480 = 0x1b8
---------------------------- 0x0000 02c0
0xf0| .symtab |
---------------------------- 0x0000 03b0
0x66| .strtab |
---------------------------- 0x0000 0416
---------------------------- 0x0000 0418 这里是因为对齐的原因,与前面的段有两个字节的间隔
0x28| .rel.text |
---------------------------- 0x0000 0440(文件大小)
3.5 ELF 链接的接口 ---- 符号
链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table), 这个表里面记录
了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号
值就是它们的地址。除了函数和变量之外,还存在其他几种不常用到的符号。我们将符号表中的所有符号进行分类,它们有可
能是下面这些类型中的一种:
1. 定义在本目标文件的全局符号,可以被其他目标文件引用。比如 simplesection.o 里面的 func1、main 和 global_init_var
2. 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(External Symbol),也就是
符号引用,比如 simplesection.o 里面的 printf
3. 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如 simplesection.o 里面的 .text .data 等。
4. 局部符号,这类符号只在编译单元内部可见。比如 simplesection.o 里面的 static_var 和 static_var2。调试
器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也
忽略它们。
5. 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。
可以使用 nm 查看 simplesection.o 的符号表:
$ nm simplesection.o
00000000 T func1
00000000 D global_init_var
00000004 C global_uninit_var
0000001b T main
U printf
00000004 d static_var.1255
00000000 b static_var2.1256
1. ELF 符号表的结构
ELF 文件中的符号表往往是谁的中的一个段,段名一般叫 .symtab,符号表的结构很简单,它是一个 Elf32_Sym 结构
的数组,定义如下:
typedef struct
{
Elf32_Word st_name; // 符号名,这个成员包含了该名在字符串表中的下标
Elf32_Addr st_value; // 符号相对应的值,这个值跟符号有关,可能是一个绝对值,也可能是一个
// 地址等,不同的符号,它所对应的值含义不同,它分以下情况:
// 1. 在目标文件中,如果是符号的定义并且该符号不是 COMMON 块
// 类型的,即 st_shndx 不是 SHN_COMMON,则 st_value 表示
// 该符号在段中的偏移。即符号所对应的函数或变量位于由
// st_shndx 指定的段,偏移 st_value 的位置。这也是目标
// 文件中定义全局变量的符号的最后常见情况,比如 simplesection.o
// 中的 func1、main 和 global_init_var
// 2. 在目标文件中,如果符号是 COMMON 块 类型的,则 st_value
// 表示该符号的对齐属性,比如 simplesection.o 中的 global_uninit_var
// 3. 在可执行文件中,st_value 表示符号的虚拟地址。这个虚拟
// 对于动态链接器十分有用
Elf32_Word st_size; // 符号大小,对于包含数据的符号,这个值是该数据类型的大小,比如一个
// double 型的符号它占用 8 个字节。如果该值为 0 ,则表示该符号
// 大小为 0 或未知
unsigned char st_info; // 符号类型和绑定信息,该成员低 4 位表示符号类型,高 28 位表示符号
// 符号绑定信息
// 符号的类型:
// STB_LOCAL: 0 局部符号,对于目标文件的外部不可见
// STB_GLOBAL: 1 全局符号,外部可见
// STB_WEAK: 2 弱引用
// 符号的绑定信息:
// STT_NOTYPE: 0 未知类型符号
// STT_OBJECT: 1 该符号是个数据对象,比如变量、数组等
// STT_FUNC: 2 该符号是个函数或其他可执行代码
// STT_SECTION:3 该符号表示一个段,这种符号发须是 STB_LOCAL 的
// STT_FILE: 4 该符号表示文件名,一般都是该目李海霞文件所对应
// 的源文件名,它一定是 STB_LOCAL 类型的,并
// 且它的 st_shndx 一定是 SHN_ABS
unsigned char st_other; // Symbol visibility 该成员目前为 0,没用
Elf32_Section st_shndx; // 符号所在的段, 如果符号定义在本目标文件中,那么这个成员表示符号所在的
// 段在段表中的下标,但是如果符号不是定义在本目标文件中,或者对于有
// 些特殊符号,sh_shndx 的值有些特殊:
// SHN_ABS: 0xfff1 表示该符号包含了一个绝对的值。比如表示文件
// 名的符号就属于这种类型的
// SHN_COMMON: 0xfff2 表示该符号是一个 COMMON 块 类型的符号,
// 一般来说,未初始化的全局符号定义就是这种类
// 型的,比如 simplesection.o 里面的 global_uninit_var
// SHN_UNDEF: 0 表示该符号未定义,这个符号表示该符号在本目标
// 文件被引用到,但是定义在其他目标文件中
} Elf32_Sym;
上面了解了 ELF 文件的符号表,下面以 simplesection.o 里面的符号为例子,分析各个符号在符号表中的状态
$ readelf -s simplesection.o
Symbol table '.symtab' contains 15 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 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000004 4 OBJECT LOCAL DEFAULT 3 static_var.1255 // 两个静态变量,只是编译
7: 00000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1256 // 单元内部可见
8: 00000000 0 SECTION LOCAL DEFAULT 7
9: 00000000 0 SECTION LOCAL DEFAULT 6
10: 00000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var // 在 .bss 段,下标为 3
11: 00000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var // 定义未初始化,不在 bss
12: 00000000 27 FUNC GLOBAL DEFAULT 1 func1 // .text 段表下标为 1
13: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf // 引用未定义
14: 0000001b 53 FUNC GLOBAL DEFAULT 1 main // .text 段表下标为 1