ELF文件格式
ELF文件(Executable Linkable Format)是一种文件存储格式。Linux下的目标文件和可执行文件都按照该格式进行存储,有必要做个总结。
概要
本文主要记录总结32位的Intel x86平台下的ELF文件结构。ELF文件以Section的形式进行存储。代码编译后的指令放在代码段(Code Section),全局变量和局部静态变量放到数据段(Data Section)。文件以一个“文件头”开始,记录了整个文件的属性信息。
未链接的目标文件结构
SimpleSection.c
int printf(const char* format, ...);int global_init_var = 84;
int global_uniit_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;
}
对于上面的一段c代码将其编译但是不链接。gcc -c -m32 SimpleSection.c
( -c表示只编译不链接,-m32表示生成32位的汇编)得到SimpleSection.o。可以用objdump或readelf命令查看目标文件的结构和内容。
ELF文件头
可以用readelf -h
查看文件头信息。执行readelf -h SimpleSection.o
后:
root@DESKTOP-2A432QS:~/c# readelf -h SimpleSection.o
ELF Header:Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32Data: 2's complement, little endianVersion: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: REL (Relocatable file)Machine: Intel 80386Version: 0x1Entry point address: 0x0Start of program headers: 0 (bytes into file)Start of section headers: 832 (bytes into file)Flags: 0x0Size of this header: 52 (bytes)Size of program headers: 0 (bytes)Number of program headers: 0Size of section headers: 40 (bytes)Number of section headers: 13Section header string table index: 10
程序头包含了很多重要的信息,每个字段的含义可参考ELF结构文档。主要看下:
- Entry point address:程序的入口地址,这是没有链接的目标文件所以值是0x00
- Start of section headers:段表开始位置的首字节
- Size of section headers:段表的长度(字节为单位)
- Number of section headers:段表中项数,也就是有多少段
- Start of program headers:程序头的其实位置(对于可执行文件重要,现在为0)
- Size of program headers:程序头大小(对于可执行文件重要,现在为0)
- Number of program headers:程序头中的项数,也就是多少Segment(和Section有区别,后面介绍)
- Size of this header:当前ELF文件头的大小,这里是52字节
段表及段(Section)
段表
ELF文件由各种各样的段组成,段表就是保存各个段信息的结构,以数组形式存放。段表的起始位置,长度,项数分别由ELF文件头中的Start of section headers,Size of section headers,Number of section headers指出。使用readelf -S SimpleSection.o
查看SimpleSection.o的段表如下:
There are 13 section headers, starting at offset 0x340: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 000062 00 AX 0 0 1[ 2] .rel.text REL 00000000 0002a8 000028 08 I 11 1 4[ 3] .data PROGBITS 00000000 000098 000008 00 WA 0 0 4[ 4] .bss NOBITS 00000000 0000a0 000004 00 WA 0 0 4[ 5] .rodata PROGBITS 00000000 0000a0 000004 00 A 0 0 1[ 6] .comment PROGBITS 00000000 0000a4 000036 01 MS 0 0 1[ 7] .note.GNU-stack PROGBITS 00000000 0000da 000000 00 0 0 1[ 8] .eh_frame PROGBITS 00000000 0000dc 000064 00 A 0 0 4[ 9] .rel.eh_frame REL 00000000 0002d0 000010 08 I 11 8 4[10] .shstrtab STRTAB 00000000 0002e0 00005f 00 0 0 1[11] .symtab SYMTAB 00000000 000140 000100 10 12 11 4[12] .strtab STRTAB 00000000 000240 000065 00 0 0 1
Key to Flags:W (write), A (alloc), X (execute), M (merge), S (strings)I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)O (extra OS processing required) o (OS specific), p (processor specific)
总共有13个Section,重点关注.text, .data, .rodata, .symtab, .rel.text段。
代码段
.text段保存代码编译后的指令,可以用objdump -s -d SimpleSection.o
查看SimpleSection.o代码段的内容。
SimpleSection.o: file format elf32-i386Contents of section .text:0000 5589e583 ec0883ec 08ff7508 68000000 U.........u.h...0010 00e8fcff ffff83c4 1090c9c3 8d4c2404 .............L$.0020 83e4f0ff 71fc5589 e55183ec 14c745f0 ....q.U..Q....E.0030 01000000 8b150400 0000a100 00000001 ................0040 c28b45f0 01c28b45 f401d083 ec0c50e8 ..E....E......P.0050 fcffffff 83c4108b 45f08b4d fcc98d61 ........E..M...a0060 fcc3 ..
...省略 Disassembly of section .text:00000000 <func1>:0: 55 push %ebp1: 89 e5 mov %esp,%ebp3: 83 ec 08 sub $0x8,%esp6: 83 ec 08 sub $0x8,%esp9: ff 75 08 pushl 0x8(%ebp)c: 68 00 00 00 00 push $0x011: e8 fc ff ff ff call 12 <func1+0x12>16: 83 c4 10 add $0x10,%esp19: 90 nop1a: c9 leave 1b: c3 ret 0000001c <main>:1c: 8d 4c 24 04 lea 0x4(%esp),%ecx20: 83 e4 f0 and $0xfffffff0,%esp23: ff 71 fc pushl -0x4(%ecx)26: 55 push %ebp27: 89 e5 mov %esp,%ebp29: 51 push %ecx2a: 83 ec 14 sub $0x14,%esp2d: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp)34: 8b 15 04 00 00 00 mov 0x4,%edx3a: a1 00 00 00 00 mov 0x0,%eax3f: 01 c2 add %eax,%edx41: 8b 45 f0 mov -0x10(%ebp),%eax44: 01 c2 add %eax,%edx46: 8b 45 f4 mov -0xc(%ebp),%eax49: 01 d0 add %edx,%eax4b: 83 ec 0c sub $0xc,%esp4e: 50 push %eax4f: e8 fc ff ff ff call 50 <main+0x34>54: 83 c4 10 add $0x10,%esp57: 8b 45 f0 mov -0x10(%ebp),%eax5a: 8b 4d fc mov -0x4(%ebp),%ecx5d: c9 leave 5e: 8d 61 fc lea -0x4(%ecx),%esp61: c3 ret
可以看到.text段里保存的正是func1()和main()的指令。
数据段和只读数据段
.data段保存的是已经初始化了的全局静态变量和局部静态变量。前面SimpleSection.c中的global_init_varabal和static_var正是这样的变量。使用objdump -x -s -d SimpleSection.o
查看:
Contents of section .data:0000 54000000 55000000 T...U...
Contents of section .rodata:0000 25640a00 %d..
最左边的0000是偏移,不用看,后面跟着的0x00000054和0x00000055正是global_init_varabal和static_var的初始值。
.rodata段存放的是只读数据,包括只读变量(const修饰的变量和字符串常量),这个例子中保存了"%d\n"正是调用printf的时候使用的字符常量。
符号表段
符号表段一般叫做.symtab,以数组结构保存符号信息(函数和变量),对于函数和变量符号值就是它们的地址。主要关注两类符号:
- 定义在目标文件中的全局符号,可以被其他目标文件引用,比如SimpleSction.o里面的func1, main和global_init_var。
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,比如pritnf。
可以用readelf -s SimpleSection.o
查看SimpleSection.o的符号:
Symbol table '.symtab' contains 16 entries:Num: Value Size Type Bind Vis Ndx Name0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS SimpleSection.c2: 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.14887: 00000000 4 OBJECT LOCAL DEFAULT 4 static_var2.14898: 00000000 0 SECTION LOCAL DEFAULT 7 9: 00000000 0 SECTION LOCAL DEFAULT 8 10: 00000000 0 SECTION LOCAL DEFAULT 6 11: 00000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var12: 00000004 4 OBJECT GLOBAL DEFAULT COM global_uniit_var13: 00000000 28 FUNC GLOBAL DEFAULT 1 func114: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf15: 0000001c 70 FUNC GLOBAL DEFAULT 1 main
可以看到:
- func1和main的Ndx对应的值是1,表示在.text段(.text段在段表中的索引是1),类型是FUNC,value分别是0x00000000和0x0000001c,表明这两个函数指令字节码的首字节分别在.text段的0x00000000和0x0000001c偏移处。
- printf的Ndx是UND,表明这个符号没有在SimpleSection.o中定义,仅仅是被引用。
- global_init_var和static_var.1488两个符号的Ndx都是3,说明他们被定义在数据段,value分别是0x00000000和0x00000004,表示这个符号的位置在数据段的0x00000000和0x00000004偏移处,翻看上一节
Contents of section .data:0000 54000000 55000000 T...U...
数据段0x00000000和0x00000004偏移处保存的正是global_init_var和static_var这两个变量。
重定位表段
重定位表也是一个段,用于描述在重定位时链接器如何修改相应段里的内容。对于.text段,对应的重定位表是.rel.text表。使用objdump -r SimpleSection.o
查看重定位表。
SimpleSection.o: file format elf32-i386RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000d R_386_32 .rodata
00000012 R_386_PC32 printf
00000036 R_386_32 .data
0000003b R_386_32 .bss
00000050 R_386_PC32 func1
printf对应的那行的OFFSET为0x00000012,表明.text段的0x00000012偏移处需要修改。我们objdump -s -d SimpleSection.o
查看代码段的0x00000012偏移,发现是”fc ff ff ff“是call指令的操作数。
00000000 <func1>:0: 55 push %ebp1: 89 e5 mov %esp,%ebp3: 83 ec 08 sub $0x8,%esp6: 83 ec 08 sub $0x8,%esp9: ff 75 08 pushl 0x8(%ebp)c: 68 00 00 00 00 push $0x011: e8 fc ff ff ff call 12 <func1+0x12>16: 83 c4 10 add $0x10,%esp19: 90 nop1a: c9 leave 1b: c3 ret
也就是说,在没有重定位前call指令的操作”fc ff ff ff“是无效的,需要在重定位过程中进行修正。func1那行也同理。
总结
ELF文件结构可以用下面的图表示:
可执行程序结构
和未链接的ELF文件结构一样,只不过引入了Segment的概念(注意和Section进行区分)。Segment本质上是从装载的角度重新划分了ELF的各个段。目标文件链接成可执行文件时,链接器会尽可能把相同权限属性的段(Section)分配到同一Segment。Segment结构的起始位置,项数,大小分别由ELF头中的Size of program headers,Number of program headers, Size of this header字段指定。
参考资料:
- 《程序员的自我修养》第3,6章
- ELF结构文档
posted @ 2018-10-07 15:12 gatsby123 阅读(...) 评论(...) 编辑 收藏
刷新评论刷新页面返回顶部
Copyright © 2020 gatsby123
Powered by .NET Core on Kubernetes