Linux 上源码编译后的 .o 文件即目标文件,目标文件结构上和可执行文件格式很相似,通过链接器链接相应的库后得到可执行文件 .elf。为了描述方便,文中不区分二者的存储格式。elf 存储格式涵盖了程序的编译、链接、装载和执行过程。了解目标文件的格式对认识操作系统,特别是进程加载方面大有裨益。那么目标文件包含什么东西呢?显而易见,应该包含会代码和数据,另外为了支持链接,其中还有符号表,为了支持调试还会有调试信息等等东西。
本文将以一个代码片段为例,深入分析 elf 文件中常用的各段。本文参考了 《程序员的自我修养》一书,感谢前人的付出。
Executable File /
Object File
------------------------
| File Header | <- 文件头,描述 elf 文件的属性...
------------------------
| .text section | <- 指令码
------------------------
| .data section | <- 初始化的非零全局变量和非零局部静态变量
------------------------
| .bss section | <- 未初始化或值为0的全局变量和为0的局部静态变量
------------------------
| other section | <- 其他段
------------------------
| String Tables | <- 字符串表
------------------------
| Symbol Tables | <- 符号表,用于链接
------------------------
| Section header table | <- 段头表,描述各个段的位置、大小等属性
------------------------
分段存储有很多好处:
后文针对 elf 文件的分析均以下面的代码为例。读者可以参照着实际体验,加深印象。
int printf(const char * format, ...);
// extern long __bss_start__[];
// extern long __bss_end__[];
int g_init_var = 0x1234; // .data
int g_uinit_var1; // .bss
int g_uinit_var2; // .bss
void func1(int i) // .text
{
printf("%x\n", i); // .text
}
int main() // .text
{
static int s_var = 0x1235; // .data
static int s_var2; // .bss
int a = 1;
int b;
func1(s_var + s_var2 + a + b);
// printf("%lx, %lx\n", __bss_start__, __bss_end__);
return 0;
}
# gcc version 7.2.1 20171011 (Linaro GCC 7.2-2017.11)
$ aarch-linux-gnu-gcc -c simpleSection.c
# 得到可重定位文件 simpleSection.o
$ aarch-linux-gnu-gcc simpleSection.c
# 得到可执行文件 a.out
elf 文件头描述了整个 elf 文件的属性。比如程序是 32位还是 位的,elf 文件的大小端,目标硬件平台,elf 文件的属性(可重定位文件、可执行文件等),还包括一个段头表(Section Header Table)信息,它描述了文件中各个段在文件中的偏移以及属性。
// /usr/include/elf.h
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf_Half e_type; /* Object file type */
Elf_Half e_machine; /* Architecture */
Elf_Word e_version; /* Object file version */
Elf_Addr e_entry; /* Entry point virtual address */
Elf_Off e_phoff; /* Program header table file offset */
Elf_Off e_shoff; /* Section header table file offset */
Elf_Word e_flags; /* Processor-specific flags */
Elf_Half e_ehsize; /* ELF header size in bytes */
Elf_Half e_phentsize; /* Program header table entry size */
Elf_Half e_phnum; /* Program header table entry count */
Elf_Half e_shentsize; /* Section header table entry size */
Elf_Half e_shnum; /* Section header table entry count */
Elf_Half e_shstrndx; /* Section header string table index */
} Elf_Ehdr;
elf 文件头信息如下:
这个 simpleSection.o 文件是小端的,类型是可重定位文件,目标机器是 AArch,段头表偏移位于文件 1112 (0x458)字节处。文件头本身占用 字节,每个段头占用 字节,共有 11 个段,各个段头名字组成的字符串形成了一个单独的段,它在所有段中的索引是 10,也就是最后一个段。
aarch-linux-gnu-readelf -h simpleSection.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: AArch
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1112 (bytes into file)
Flags: 0x0
Size of this header: (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: (bytes)
Number of section headers: 11
Section header string table index: 10
段头表描述了 ELF 文件包含的所有段的信息。每个段都是由下面的结构体所描述。
// /usr/include/elf.h
typedef struct
{
Elf_Word sh_name; /* Section name (string tbl index) */
Elf_Word sh_type; /* Section type */
Elf_Xword sh_flags; /* Section flags */
Elf_Addr sh_addr; /* Section virtual addr at execution */
Elf_Off sh_offset; /* Section file offset */
Elf_Xword sh_size; /* Section size in bytes */
Elf_Word sh_link; /* Link to another section */
Elf_Word sh_info; /* Additional section information */
Elf_Xword sh_addralign; /* Section alignment */
Elf_Xword sh_entsize; /* Entry size if section holds table */
} Elf_Shdr;
通过 readelf 工具可以获取到更详细的信息。
$ aarch-linux-gnu-readelf -S simpleSection.o
There are 11 section headers, starting at offset 0x458:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000074 0000000000000000 AX 0 0 4
[ 2] .rela.text RELA 0000000000000000 00000340
00000000000000c0 0000000000000018 I 8 1 8
[ 3] .data PROGBITS 0000000000000000 000000b4
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 0000000000000000 000000bc
0000000000000004 0000000000000000 WA 0 0 4
[ 5] .rodata PROGBITS 0000000000000000 000000c0
0000000000000004 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 000000c4
000000000000002e 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 000000f2
0000000000000000 0000000000000000 0 0 1
[ 8] .symtab SYMTAB 0000000000000000 000000f8
00000000000001e0 0000000000000018 9 14 8
[ 9] .strtab STRTAB 0000000000000000 000002d8
0000000000000065 0000000000000000 0 0 1
[10] .shstrtab STRTAB 0000000000000000 00000400
0000000000000052 0000000000000000 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)
可以看到段头表从偏移 0x458 开始,这和文件头中记录的偏移是一致的。第一个段是无效的段,类型是 NULL。
// 这里没有表达为了对齐而加的 padding
----------------- 0x00000000
| ELF Header |
----------------- 0x00000040
| .text |
----------------- 0x000000b4
| .data |
----------------- 0x000000bc
| .bss |
----------------- 0x000000c0
| .rodata |
----------------- 0x000000c4
| .comment |
----------------- 0x000000f2
|.note.GNU-stack|
----------------- 0x000000f8
| .symtab |
----------------- 0x000002d8
| .strtab |
----------------- 0x00000340
| .rela.text |
----------------- 0x00000400
| .shstrtab |
----------------- 0x00000458
| Section table |
----------------- 0x00000718
$ ls -l simpleSection.o
-rw-rw-r-- 1 xxx xxx 1816 Feb 9 10:39 simpleSection.o
这里要澄清一下,.bss 段看起来占用了4个字节,实际上就算继续增加未初始化的静态局部变量,.rodata 段的偏移还是 0x000000c0。具体的原因下文有分析。
# Display the full contents of all sections requested
$ aarch-linux-gnu-objdump -s simpleSection.o
simpleSection.o: file format elf-littleaarch
Contents of section .text:
0000 fd7bbea9 fd030091 a01f00b9 00000090 .{..............
0010 00000091 a11f40b9 00000094 1f2003d5 ......@...... ..
0020 fd7bc2a8 c0035fd6 fd7bbea9 fd030091 .{...._..{......
0030 20008052 a01f00b9 00000090 00000091 ..R............
0040 010040b9 00000090 00000091 000040b9 ..@...........@.
0050 2100000b a01f40b9 2100000b a01b40b9 !.....@.!.....@.
0060 2000000b 00000094 00008052 fd7bc2a8 ..........R.{..
0070 c0035fd6 .._.
Contents of section .data:
0000 34120000 35120000 4...5...
Contents of section .rodata:
0000 25780a00 %x..
Contents of section .comment:
0000 00474343 3a20284c 696e6172 6f204743 .GCC: (Linaro GC
0010 4320372e 322d3230 31372e31 31292037 C 7.2-2017.11) 7
0020 2e322e31 20323031 37313031 3100 .2.1 20171011.
# Display assembler contents of executable sections
$ aarch-linux-gnu-objdump -d simpleSection.o
...
Disassembly of section .text:
0000000000000000 <func1>:
0: a9be7bfd stp x29, x30, [sp, #-32]!
4: 910003fd mov x29, sp
8: b9001fa0 str w0, [x29, #28]
c: 90000000 adrp x0, 0 <func1>
...
# Displays the sizes of sections inside binary files
$ aarch-linux-gnu-size simpleSection.o
text data bss dec hex filename
120 8 4 132 84 simpleSection.o
.text 中存的是指令码,长度是 0x74 个字节和段表中记录的一致。不过为什么 size 工具打出来的要多四个字节 😃。
$ aarch-linux-gnu-size -A --common simpleSection.o
simpleSection.o :
section size addr
.text 116 0
.data 8 0
.bss 4 0
.rodata 4 0
.comment 46 0
.note.GNU-stack 0 0
*COM* 8 0
Total 186
原来是输出格式不同导致的。伯克利格式把 .rodata 的长度计入到 .text 段。
-A|-B --format={sysv|berkeley} Select output style (default is berkeley)。选择 System V 的格式查看,就能对应上了。
.data 存的是初始化了的全局变量和局部静态变量 0x1234 和 0x1235。
.rodata 存的是打印的格式化字符串 %x\n。
.comment 中存的是编译器的信息。
.bss 为未初始化的全局变量和局部静态变量预留了空间,elf 文件中并没有存数据,只是在段表中记录了 .bss 段的信息。
$ aarch-linux-gnu-objdump -t simpleSection.o
...
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 simpleSection.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000004 l O .data 0000000000000004 s_var.3113
0000000000000000 l O .bss 0000000000000004 s_var2.3114
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000004 g_init_var
0000000000000004 O *COM* 0000000000000004 g_uinit_var1
0000000000000004 O *COM* 0000000000000004 g_uinit_var2
0000000000000000 g F .text 0000000000000028 func1
0000000000000000 *UND* 0000000000000000 printf
0000000000000028 g F .text 000000000000004c main
.bss 段在 elf 文件中是不占用空间的,只在 .bss 段表中记录了变量的总大小,因为值都为 0,没必要在可执行文件中实际存储值,操作系统将在加载可执行文件的时候解析段大小的信息,然后为它分配内存。通过符号表可以看到只有局部静态变量放在了 .bss,而全局未初始化的变量是一个未定义的 COMMON 符号。这和编程语言和编译器实现相关,最终满足放在该段条件的会在链接成可执行文件时在 .bss 段分配空间,这么做的原因是链接过程涉及到符号的强与弱 (strong & weak symbol),需要在链接阶段做裁决,假如有其他的源文件定义了 int g_uinit_var1 = 1;。那么 g_uinit_var1 将是一个强符号,由于被初始化了将被放在 .data,而不是 .bss。
...
[23] .bss NOBITS 0000000000411038 00001038
0000000000000010 0000000000000000 WA 0 0 4
[24] .comment PROGBITS 0000000000000000 00001038
000000000000002d 0000000000000001 MS 0 0 1
...
# List symbols in files
$ aarch-linux-gnu-nm a.out
U abort@@GLIBC_2.17
0000000000411048 B __bss_end__
0000000000411048 B _bss_end__
0000000000411038 B __bss_start
0000000000411038 B __bss_start__
...
extern long __bss_start__[];
extern long __bss_end__[];
根据前面的描述,.shstrtab 是段头名称字符串组成的段,位于 0x400 的偏移处。而段头表位于 0x458 的偏移处。我们以 .text 段为例,它的下标是 1,即位于 0x458 + 0x40 = 0x498 偏移处。可以看到 {} 表示的即是 sh_name,它的含义是段名在 .shstrtab 中的偏移,显然 0x420 的位置刚好就是字符串 .text。
00000400: 00 2e 73 79 6d 74 61 62 00 2e 73 74 72 74 61 62 ..symtab..strtab
00000410: 00 2e 73 68 73 74 72 74 61 62 00 2e 72 65 6c 61 ..shstrtab..rela
00000420: 2e 74 65 78 74 00 2e 61 74 61 00 2e 62 73 73 .text..data..bss
00000430: 00 2e 72 6f 61 74 61 00 2e 63 6f 6d 6d 65 6e ..rodata..commen
00000440: 74 00 2e 6e 6f 74 65 2e 47 4e 55 2d 73 74 61 63 t..note.GNU-stac
00000450: 6b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 k...............
00000460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000490: 00 00 00 00 00 00 00 00 {20 00 00 00} 01 00 00 00 ........ .......
000004a0: 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
在链接的时候,函数和变量统称为符号,链接器需要符号信息来完成链接工作。除了函数和变量外,还有其他符号,如段名,行号信息等,这里不详述。前面提到了符号表,符号表也是一个段。其中记录了符号的名称、大小等信息。结构体如下:
typedef struct
{
Elf_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf_Section st_shndx; /* Section index */
Elf_Addr st_value; /* Symbol value */
Elf_Xword st_size; /* Symbol size */
} Elf_Sym;
$ aarch-linux-gnu-readelf -s simpleSection.o
Symbol table '.symtab' contains 20 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS simpleSection.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 NOTYPE LOCAL DEFAULT 3 $d
6: 0000000000000000 0 SECTION LOCAL DEFAULT 5
7: 0000000000000000 0 NOTYPE LOCAL DEFAULT 5 $d
8: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 $x
9: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 s_var.3113
10: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 s_var2.3114
11: 0000000000000000 0 NOTYPE LOCAL DEFAULT 4 $d
12: 0000000000000000 0 SECTION LOCAL DEFAULT 7
13: 0000000000000000 0 SECTION LOCAL DEFAULT 6
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 g_init_var
15: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM g_uinit_var1
16: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM g_uinit_var2
17: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 func1
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
19: 0000000000000028 76 FUNC GLOBAL DEFAULT 1 main
无论是目标文件、可执行文件、库甚至是核心转储(coredump),它们实际上都基于相似的格式。通过 elf 文件头,我们可以知道段头表的位置、每个段头的大小、段头字符串表的下标,根据这几个信息我们可以找到 elf 文件中所有段的信息,从而解析整个 elf 文件。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- 99spj.com 版权所有 湘ICP备2022005869号-5
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务