在Linux系统的可执行文件(ELF文件)中,开头是一个文件头,用来描述程序的布局,整个文件的属性等信息,包括文件是否可执行、静态还是动态链接及入口地址等信息;
程序文件中包含了程序头,程序的入口地址等信息不需要写死,调用代码可以通用,根据实际情况加载;此时的文件不是纯碎的二进制可执行文件了,因为包含的程序头不是可执行代码;将这种包含程序头的文件读入到内存后,从程序头中读取入口地址,跳转到入口地址执行;
目录
[TOC]
0. 简介
0.1 文件格式
linux系统中用C语言编写完代码,使用编译器gcc编译生成的是ELF格式的二进制文件;
ELF指:Executable and Linkable Format,可执行链接格式;本文中的目标文件指各种类型符合ELF规范的我呢见,如:二进制可执行文件、Linux下的.o目标文件和.so动态库文件;
可执行文件(Executable file):经过编译链接后,可以直接运行的程序文件;
共享目标文件(Shared object file):动态链接库,在可执行文件被加载的过程中动态链接,成为程序代码的一部分;
可重定位文件(Relocatable file):可重定位文件即目标文件,是源文件编译后但未完成链接的半成品,被用于与其他目标文件合并链接,以构建出二进制可执行文件或动态链接库;
核心转储文件(Core dump file):当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些信息转储到核心转储文件;
0.2 段和节
程序中的段(Segment)和节(Secton)是真正的程序体;段包括代码段和数据段等,段是由节组成的,多个节经过链接后被合并为一个段;
段和节的信息用header描述,程序头是program header,节头是section header;段和节的大小和数量都是不固定的,需要用专门的数据结构来描述,即程序头表(program header table)和节头表(section header table),这是两个数组,元素分别是程序头(program header)和节头(section header);程序头表(program header table)中的元素全是程序头(program header),节头表(section header table)中的元素全是节头(section header);程序头表是用来描述段(Segment)的,成为段头表;段是程序本身的组成部分;
由于段和节的大小和数量都是不固定的,程序头表和节头表的大小也不固定,两个表在程序文件中的位置也不固定;需要在一个固定的位置,用一个固定大小的数据结构来描述程序头表和节头表的大小和位置信息,即位于文件最开始部分的ELF header;
1. ELF header
ELF文件分为文件头和文件体两部分;先用ELF header从文件全局概要出程序中程序头表、节头表的位置和大小等信息;然后从程序头表和节头表中分别解析出各个段和节的位置和大小等信息;
可执行文件和待重定位文件,文件最开头的部分是ELF header;程序头表对于可执行文件是必须的,而对于待重定位文件是可选的;
ELF文件头用Elf32_Ehdr和Elf64_Ehdr结构体表示;
1 | // include/uapi/linux/elf.h |
成员 | 大小(32位) | 大小(64位) | 说明 |
---|---|---|---|
e_ident | 16 | 16 | 表示ELF字符等信息,开头四个字节是固定不变的elf文件魔数,0x7f 0x45 0x4c 0x46;可用于确认文件类型是否正确; |
e_type | 2 | 2 | ELF目标文件的类型; |
e_machine | 2 | 2 | ELF目标文件的体系结构类型,即要在哪种硬件平台运行; |
e_version | 4 | 4 | 版本信息; |
e_entry | 4 | 8 | 操作系统运行该程序时,将控制权转交到的虚拟地址; |
e_phoff | 4 | 8 | 程序头表(program header table)在文件内的字节偏移量;如果没有程序头表,该值为0; |
e_shoff | 4 | 8 | 节头表(section header table)在文件内的字节偏移量;如果没有节头表,该值为0 |
e_flags | 4 | 4 | 与处理器相关的标志; |
e_ehsize | 2 | 2 | ELF header的大小; |
e_phentsize | 2 | 2 | 程序头表(program header table)中每个条目(entry)的大小,即每个用来描述段信息的数据结构的大小;struct Elf32_Phdr; |
e_phnum | 2 | 2 | 程序头表中条目的数量,即段的个数; |
e_shentsize | 2 | 2 | 节头表(section header table)中每个条目(entry)的大小,即每个用来描述节信息的数据结构的大小; |
e_shnum | 2 | 2 | 节头表中条目的数量,即节的个数; |
e_shstrndx | 2 | 2 | string name table在节头表中的索引index; |
e_ident[EI_NIDENT]是16字节大小的数组,用来表示ELF字符等信息,开头四个字节是固定不变的elf文件魔数,0x7f 0x45 0x4c 0x46;
e_ident数组 | 说明 |
---|---|
e_ident[0~3] = 0x7f E L F | 固定的ELF文件魔数,用于识别ELF文件 |
e_ident[4] | 表示ELF文件的类型,0:不可识别类型,1:32位elf格式文件,2:64位elf格式文件 |
e_ident[5] | 指定编码格式,字节序是大端还是小端,0:非法编码格式,1:小端LSB,2:大端MSB |
e_ident[6] | ELF头的版本信息,默认为1,0:非法版本,1:当前版本 |
e_ident[7~15] | 保留,置为0 |
e_type:2字节,用来指定ELF目标文件的类型;
1 | // include/uapi/linux/elf.h |
e_machine:2字节,用来描述ELF目标文件的体系结构类型,即要在哪种硬件平台运行;
1 | // include/uapi/linux/elf-em.h |
2. 程序头表
程序头表(也称为段表)是一个描述文件中各个段的数组,程序头表描述了文件中各个段在文件中的偏移位置及段的属性等信息;从程序头表里可以得到每个段的所有信息,包括代码段和数据段等;各个段的内容紧跟ELF文件头保存;程序头表中各个段用Elf32_Phdr或Elf64_Phdr结构体表示;
1 | // include/uapi/linux/elf.h |
Elf32_Phdr或Elf64_Phdr结构体用来描述位于磁盘上的程序中的一个段;
成员 | 大小(32位) | 大小(64位) | 说明 |
---|---|---|---|
p_type | 4 | 4 | 程序中该段的类型; |
p_offset | 4 | 8 | 本段在文件内的起始偏移; |
p_vaddr | 4 | 8 | 本段在内存中的起始虚拟地址; |
p_paddr | 4 | 8 | 用于与物理地址相关的系统中; |
p_filesz | 4 | 8 | 本段在文件中的大小; |
p_memsz | 4 | 8 | 本段在内存中的大小; |
p_flags | 4 | 4 | 与本段相关的标志; |
p_align | 4 | 8 | 本段在文件和内存中的对齐方式;如果为0或1,表示不对齐,否则应该为2的幂次数; |
p_type:4字节,用来指明程序中该段的类型;
1 | // include/uapi/linux/elf.h |
p_flags:4字节,用来指明与本段相关的标志;
1 | // include/uapi/linux/elf.h |
3. 节头表
节头表中各个节用Elf32_Shdr或Elf64_Shdr结构体表示;
1 | // include/uapi/linux/elf.h |
Elf32_Shdr或Elf64_Shdr结构体用来描述位于磁盘上的程序中的一个节;
成员32位64位说明sh_name44节名称,值是字符串的一个索引;节名称字符串以’\0’结尾,统一存储在字符串表中,使用该字段存储节名称字符串在字符串表中的索引位置;sh_type44节的类型;sh_flags48节的标志;sh_addr48节在内存中的起始地址,指定节映射到虚拟地址空间中的位置;sh_offset48节在文件中的起始位置;sh_size48节的大小;sh_link44引用另一个节头表项,根据节类型有不同的解释;sh_info44节的附加信息,与sh_link联合使用;sh_addralign48节数据在内存中的对齐方式;sh_entsize48指定节中各数据项的长度,各数据项长度要相同;
成员 | 大小(32位) | 大小(64位) | 说明 |
---|---|---|---|
sh_name | 4 | 4 | 节名称,值是字符串的一个索引;节名称字符串以’\0’结尾,统一存储在字符串表中,使用该字段存储节名称字符串在字符串表中的索引位置; |
sh_type | 4 | 4 | 节的类型; |
sh_flags | 4 | 8 | 节的标志; |
sh_addr | 4 | 8 | 节在内存中的起始地址,指定节映射到虚拟地址空间中的位置; |
sh_offset | 4 | 8 | 节在文件中的起始位置; |
sh_size | 4 | 8 | 节的大小; |
sh_link | 4 | 4 | 引用另一个节头表项,根据节类型有不同的解释; |
sh_info | 4 | 4 | 节的附加信息,与sh_link联合使用; |
sh_addralign | 4 | 8 | 节数据在内存中的对齐方式;sh_entsize48指定节中各数据项的长度,各数据项长度要相同; |
sh_name
节名称sh_name的值是字符串的一个索引,节名称字符串以’\0’结尾,字符串统一存放在字符串表中,使用sh_name的值作为字符串表的索引,找到对应的字符串即为节名称;
字符串表中包含多个以’\0’结尾的字符串;在目标文件中,这些字符串通常是符号的名字或节的名字,需要引用某些字符串时,只需要提供该字符串在字符串表中的序号即可;
字符串表中的第一个字符串(序号为0)是空串,即’\0’,可以用于表示没有名字或一个空的名字;如果字符串表为空,节头中的sh_size值为0;
sh_type:节的类型;
1 | // include/uapi/linux/elf.h |
sh_flags:节的标志;
1 | // include/uapi/linux/elf.h |
4. readelf命令
readelf命令用于查看ELF格式的文件信息,
1 | $ readelf -H |
命令:
1 | $ readelf -h file // 显示ELF文件头 |
5. ELF文件实例
针对32位系统,通过一个极为简单的C语言的编译和分析,进行ELF文件中各结构体成员功能分析;
代码示例:
1 | int main(void) |
5.0 生成可执行文件
编译为中间文件,并链接为目标文件;
1 | $ gcc -c -o main.o main.c |
通过file命令查看main.o文件属性;main.o文件是32位大端可重定向的ELF文件,机器平台为PowerPC;
1 | $ file main.o main.o: ELF 32-bit MSB relocatable, PowerPC or cisco 4500, version 1 (SYSV), not stripped |
通过file命令查看kernel.bin文件属性;kernel.bin文件是32位大端可执行的ELF文件,机器平台为PowerPC;
1 | $ file kernel.bin kernel.bin: ELF 32-bit MSB executable, PowerPC or cisco 4500, version 1 (SYSV), statically linked, not stripped |
通过nm命令查看main.o文件的符号表,符号表中仅有一个main函数符号;
1 | $ nm main.o 00000000 T main |
通过nm查看kernel.bin文件的符号表,
1 | $ nm kernel.bin c0011510 A __bss_start c0011510 A _edata c0011510 A _end c0001500 T main |
5.1 ELF header
1 | $ xxd -u -a -g 1 -s 0 -l 0x100 kernel.bin |
000x0F是e_ident数组,e_ident[0]e_ident[3]的四个字节是固定的ELF魔数,0x7f, 0x45, 0x4c, 0x46;e_ident[4] = 1表示文件是32位的ELF文件,e_ident[5] = 2表示文件时大端字节序,e_ident[6] = 1表示默认版本,e_ident[7]~e_ident[15]的9个字节置为0;
0x10~0x11是e_type,2个字节,e_type = 0x00 02,表示类型是ET_EXEC,即可执行文件;
0x12~0x13是e_machine,2个字节,e_machine = 0x00 14,表示机器是EM_PPC,即power pc硬件平台;
0x14~0x17是e_version,4个字节,e_version = 0x00 00 00 01,表示版本信息;
0x18~0x1B是e_entry,4个字节,e_entry = 0xC0 00 15 00,表示程序的虚拟入口地址;
0x1C~0x1F是e_phoff ,4个字节,e_phoff = 0x00 00 00 34,表示程序头表在文件中的偏移量;
0x20~0x23是e_shoff,4个字节,e_shoff = 0x00 00 15 74,表示节头表在文件内的偏移量;
0x24~0x27是e_flags,4个字节,e_flags = 0x00 00 00 00,表示属性;
0x28~0x29是e_ehsize,2个字节,e_ehsize = 0x00 34,表示ELF header大小是0x34字节;程序头表紧跟ELF header之后;
0x2A~0x2B是e_phentsize,2个字节,e_phentsize = 0x00 20,表示program header结构ELF32_Phdr的大小,此处为0x20;
0x2C~0x2D是e_phnum,2个字节,e_phnum = 0x00 02,表示程序头表中段的个数,此处为2个段;
0x2E~0x2F是e_shentsize,2个字节,e_shentsize = 0x00 28,表示节头表中节的大小,此处为0x28;
0x30~0x31是e_shnum,2个字节,e_shnum = 0x00 06,表示节头表中节的个数,此处为6个节;
0x32~0x33是e_shstrndx,2个字节,e_shstrndx = 0x00 03,表示string name table在节头表中的索引为3;
5.2 程序头表
1 | $ xxd -u -a -g 1 -s 0 -l 0x100 kernel.bin ...... |
在ELF header中,程序头表偏移e_phoff = 0x00 00 00 34,所以程序头表的偏移位置为0x34,程序头表中段的大小e_phentsize = 0x00 20,段的个数e_phnum = 0x00 02,表示程序头表中有两个段,每个段大小0x20字节;ELF header大小e_ehsize = 0x00 34,程序头表紧跟ELF header之后;0x34~0x73之间共0x40个字节,是两个程序头的内容,从0x34地址开始,按照Elf32_Phdr结构分析;
第一个段头在0x34~0x53,共0x20个字节;
0x34~0x37是p_type,4个字节,p_type = 0x00 00 00 01,表示类型为PT_LOAD,为可加载程序段;
0x38~0x3B是p_offset,4个字节,p_offset = 0x00 00 00 00,表示本段在文件内的偏移量;
0x3C~0x3F是p_vaddr,4个字节,p_vaddr = 0xC0 00 00 00,表示本段被加载到内存后的起始虚拟地址;
0x40~0x43是p_paddr,4个字节,p_paddr = 0xC0 00 00 00,保留,和p_vaddr一致;
0x44~0x47是p_filesz,4个字节,p_filesz = 0x00 00 15 10,表示本段在文件中的大小;
0x48~0x4B是p_memsz,4个字节,p_memsz = 0x00 00 15 10,表示本段在内存中的大小;
0x4C~0x4F是p_flags,4个字节,p_flags = 0x00 00 00 05,表示本段的属性,5 = 4 + 1 = PF_R + PF_X,此处为可读和可执行权限;
0x50~0x53是p_align,4个字节,p_align = 0x00 01 00 00,表示本段的对齐方式,此处为64K对齐;
第二个段头在0x54~0x73,共0x20个字节;
0x54~0x57是p_type,4个字节,p_type = 0x64 74 E5 51,表示类型为PT_GNU_STACK,为GNU的栈段;
1 | // include/uapi/linux/elf.h |
之后的分析省略;
5.3 节头表
在ELF header中,节头表偏移e_shoff = 0x00 00 15 74,所以节头表的偏移位置为0x1574,节头表中段的大小e_shentsize = 0x00 28,段的个数e_shnum = 0x00 06,表示节头表中有六个节,每个节0x28字节;0x1574~0x1663共0xF0个字节,是六个节头的内容,从0x1574地址开始,按照Elf32_Shdr结构分析;
1 | $ xxd -u -g 1 -s 0x1570 -l 0x100 kernel.bin |
第一个节头在0x1574~0x159B,共0x28个字节;
0x1574~0x1577是sh_name,4个字节,sh_name = 0x00 00 00 00,表示节名称是在字符串表索引值为0x00的字符串,该字符串为空;
0x1578~0x157B是sh_type,4个字节,sh_type = 0x00 00 00 00,表示该节是一个无效的节头,没有对应的节;该节中其他成员也无意义;
第二个节头在0x159C~0x15C3,共0x28个字节;
0x159C~0x159F是sh_name,4个字节,sh_name = 0x00 00 00 1B,表示节名称是在字符串表索引值为0x1B的字符串;该字符串为”.text”;
0x15A0~0x15A3是sh_type,4个字节,sh_type = 0x00 00 00 01,表示
0x15A4~0x15A7是sh_flags,4个字节,sh_flags = 0x00 00 00 06,0x06 = 0x04 + 0x02 = SHF_EXECINSTR + SHF_ALLOC,表示该节内容是指令代码,并且包含内存在进程运行时需要占用内存单元;
0x15A8~0x15AB是sh_addr,4个字节,sh_addr = 0xC0 00 15 00,表示该节在内存中的起始地址,节映射到虚拟地址空间中的位置;
0x15AC~0x15AF是sh_offset,4个字节,sh_offset = 0x00 00 15 00,表示该节在文件中的起始位置;
0x15B0~0x15B3是sh_size,4个字节,sh_size = 0x00 00 00 10,表示该节的大小;
0x15B4~0x15B7是sh_link,4个字节,sh_link = 00 00 00 00
0x15B8~0x15BB是sh_info,4个字节,sh_info = 0x00 00 00 00
0x15BC~0x15BF是sh_addralign,4个字节,sh_addralign = 0x00 00 00 04,表示该节的数据在内存中以16字节对齐;
0x15C0~0x15C3是sh_entsize,4个字节,sh_entsize = 0x00 00 00 00
以后各节的解析省略;
5.4 readelf查看结果
1) 显示程序的ELF文件头
1 | $ readelf -h kernel.bin |
2) 显示程序所有的程序头
1 | $ readelf -l kernel.bin |
3) 显示程序所有的节头
1 | $ readelf -S kernel.bin |
4) 综合显示程序所有的头信息,包含ELF文件头、程序头、节头信息;
1 | $ readelf -e kernel.bin |
通过readelf命令查看的结果,和按照ELF文件分析的结果,对比结果一致;
6. 总结
在Linux系统的可执行文件(ELF文件)中,开头是一个文件头,用来描述程序的布局,整个文件的属性等信息,包括文件是否可执行、静态还是动态链接及入口地址等信息;生成的文件不是纯碎的二进制可执行文件了,因为包含的程序头不是可执行代码;将这种包含程序头的文件读入到内存后,从程序头中读取入口地址,跳转到入口地址执行;
参考资料
《操作系统真象还原》
程序编译-汇编-链接的理解!—03-ELF头和节头表
ELF文件-节和节头