ELF文件
01 什么是ELF文件?
-
现在PC平台流行的可执行文件格式(Executable) 主要是Windows下的PE (PortableExecutable)和Linux的ELF ( Executable Linkable Format),它们都是COFF (Common fileformat)格式的变种。
-
COFF(通用对象文件格式)是由UnixSystemVRelease3首先提出并且使用的格式规范,后来微软公司基于COFF格式,制定了PE格式标准,并将其用于当时的WindowsNT系统。SystemV Release 4在COFF的基础上引入了ELF格式,目前流行的Linux系统也以ELF作为基本可执行文件格式。这也就是为什么目前PE和ELF如此相似的主要原因,因为它们都是源于同一种可执行文件格式COFF。
-
在linux下ELF文件格式包括了
- 可重定位文件(.obj, xxxx.o)
- 可执行文件(.exe, /bin/bash)
- 共享目标文件(.dll, .so)
- 核心转储文件(core dump)
-
通过file命令可以查看文件格式
命令 结果 file fun.o fun.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped file fun libfun.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=d9fb2a4d3debed0a19452dbd13e10f7411ff72cb, not stripped file libfun.so fun: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=bd6a4c4f0375be6f0a889b84f48f4baa110ee860, not stripped
02 目标文件
-
目标文件(.o)中的内容至少有编译后的机器指令代码、数据,还包括了链接时所须要的一-些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“段”( Segment)。
-
源码(.c)编译成机器指令后(.o),机器指令经常被放到代码段中(.text .code),全局变量被放到数据段(.data)
-
程序员的自我修养
-
来看看一个目标文件
// fun.c int global_init_var =100; int fun(int a, int b) { return 0; } int main() { fun(); retrun 0; } // gcc -c fun.c -o fun.o
-
这里可以使用linux的两个工具查看可执行文件的objdump和readelf他们都属于binutils的工具集
objdump -h fun.o
-
查看目标文件中区段信息,“-s”打印所有区段中16进制数据,"-d"显示所有指令的反汇编代码
objdump -s -d fun.o
-
常见的区段名
常用的段名 说明 .rodata Read only Data,这种段里存放的是只读数据,比如字符串常量、全局const .comment 变量。跟“,rodata” 一样存放的是编译器版本信息,比如字符串: "GCC: (GNU) 4.2.0" .debug 调试信息 .dynamic 动态链接信息 .hash 符号哈希表 .line 调试时的行号表,即源代码行号与编译后指令的对应表 .note 额外的编译器信息。比如程序的公司名、发布版本号等 .strtab StringTable.字符串表,用于存储ELF文件中用到的各种字符串 .symtab Symbol Table.符号表 .shstrtab Section String Table.段名表 .plt 动态链接的跳转表 .got 全局入口表 .init 程序初始化与终结代码段常用的段名 -
最后通过ld连接器将目标文件和其他目标文件,so文件,相同属性的区段合并,最后生成一个可执行文件
03 ELF文件结构
- ELF文件格式结构
- 链接视图,执行视图
04 ELF工具分析
-
readelf命令是binutils工具集中的一个查看elf文件格式的工具。
-
readelf文件常用参数
常用参数 说明 -a 显示文件所有内容 -h 显示ELF文件头 -S 显示ELF文件区段头表(Section Header Table)信息 -l 显示ELF文件程序头表(Program Header Table)信息 -s 显示ELF文件符号表 -r 显示ELF文件重定位表 -d 显示ELF文件动态加载信息 -
下载readelf源码,辅助我们学习elf文件
- 1.找到安装路径 which readelf
- 2.查看源码包dpkg -S /usr/bin/readelf
- 3.下载源码apt-get source binutils
-
搜索main函数
- 在./binutils/readelf.c文件最底下
- **process_file()**整个解析elf文件的开始
- process_object()
- get_file_header()
- process_file_headers()
- process_program_headers()
- process_dynameic_section()
- process_relocs
- process_symbol_table()
- 在./binutils/readelf.c文件最底下
-
010edit分析工具
05 ELF头部
-
readelf -h main
-
ELF文件结构体定义都在/usr/include/elf.h 文件中
typedef struct { unsigned char e_ident[EI_NIDEN] /*16 文件标识以及其他信息*/ Elf32_Half e_type; /* 文件类型 */ Elf32_Half e_machine; /* CPU体系 */ Elf32_Word e_version; /* 文件版本 */ Elf32_Addr e_entry; /* 入口点地址 */ Elf32_Off e_phoff; /* 程序头表文件偏移 */ Elf32_Off e_shoff; /* 区段头表文件偏移*/ Elf32_Word e_flags; /* 处理器标志 */ Elf32_Half e_ehsize; /* ELF header 大小 */ Elf32_Half e_phentsize; /* 一项程序头表大小 */ Elf32_Half e_phnum; /* 程序头表格个数 */ Elf32_Half e_shentsize; /* 一项区段头表大小 */ Elf32_Half e_shnum; /* 区段头表个数 */ Elf32_Half e_shstrndx; /* 区段头的字符串表 下标 */ } Elf32_Ehdr;
-
ELF文件魔术:e_ident 前4字节
// 文件:usr/include/elf.h #define EI_MAG0 0 /* File identification byte 0 index */ #define ELFMAG0 0x7f /* Magic number byte 0 */ #define EI_MAG1 1 /* File identification byte 1 index */ #define ELFMAG1 'E' /* Magic number byte 1 */ #define EI_MAG2 2 /* File identification byte 2 index */ #define ELFMAG2 'L' /* Magic number byte 2 */ #define EI_MAG3 3 /* File identification byte 3 index */ #define ELFMAG3 'F' /* Magic number byte 3 *
-
文件类型:e_ident 第4字节
- 第 4 字节表示文件类别,0 表示无效文件,1 表示 32 位 ELF 文件,2,表示 64 位 ELF 文 件,分别对应一个宏。
#define EI_CLASS 4 /* File class byte index */ #define ELFCLASSNONE 0 /* Invalid class */ #define ELFCLASS32 1 /* 32-bit objects */ #define ELFCLASS64 2 /* 64-bit objects */ #define ELFCLASSNUM 3
-
字节序:e_ident 第5字节
- 第 5 字节表示字节序,0 表示无效格式、1 表示小端字节序、2 表示大端字节序,源码 中可见以下宏
#define EI_DATA 5 /* Data encoding byte index */ #define ELFDATANONE 0 /* Invalid data encoding */ #define ELFDATA2LSB 1 /* 2's complement, little endian */ #define ELFDATA2MSB 2 /* 2's complement, big endian */ #define ELFDATANUM 3
-
ELF版本:e_ident 第6字节
- 第 6 字节表示 ELF 版本,默认必须是 1
#define EI_VERSION 6 /* File version byte index */ /* Value must be EV_CURRENT */
-
系统版本:e_ident 第7字节
- 第 7 字节表示系统版本,默认是 ELFOSABI_NONE ,即 0,其他很多的宏可以见源
#define EI_OSABI 7 /* OS ABI identification */ #define ELFOSABI_NONE 0 /* UNIX System V ABI */ #define ELFOSABI_SYSV 0 /* Alias. */ #define ELFOSABI_HPUX 1 /* HP-UX */ #define ELFOSABI_NETBSD 2 /* NetBSD. */ //...
-
ABI版本:e_ident 第8字节
- 第 8 字节表示 ABI 版本,默认为 0,相关的宏
#define EI_ABIVERSION 8 /* ABI version */
-
e_type
- e_type 表示 ELF 文件类型,0 表示无效文件类型,1 表示可重定位目标文件、2 表 示可执行文件、3 表示共享目标文件、4 表示核心转储文件。汇编器输出的是可重定位文 件,静态链接器输出的是可执行文件或是共享目标文件。 相关的宏
#define ET_NONE 0 /* No file type */ #define ET_REL 1 /* Relocatable file */ #define ET_EXEC 2 /* Executable file */ #define ET_DYN 3 /* Shared object file */ #define ET_CORE 4 /* Core file */ #define ET_NUM 5 /* Number of defined types */ #define ET_LOOS 0xfe00 /* OS-specific range start */ #define ET_HIOS 0xfeff /* OS-specific range end */ #define ET_LOPROC 0xff00 /* Processor-specific range start */ #define ET_HIPROC 0xffff /* Processor-specific range end */
-
e_machine
- e_machine,表示 ELF 文件所在的机器类型,例如 3 表示 Intel386,40 表示 ARM。 源码中对应的宏
-
e_version:
- 表示ELF文件的版本,一般取值为1
-
e_entry :
- 表示程序入口点线性地址,一般用于ELF可执行文件。对应可重定位目标文件,该字段设置为0
-
e_phoff :
- e_phoff,表示程序头表在ELF文件内的偏移地址,标识了程序头表在文件内的位置。
-
e_shoff:
- e_shoff,表示段表在ELF文件内的偏移地址,标识了段表在文件内的位置。
-
e_flags
- 表示ELF文件平台相关的属性,一般默认为0.
-
e_ehsize
- e_ehsize表示ELF文件头的大小。
-
e_phentsize
- e_phentsize表示程序头表项的大小。
-
e_phnum
- e_phnum表示程序头表项的个数。可以使用e_phoff(偏移)、e_phentsize(表项大小)、e_phnum(表项个数)计算出程序头表占据空间。
-
e_shentsize
- e_shentsize表示段头表项的大小。
-
e_shnum
- e_shnum表示段头表项的个数。与程序头表类似,段头表相关的字段也有e_shoff(偏移)、e_shentsize(表项大小)、e_shnum(表项数量)。
-
e_shstrndx
-
e_shstrndx表示段表字符串表(.shstrtab)所在段在段表中的索引。
-
分析代码
// 解析elf头部 void parse_elf_head(char *pbuff) { // 获取elf头部 Elf32_Ehdr *phdr = get_elf_head(pbuff); // elf Migic 魔术 for (int i = 0; i < 16; i++) { printf("%2x ", phdr->e_ident[i]); } // elf ident Class 文件类型 printf("\n class:"); if(phdr->e_ident[EI_CLASS] == 1) { printf("ELF32"); }else if(phdr->e_ident[EI_CLASS] == 2){ printf("ELF64"); }else{ printf("Unknow"); } // elf type 类型 4 core shared exec reload printf("\n type:"); char * sztype[5]={"unknow","可重定位","可执行","动态共享","核心转储"}; if(phdr->e_type>4 || phdr->e_type == 0 ) { printf("Unknow"); }else{ printf(sztype[phdr->e_type]); }; // 判断机器 printf("\n machine:"); if(phdr->e_machine == EM_386) { printf("intel 80386 "); }else if(phdr->e_machine == EM_X86_64) { printf("amd x86_64 "); } // 入口点 printf("\n entry: %08x",phdr->e_entry); // 程序头 printf("\n program: offset=%08x entsize=%08x num=%d", phdr->e_phoff,phdr->e_phentsize,phdr->e_phnum); // 区段头 printf("\n section: offset=%08x entsize=%08x num=%d\n", phdr->e_shoff,phdr->e_shentsize,phdr->e_shnum); // 字符表位置 printf("\n string index : %d",phdr->e_shstrndx); printf("\n"); }
06 ELF区段头表(section)
-
区段头表在解析(编译)时候用到,运行程序时区段头表没有意义,系统只会加载程序表
typedef struct { Elf32_Word sh_name; /* 区段字符串表的索引 sh_name + .strtab */ Elf32_Word sh_type; /* 区段类型(重要)*/ Elf32_Word sh_flags; /* 区段标志 */ Elf32_Addr sh_addr; /* 虚拟地址 */ Elf32_Off sh_offset; /* 区段在文件的偏移(重要) */ Elf32_Word sh_size; /* 区段大小 */ Elf32_Word sh_link; /* 与之相关联的 区段符号表 的索引(重要 可以找到字符) */ Elf32_Word sh_info; /* 与之相关联的区段,对与不同类型区段存放相关的区段(索引) */ Elf32_Word sh_addralign; /* 区段对齐值 */ Elf32_Word sh_entsize; /* 区段每个元素大小,对于重定位和符号表有用 */ } Elf32_Shdr;
-
字段说明
字段 说明 sh_name 是一个4字节偏移,记录了段名字符串在段表字符串表(“.shstrtab” 段)内的偏移 ,".shstrta"段由Elf32_ehdr->e_shstrndx指定。 sh_type 表示段的类型。段的类型有很多,常见的有SHT_PROGBITS,表示程序数据,有代码段.text、数据段.data, SHT_SYMTAB表示符号表,有符号表段.symtab,SHT_STRTAB表示字符串表,有段表字符串表段.shstrtab,串表段.strtab。还有专门存放构造函数数组的段SHT_INIT_ARRAY,析构函数数组段SHT_FINI_ARRAY。 具体类型见下列 sh_flags 表示段标志,记录段的属性。其中0表示默认属性。1表示段可写,取值SHF_WRITE。2表示段加载后需要为之分配内存空间,取值SHF_ALLOC。4表示段可以执行,取值SHF_EXECINSTR。段标志属性可以进行复合叠加,比如代码段“.text”属性,具有可分配、可执行属性,数据段“.data”具有可写可分配属性。 sh_addr 表示段加载后的线性地址。在可重定位目标文件内,无法确定段的虚拟地址,故设置默认值为0。 sh_offset 表示段在文件内的偏移,根据此偏移可以确定段的位置,读取段的内容 sh_size 表示段的大小,单位为字节。需要注意的是,如果段类型为SHT_NOBITS,段内没有数据,那么段大小并非指文件块的大小,而是指段加载后占用内存的大小。比如bss段 sh_link sh_link记录的是符号表使用的串表所在段(一般是.strtab)对应段表项在段表内的索引 sh_info 此成员给出附加信息,其解释依赖于节区类型。 sh_addralign 表示段对齐方式,对齐规则为 sh_offset%sh_addralign=0,即段的文件偏移必须是sh_addralign的整数倍。sh_addralign取值必须是2的整数幂,如1、2、4、8等。常见对齐值 sh_entsize 一般用于保存诸如符号表段、重定位表段时,表示段内保存表的表项大小。例如符号表段“.symtab”内保存的符号表的表项大小为sizeof(Elf32_Sym)=16字节,重定位表段“.rel.plt”内保存的重定位表的表项大小为sizeof(Elf32_Rel)=8字节。 -
类型说明sh_type
类型 值 说明 SHT_NULL 0 此值标志节区头部是非活动的,没有对应的节区。此节区头部 中的其他成员取值无意义 SHT_PROGBITS 1 此节区包含程序定义的信息,其格式和含义都由程序来解释,通常数据和代码段都是这个属性 SHT_SYMTAB 2 节区提供用于链接编辑(指 ld 而言) 的符号,尽管也可用来实现动态链接。 SHT_STRTAB 3 此节区包含字符串表。目标文件可能包含多个字符串表节区。 SHT_RELA 4 此节区包含重定位表项,其中可能会有补齐内容(addend),例 如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多 个重定位节区 SHT_HASH 5 此节区包含符号哈希表。所有参与动态链接的目标都必须包含 一个符号哈希表。目前,一个目标文件只能包含一个哈希表, 不过此限制将来可能会解除 SHT_DYNAMIC 6 此节区包含动态链接的信息。目前一个目标文件中只能包含一 个动态节区,将来可能会取消这一限制。 SHT_NOTE 7 此节区包含以某种方式来标记文件的信息 SHT_NOBITS 8 这种类型的节区不占用文件中的空间,其他方面和 SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员 sh_offset 中还是会包含概念性的文件偏移 SHT_REL 9 此节区包含重定位表项,其中没有补齐(addends),例 如 32 位 目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定 位节区。 SHT_SHLIB 10 此节区被保留,不过其语义是未规定的。包含此类型节区的程 序与 ABI 不兼容。 SHT_DYNSYM 11 作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节 区,其中保存动态链接符号的一个最小集合,以节省空间。 SHT_LOPROC 0X70000000 这一段(包括两个边界),是保留给处理器专用语义的。 SHT_HIPROC OX7FFFFFFF 这一段(包括两个边界),是保留给处理器专用语义的。 -
段类标志sh_flags
类型 值 说明 SHF_WRITE 0 节区包含进程执行过程中将可写的数据。 SHF_ALLOC 2 此节区在进程执行过程中占用内存。某些控制节区并不出现于目标 文件的内存映像中,对于那些节区,此位应设置为 0 SHF_EXECINSTR 4 节区包含可执行的机器指令。 SHF_MASKPROC 0xF0000000 所有包含于此掩码中的四位都用于处理器专用的语义 -
sh_link和sh_info字段
- 根据节区类型的不同,sh_link 和 sh_info 的具体含义也有所不同:
sh_type sh_link sh_info SHT_DYNAMIC 此节区中条目所用到的字符串表格 的节区头部索引 0 SHT_HASH 0 SHT_REL 相关符号表的节区头部索引 重定位所适用的节区的 节区头部索引 SHT_RELA 相关符号表的节区头部索引 重定位所适用的节区的 节区头部索引 SHT_SYMTAB 相关联的字符串表的节区头部索 最后一个局部符号(绑 定 STB_LOCAL)的符 号表索引值加1 SHT_DYNSYM 相关联的字符串表的节区头部索 最后一个局部符号(绑 定 STB_LOCAL)的符 号表索引值加1 -
解析代码
// 解析elf 区段 void parse_elf_section(char *pbuff){ Elf32_Shdr *pshdr = get_elf_section(pbuff); Elf32_Ehdr *pehdr = get_elf_head(pbuff); char *section_name_tables = get_elf_section_name_table(pbuff); printf("name type addr off size al es flag link info \n"); //遍历所有区段 for(int i=0;i<pehdr->e_shnum;i++){ // 区段名 char *section_name = section_name_tables + pshdr[i].sh_name; printf("%-20s",section_name); printf(" %08x",pshdr[i].sh_type); printf(" %08x",pshdr[i].sh_addr); printf(" %08x",pshdr[i].sh_offset); printf(" %08x",pshdr[i].sh_size); printf(" %04d",pshdr[i].sh_addralign); printf(" %04d",pshdr[i].sh_entsize); printf(" %04d",pshdr[i].sh_flags); printf(" %04d",pshdr[i].sh_link); printf(" %04d\n",pshdr[i].sh_info); } }
07 ELF符号表
-
链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起,或者说像玩具积木- -样, 可以拼装形成一个整体。 为了使不同目标文件之间能够相互粘合,这些目标文件之间必须有固定的规则才行,就像积木模块必须有凹凸部分才能够拼合。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的用,即对函数和变量的地址的引用。比如目标文件B要用到了目标文件A中的函数“foo”,, 那么我们就称目标文件A定义(Define)了函数“foo”,称目标文件B引用(Reference)了目标文件A中的函数“foo”。这两个概念也同样适用于变量。每个函数或变最都有自己独特的名字,才能避免链接过程中不同变量和的数之间的混淆。在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name )。 我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。
-
链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表( Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变最和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他几种不常用到的符号。我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:
-
定义在本目标文件的全局符号,可以被其他目标文件引用。
-
在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(Extemal Symbol),也就是我们前面所讲的符号引用。
-
段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如SimpleSection.o里面的“.text”.“.data”等。.
-
局部符号,这类符号只在编译单元内部可见。比如SimpleSection.o里面的static. _var和“static_var2"。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们。
-
行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。
-
-
全局符号表
- 区段类型为 SHT_SYMTAB是全局符号表
- 区段类型为 SHT_DYNSYM动态符号表
typedef struct { Elf32_Word st_name; /* Symbol name (string tbl index) */ Elf32_Addr st_value; /* Symbol value */ Elf32_Word st_size; /* Symbol size */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf32_Section st_shndx; /* Section index */ } Elf32_Sym;
-
字段说明
字段 说明 st_name 包含目标文件符号字符串表的索引,其中包含符号名的字符串表示。如 果该值非 0,则它表示了给出符号名的字符串表索引,否则符号表项没 有名称。 注:外部 C 符号在 C 语言和目标文件的符号表中具有相同的名称。由所在区段link指向符号表 st_value 此成员给出相关联的符号的取值。依赖于具体的上下文,它可能是一个 绝对值、一个地址等等。 st_size 很多符号具有相关的尺寸大小。例如一个数据对象的大小是对象中包含 的字节数。如果符号没有大小或者大小未知,则此成员为 0 st_info 此成员给出符号的类型和绑定属性。下面给出若干取值和含义的绑定关 系。 st_other st_other 该成员当前包含 0,其含义没有定义。 st_shndx 每个符号表项都以和其他节区间的关系的方式给出定义(符号所在区段下标)。此成员给出相 关的节区头部表索引。某些索引具有特殊含义。 -
st_info字段
- st_info 中包含符号类型和绑定信息,操纵方式如:
#define ELF32_ST_BIND(i) ((i)>>4) #define ELF32_ST_TYPE(i) ((i)&0xf) #define ELF32_ST_INFO(b, t) (((b)<<4) + ((t)&0xf)
-
绑定类型 ELF32_ST_BIND(st_info)
名称 取值 说明 STB_LOCAL 0 局部符号在包含该符号定义的目标文件以外不可见。相同名 称的局部符号可以存在于多个文件中,互不影响。 STB_GLOBAL 1 全局符号对所有将组合的目标文件都是可见的。一个文件中 对某个全局符号的定义将满足另一个文件对相同全局符号的 未定义引用。 STB_WEAK 2 弱符号与全局符号类似,不过他们的定义优先级比较低。 STB_LOPROC 13 处于这个范围的取值是保留给处理器专用语义的。 STB_HIPROC 15 处于这个范围的取值是保留给处理器专用语义的。 -
符号类型 ELF32_ST_TYPE(st_info)
名称 取值 说明 STT_NOTYPE 0 符号的类型没有指定 STT_OBJECT 1 符号与某个数据对象相关,比如一个变量、数组等等 STT_FUNC 2 符号与某个函数或者其他可执行代码相关 STT_SECTION 3 符号与某个节区相关。这种类型的符号表项主要用于重定 位,通常具有 STB_LOCAL 绑定。 STT_FILE 4 传统上,符号的名称给出了与目标文件相关的源文件的名 称。文件符号具有 STB_LOCAL 绑定,其节区索引SHN_ABS,并且它优先于文件的其他 STB_LOCAL 符号 (如果有的话 ...... -
解析符号
#include "tools/elf_opation.h" void parse_elf_symbole(char *pbuff) { // 区段类型为 SHT_SYMTAB 全局符号表 SHT_DYNSYM为动态符号表 // link指向 字符串符号表 // off 指向 符号表数组 Elf32_Sym Elf32_Shdr * psymshdr = get_elf_symble_section(pbuff); Elf32_Shdr *pshdr = get_elf_section(pbuff); // 获取字符串符号表 Elf32_Shdr *psym_string_table_shdr = &pshdr[psymshdr->sh_link]; // 获取字符串表首地址 char * psym_string_table = (char *)(psym_string_table_shdr->sh_offset + pbuff); // 获取符号表数组 Elf32_Sym * psym = (Elf32_Sym *)(psymshdr->sh_offset+ pbuff); // 获取符号表个数 int sym_count= psymshdr->sh_size/psymshdr->sh_entsize; // 遍历符号表 for(int i=0;i<sym_count;i++) { char *sym_name = (char *)(psym[i].st_name + psym_string_table); if(*sym_name == 0 ){ printf("[%d]: 未知 \n",i); }else{ printf("[%d]:%s\n",i,sym_name); } // st_value符号的值,一般在可重定位目标文件内,该值记录了符号相对于所在 段基地址的偏移量,而在可执行文件内,该值记录了符号的线性地址。 psym[i].st_value; // st_info 符号属性, // 1. 其低 4 位表示符号的类型使用宏 ELF32_ST_TYPE 获取, // STT_NOTYPE (0):未知 | STT_OBJECT (1):对象 | STT_FUNC(2):函数 |STT_SECTION(3):区段 // 2. 高 4 位表示符号的绑定信息,使用宏 ELF32_ST_BIND 获取 // STB_LOCAL(0):局部符号| STB_GLOBAL(1):全局符号 。。。。 psym[i].st_info; // st_shndx 符号所在段索引, // 当该字段 为 0 时,表示符号未定义,取值 SHN_UNDEF。 // 该字段为 0Xfff1 时,表示符号为绝对值, 比如文件名,取值 SHN_ABS。 // 该字段为 0xfff2 时,表示符号在 COMMON 块内,取值为 SHN_COMMON,特别的,此时符号的 st_value 表示符号的对齐属性 psym[i].st_shndx; printf("\t\t %08x\n",psym[i].st_value); printf("\t\t %08x\n",psym[i].st_size); const char *attrs[20]={"未知","对象","函数","区段","文件"}; printf("\t\t %s\n",attrs[ELF32_ST_TYPE( psym[i].st_info)]); const char *binds[20]={"局部符号","全局符号","弱引用"}; printf("\t\t %s\n",binds[ELF32_ST_BIND( psym[i].st_info)]); } }
08 ELF重定位表
-
重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时, 相关的调用指令必须把控制传输到适当的目标执行地址,例如在main函数中使用到printf函数,那么会在重定位表中记录printf函数位置(rel.plt)
-
重定位表类型 SHT_REL、SHT_RELA
-
重定位可能有多个
typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel; typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Word r_addend; } Elf32_Rela;
-
重定位字段
成员 说明 r_offset 此成员给出了重定位动作所适用的位置。对于一个可重定位文件而言, 此值是从节区头部开始到将被重定位影响的存储单位之间的字节偏 移。对于可执行文件或者共享目标文件而言,其取值是被重定位影响 到的存储单元的虚拟地址。 r_info 此成员给出要进行重定位的符号表索引,以及将实施的重定位类型。 例如一个调用指令的重定位项将包含被调用函数的符号表索引。如果 索引是 STN_UNDEF,那么重定位使用 0 作为“符号值”。重定位类型是和处理器相关的。当程序代码引用一个重定位项的重定位类型或 者符号表索引,则表示对表项的 r_info 成员应用 ELF32_R_TYPE 或 者 ELF32_R_SYM 的结果。 #define ELF32_R_SYM(i) ((i)>>8) #define ELF32_R_TYPE(i) ((unsigned char)(i)) #define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t)) r_addend 此成员给出一个常量补齐,用来计算将被填充到可重定位字段的数值 -
重定位类型
类型名 | 值 | 说明 |
---|---|---|
R_386_NONE | 0 | |
R_386_32 | 1 | 32重定位数据,ld链接时使用 |
R_386_PC32 | 2 | 32重定位数据,ld链接时使用 |
R_386_GOT32 | 3 | 此重定位类型计算从全局偏移表基址到符号 的全局偏移表项之间的距离。它会通知连接编 辑器构造一个全局偏移表。 |
R_386_PLT32 | 4 | 此重定位类型计算符号的过程链接表项的地 质,并通知链接编辑器构造一个过程链接表。 |
R_386_JMP_SLOT | 7 | 链接编辑器创建这种重定位类型主要是为了 支持动态链接。其偏移地址成员给出过程链接 表项的位置。动态链接器修改过程链接表项的 内容,把控制传输给指定符号的地址 |
R_386_RELATIVE | 8 | 链接编辑器创建这种重定位类型是为了支持 动态链接。其偏移地址成员给出共享目标中的 一个位置,在该位置包含了代表相对地址的一 个数值。动态链接器通过把共享目标被加载到 的虚地址和相对地址相加,计算对应的虚地 址。这种类型的重定位项必须设置符号表索引 为 0。 |
-
需要重定位类型
void parse_elf_reloc(char *pbuff){ // 获取重定位表 Elf32_Shdr *preloc_shdr = get_elf_reloc_section(pbuff); // sh_offset: 指向重定位表数组 // sh_link : 指向符号表下标 (Elf32_Sym) // sh_info : 指向符号所在区段下标 (Elf32_Shdr) while (preloc_shdr){ Elf32_Shdr *pshdr = get_elf_section(pbuff); // 重定位数据所在区段名称 char * rel_section_name = (char *)(preloc_shdr->sh_name + get_elf_section_name_table(pbuff)); printf("%s \n",rel_section_name); // 获取重定位数组 Elf32_Rel *prelocs = (Elf32_Rel *)(preloc_shdr->sh_offset + pbuff); // 获取重定位个数 int reloc_cont = preloc_shdr->sh_size / preloc_shdr->sh_entsize; // 获取重定位对应的符号表 Elf32_Sym *preloc_sym_table = (Elf32_Sym *)(pshdr[preloc_shdr->sh_link].sh_offset + pbuff); char *preloc_sym_string_table = (char *)(pshdr[pshdr[preloc_shdr->sh_link].sh_link].sh_offset + pbuff); printf("offset info type sym.value sym.name\n"); //遍历重定位表 for (int i = 0; i < reloc_cont; i++){ // r_info,描述了重定位类型和符号 // 1.低 8 位表示重定位类型,使用宏 ELF_32_R_TYPE 获取, // 2.高 24 位表示重定位符号对应符号表项在符号表内的索引,使用宏 ELF_32_R_SYM 获取。 // 3.不同的处理器体系结构都属于自己的一套重定位类型,对于 x86 体 系结构而言,静态链接中常见的重定位类型有两种: // 3.1 绝对地址重定位(取值 R_386_32) // 5.2 相对地址重定位 R_386_PC32。 // 每条重定位信息的含义为使用 r_info 记录的符号的线性地 址,根据重定位类型更新 r_offset 处的内存信息。 prelocs[i].r_info; //r_offset,表示重定位地址. // 对应可重定位目标文件来说,表示重定位位置相对于 被重定位段的基地址的偏移。 // 而对于可执行文件或共享目标文件来说,表示重定位位置对 应的线性地址,这个与动态了解相关 prelocs[i].r_offset; printf("%08x ", prelocs[i].r_offset); printf("%08x ", prelocs[i].r_info); const char *types[20] = { "R_386_NONE", "R_386_32", "R_386_PC32", "R_386_GOT32", "R_386_PLT32", "R_386_COPY", "R_386_GLOB_DAT","R_386_JMP_SLOT"}; printf("%-15s ", types[ELF32_R_TYPE(prelocs[i].r_info)]); int sym_index = ELF32_R_SYM(prelocs[i].r_info); printf("%08x ", preloc_sym_table[sym_index].st_value); char *reloc_sym_name = (char *)(preloc_sym_table[sym_index].st_name + preloc_sym_string_table); printf("%15s ", reloc_sym_name); printf("\n"); } printf("\n"); // 遍历下一个重定位表 preloc_shdr = get_elf_reloc_next_section(preloc_shdr); } }
09 ELF动态链接表
-
如果一个目标文件参与动态链接,它的程序头部表将包含类型为 PT_DYNAMIC 的元素。此“段”包含.dynamic 节区。该节区采用一个特殊符号_DYNAMIC 来标记。
-
段类型是 SHT_DYNAMIC (PT_DYNAMIC)
-
通常我们可以从区段(程序段)中去解析,它们两个是一样的位置。当程序运行时应该从程序段解析。
typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn; extern Elf32_Dyn _DYNAMIC[];
- d_val 此 Elf32_Word 对象表示一个整数值,可以有多种解释。
- d_ptr 此 Elf32_Addr 对象代表程序的虚拟地址。
- 如前所述,文件的 虚拟地址可能与执行过程中的内存虚地址不匹配。在解释包含于动态 结构中的地址时,动态链接程序基于原来文件值和内存基地址计算实 际地址。为了保持一致性,文件中不包含用来“纠正”动态结构中重定位项地址的重定位项目。
-
动态d_tag包括
名称 数值 说明 DT_NULL 0 标记为 DT_NULL 的项目标注了整个 _DYNAMIC 数 组的末端 DT_NEEDED 1 此元素包含一个 NULL 结尾的字符串的字符串表 偏移,该字符串给出某个需要的库的名称。所使用 的字符串表根据 DT_STRTAB 项目中记录的内容确 定。所谓的偏移即是指在该表中的下标。动态数组中可以包含多个这种类型的条目。这些条目的相对 顺序很重要,尽管他们与其他类型条目间的顺序没 有很大关系。 在link中保存字符串表的index (这个程序中所有使用的so文件名) DT_PLTRELSZ 2 此元素给出了与过程链接表(PLT)相关联的重定 位项的总计大小(按字节)。如果存在 DT_JMPREL 类型的条目,必须有与之配合的 DT_PLTRELSZ 条 目。 DT_PLTGOT 3 此元素给出一个与过程链接表(PLT)与/或全局偏 移表相关联的一个地址 DT_HASH 4 此元素包含符号哈希表的地址。此哈希表指的是被 DT_SYMTAB 元素引用的符号表 DT_STRTAB 5 此元素包含字符串表的地址,符号名、库名、和其 他字符串都包含在此表 DT_SYMTAB 6 此元素包含符号表的地址。对 32 位的文件而言, 这个符号表中的条目是 Elf32_Sym 类型。 DT_REL 17 此元素与 DT_RELA 类似,只是其表格中包含隐式 的补齐,对 32 位文件而言,就是 Elf32_Rel。如 果文件中包含此元素,那么动态结构中也必须包含 DT_RELSZ 和 DT_RELENT 元素。 DT_RELSZ 18 此元素包含 DT_REL 重定位表的总计大小,按字节 数计算 DT_JMPREL 23 如果存在这种成员,则表示条目的 d_ptr 成员包 含了某个重定位项的地址,并且该重定位项仅与过 程链接表相关。把重定位项分开有利于让动态链接 器在进程初始化时忽略它们,当然后期绑定必须可 行。如果存在此成员,相关的 DT_PLTRELSZ 和 DT_PLTREL 必须也存在 ....... -
解析代码
void parse_elf_dynamic_link(char *pbuff){ Elf32_Shdr *pshdr = get_elf_section(pbuff); // 获取动态连接区段 Elf32_Shdr *pdynamic_shdr = get_elf_dynamic_link_section(pbuff); // 动态连接区段的字符串表 char * dynamic_string_tables = (char *)(pshdr[pdynamic_shdr->sh_link].sh_offset + pbuff); // 获取动态连接数组 Elf32_Dyn * pdynamic_links = (Elf32_Dyn *)(pdynamic_shdr->sh_offset + pbuff); // 获取动态连接信息个数 int dynamic_count = pdynamic_shdr->sh_size / pdynamic_shdr->sh_entsize; // d_val 标记,用于控制d_un含义 // d_val 此 Elf32_Word 对象表示一个整数值,可以有多种解释。 // d_ptr 此 Elf32_Addr 对象代表程序的虚拟地址。 // 如前所述,文件的 虚拟地址可能与执行过程中的内存虚地址不匹配。 // 在解释包含于动态 结构中的地址时,动态链接程序基于原来文件值和内存基地址计算实 际地址。 // 为了保持一致性,文件中不包含用来“纠正”动态结构中重定位项地址的重定位项目。 printf("标志 类型 值\n"); for(int i=0;i<dynamic_count;i++){ printf("%08x ",pdynamic_links[i].d_tag); const char* types[100]={ "DT_NULL","DT_NEEDED","DT_PLTRELSZ","DT_PLTGOT", "DT_HASH","DT_STRTAB","DT_SYMTAB","DT_RELA"}; if(pdynamic_links[i].d_tag <100) { printf("%10s\t",types[pdynamic_links[i].d_tag]); }else{ printf("%10s\t",types[99]); } // 显示值-动态链接库 if(pdynamic_links[i].d_tag == DT_NEEDED){ char *so_name = (char *)(pdynamic_links[i].d_un.d_val + dynamic_string_tables); printf("%10s\t",so_name); }else{ printf("%08x\t",pdynamic_links[i].d_un.d_val); } printf("\n"); } }
10 ELF程序头表
-
程序头表记录了程序运行时那些段需要加载到内存中
typedef struct { Elf32_Word p_type; /* Segment type */ Elf32_Off p_offset; /* Segment file offset */ Elf32_Addr p_vaddr; /* Segment virtual address */ Elf32_Addr p_paddr; /* Segment physical address */ Elf32_Word p_filesz; /* Segment size in file */ Elf32_Word p_memsz; /* Segment size in memory */ Elf32_Word p_flags; /* Segment flags */ Elf32_Word p_align; /* Segment alignment */ } Elf32_Phdr;
-
类型书说明
类型 | 值 | 说明 |
---|---|---|
PT_NULL | 0 | 此数组元素未用。结构中其他成员都是未定义的。 |
PT_LOAD | 1 | 此数组元素给出一个可加载的段,段的大小由 p_filesz 和 p_memsz 描述。文件中的字节被映射到内存段开始处。如果 p_memsz 大于 p_filesz,“剩余”的字节要清零。p_filesz 不能大于 p_memsz。可加载 的段在程序头部表格中根据 p_vaddr 成员按升序排列 |
PT_DYNAMIC | 2 | 数组元素给出动态链接信息。 |
PT_INTERP | 3 | 数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被 当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能 在共享目标文件上发生) 。在一个文件中不能出现一次以上。如果存在 这种类型的段,它必须在所有可加载段项目的前面 ld-linux.so.6 |
PT_NOTE | 4 | 此数组元素给出附加信息的位置和大小 |
PT_SHLIB | 5 | 此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI 不符 |
PT_PHDR | 6 | 此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置, 既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出 现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起 作用。如果存在此类型段,则必须在所有可加载段项目的前面 |
PT_LOPROC | 0x70000000 | 此范围的类型保留给处理器专用语义 |
PT_HIPROC | 0x7fffffff | 此范围的类型保留给处理器专用语义 |
-
程序解析
void parse_elf_pragram(char *pbuff) { // 获取程序段 Elf32_Phdr *phdr = get_elf_pragram(pbuff); // 程序段个数 Elf32_Ehdr *ehdr = get_elf_head(pbuff); int pragram_count = ehdr->e_phnum; // 遍历所有程序段 printf("类型\t\t 偏移 虚拟地址 物理地址 文件大小 内存大小 标志 对齐\n"); for (int i = 0; i < pragram_count; i++) { const char *types[20] = {"PT_NULL", "PT_LOAD", "PT_DYNAMIC", "PT_INTERP", "PT_NOTE", "PT_SHLIB", "PT_PHDR"}; if (phdr[i].p_type < 20) { printf("%-15s", types[phdr[i].p_type]); } else { printf("%08x ", phdr[i].p_type); } // 偏移 printf("%08x ", phdr[i].p_offset); // 虚拟地址 printf("%08x ", phdr[i].p_vaddr); // 物理地址 printf("%08x ", phdr[i].p_paddr); //文件大小 printf("%08x ", phdr[i].p_filesz); // 内存大小 printf("%08x ", phdr[i].p_memsz); // 标志 printf("%04d ",phdr[i].p_flags); // 对齐 printf("%04d ",phdr[i].p_align); printf("\n"); } }
11 GOT(.got.plt)全局偏移表(PE中的IAT表)
-
位置独立的代码一般不能包含绝对的虚拟地址。全局偏移表在私有数据中包含绝对 地址,从而使得地址可用,并且不会影响位置独立性和程序代码的可共享性。程序使用 位置独立的寻址引用其全局偏移表,并取得绝对值,从而把位置独立的引用重定向到绝 对位置。
-
全局偏移表中最初包含其重定位项中要求的信息。在系统为可加载目标创建内存段 以后,动态链接器要处理重定位项,其中有一些重定位项的类型是 R_386_GLOB_DAT, 是对全局偏移表的引用。动态链接器确定相关的符号取值,计算其绝对地址,并将相应 的内存表格项目设置为正确的数值。尽管在链接编辑器构造一个目标文件时还无法知道 绝对地址,动态链接器清楚所有内存段的地址,因而能够计算其中所包含的符号的绝对 地址。
-
如果程序需要直接访问某个符号的绝对地址,那么该符号就会具有一个全局偏移表项。由于可执行文件和共享目标具有独立的全局偏移表,一个符号的地址可能出现在多 个表中。动态链接器在将控制交给进程映像中任何代码之前,要处理所有的全局偏移表重定位,因而确保了执行过程中绝对地址信息可用。
-
表项 0 是保留的,用来存放动态结构的地址,可以用符号_DYNAMIC 引用之。这样, 类似动态链接器这种程序能够在尚未处理其重定位项的时候先找到自己的动态结构。对 于动态链接器而言这点很重要,因为它必须能够在不依赖其他程序来对其内存映像进行 重定位的前提下,初始化自己。在 32 位 Intel 体系结构下,全局偏移表中的表项 1 和 2 也是保留的。
-
全局偏移表的格式和解释都是和处理器相关的。对于 32 位 Intel 体系结构而言, 符号GLOBAL_OFFSET_TABLE_ 可以用来访问该表 extern Elf32_Addr _GLOBAL_OFFSET_TABLE[];
-
符号 GLOBAL_OFFSET_TABLE_ 可能存在于 .got 节区的中间,允许使用负的/ 非负的下标来访问地址数组。
-
使用的外部函数(so)都被保存再这个表中 可以hook
12 PLT 过程连接表
-
全局偏移表(GOT,)用来将位置独立的地址计算重定向到绝对位置,与此相似, 过程链接表(PLT)能够把位置独立的函数调用重定向到绝对位置。链接编辑器物能解 析从一个可执行文件/共享目标到另一个可执行文件/共享目标控制转移(例如函数调 用) 。因此,链接编辑器让程序把控制转移给过程链接表中的表项。在 System V 中,过程链接表位于共享正文中,不过它们使用位于私有的全局偏移表中的地址。
-
动态链接器能够确定目标处的绝对地址,并据此修改全局偏移表的内存映像。动态链接器因此能够对表项进行重定位,并且不会影响程序代码的位置独立性和可共享性。 可执行文件和共享目标文件拥有各自独立的过程链接表。
-
通过一个例子来观察GOT和PLT表的作用
.got.plt中保存的是数据,为每个动态调用保存一个条目,条目的内容应该是对动态库函数的调用所跳转到的目标地址。由于Linux采用了延迟绑定技术,可执行文件中got.plt中的地址并不是目标地址,而是动态链接器(ld-linux.so)中的地址。在程序执行的第一次调用时,ld-linux把.got.plt的地址填写正确,之后的调用,就可以使用.got.plt中的目标地址了。.plt段中的内容则是实现跳转操作的代码片段。
偏移 说明 GOT[0] 存放了指向可执行文件动态段的地址(Elf32_Dyn *),动态链接器利用该地址提取动态链接相关的信息。 GOT[1] 存放link_map结构的地址,动态链接器利用 该地址来对符号进行解析。 GOT[2] 存放了指向动态链接器_dl_runtime_resolve() 函数的地址,该函数用来解析共享库函数的实际符号地 GOT[3] printf真实函数地址 -
1 测试代码 main.c
#include<stdio.h> int main() { printf("hello"); printf("hello"); return 0; } // gcc main.c -g -m32 -o main
-
2 反汇编代码
- objdump -d main
0804840b <main>: 804840b: 8d 4c 24 04 lea 0x4(%esp),%ecx 804840f: 83 e4 f0 and $0xfffffff0,%esp 8048412: ff 71 fc pushl -0x4(%ecx) 8048415: 55 push %ebp 8048416: 89 e5 mov %esp,%ebp 8048418: 51 push %ecx 8048419: 83 ec 04 sub $0x4,%esp 804841c: 83 ec 0c sub $0xc,%esp 804841f: 68 d0 84 04 08 push $0x80484d0 8048424: e8 b7 fe ff ff call 80482e0 <printf@plt> 8048429: 83 c4 10 add $0x10,%esp 804842c: 83 ec 0c sub $0xc,%esp 804842f: 68 d0 84 04 08 push $0x80484d0 8048434: e8 a7 fe ff ff call 80482e0 <printf@plt> 8048439: 83 c4 10 add $0x10,%esp 804843c: b8 00 00 00 00 mov $0x0,%eax 8048441: 8b 4d fc mov -0x4(%ebp),%ecx 8048444: c9 leave 8048445: 8d 61 fc lea -0x4(%ecx),%esp 8048448: c3 ret 8048449: 66 90 xchg %ax,%ax 804844b: 66 90 xchg %ax,%ax 804844d: 66 90 xchg %ax,%ax 804844f: 90 nop ;plt 过程链接 080482e0 <printf@plt>: ;*表示取内容,0x804a00c 是plt数组中的元素 80482e0: ff 25 0c a0 04 08 jmp *0x804a00c ;取得0x804a00c地址上的值; 第一次存放的是 0x80482e6 80482e6: 68 00 00 00 00 push $0x0 ;函数id =0 实际上GOT[3] 80482eb: e9 e0 ff ff ff jmp 80482d0 <_init+0x24> ;解析符号填充到正确地址 080482d0 <printf@plt-0x10>: 80482d0: ff 35 04 a0 04 08 pushl 0x804a004 ; 80482d6: ff 25 08 a0 04 08 jmp *0x804a008 ;调用ld.so动态链接解析模块中的函数,填充到got.plt表中 (0x804a00c地址中),这样第二次调用printf,直接找到的printf函数 80482dc: 00 00 add %al,(%eax)
objdump -s main
;0x804a00c 内容是 0x8040286e 相当于跳转回原来地址下一个位置 Contents of section .got: 8049ffc 00000000 .... Contents of section .got.plt: 804a000 149f0408 00000000 00000000 e6820408 ................ 804a010 f6820408 .... Contents of section .data: 804a014 00000000 00000000 ........
-
objdump -R main
main: 文件格式 elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ffc R_386_GLOB_DAT __gmon_start__ ;got 0804a00c R_386_JUMP_SLOT printf@GLIBC_2.0 ;got 0804a010 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0 ;got
-
执行gdb 跟踪调试
- b main
- display/i $pc
- r
- si 进入到printf@plt
- x/20x 0x804a000 查看got.plt表
(gdb) x/20x 0x804a000 ;0xf7fee000 in ?? () from /lib/ld-linux.so.2 0x804a000: 0x08049f14 0xf7ffd918 0xf7fee000 0x080482e6; 是重点0x804a00c 0x804a010: 0xf7e16550 0x00000000 0x00000000 0x00000000 0x804a020: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a030: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a040: 0x00000000 0x00000000 0x00000000 0x00000000
- finish
- 执行完函数我们再观察0x804a00c(got.plt表)
;0x804a00c 中的值变成 0xf7e47680 ,这个值就是函数的真正地址 0x804a000: 0x08049f14 0xf7ffd918 0xf7fee000 0xf7e47680 ; 是重点0x804a00c 0x804a010: 0xf7e16550 0x00000000 0x00000000 0x00000000 0x804a020: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a030: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a040: 0x00000000 0x00000000 0x00000000 0x00000000
-
13 fPIC与地址无关
-
在编译程序时加入 -fPIC (支持动态链接库)
-
测试程序
#include <stdio.h> int add(int a,int b){ return a+b; } int main(){ int sum = add(100,200); printf("%d\n",sum); return 0; }
-
普通编译-反汇编
gcc -g -m32 main.c -o main
.text:08048418 main proc near ; .text:08048418 .text:08048418 var_C = dword ptr -0Ch .text:08048418 var_4 = dword ptr -4 .text:08048418 .text:08048418 lea ecx, [esp+4] .text:0804841C and esp, 0FFFFFFF0h .text:0804841F push dword ptr [ecx-4] .text:08048422 push ebp .text:08048423 mov ebp, esp .text:08048425 push ecx .text:08048426 sub esp, 14h .text:08048429 push 0C8h .text:0804842E push 64h .text:08048430 call add .text:08048435 add esp, 8 .text:08048438 mov [ebp+var_C], eax .text:0804843B sub esp, 8 .text:0804843E push [ebp+var_C] .text:08048441 push offset format ; "%d\n" 全局变量使用绝对地址 .text:08048446 call _printf ;printf("%d",add(100,200)) .text:0804844B add esp, 10h .text:0804844E mov eax, 0 .text:08048453 mov ecx, [ebp+var_4] .text:08048456 leave .text:08048457 lea esp, [ecx-4] .text:0804845A retn .text:0804845A main endp
-
加入与地址无关选项
gcc -g -m32 main.c -fPIC -o main
.text:08048422 public main .text:08048422 main proc near ; .text:08048422 .text:08048422 var_C = dword ptr -0Ch .text:08048422 .text:08048422 lea ecx, [esp+4] .text:08048426 and esp, 0FFFFFFF0h .text:08048429 push dword ptr [ecx-4] .text:0804842C push ebp .text:0804842D mov ebp, esp .text:0804842F push ebx .text:08048430 push ecx .text:08048431 sub esp, 10h .text:08048434 call __x86_get_pc_thunk_bx ;获取当前eip地址 .text:08048439 add ebx, 1BC7h ;当前离全局“%d”数据偏移值 .text:0804843F sub esp, 8 .text:08048442 push 0C8h .text:08048447 push 64h .text:08048449 call add .text:0804844E add esp, 10h .text:08048451 mov [ebp+var_C], eax .text:08048454 sub esp, 8 .text:08048457 push [ebp+var_C] .text:0804845A lea eax, (unk_8048500 - 804A000h)[ebx];计算全局数据所在位置 .text:08048460 push eax ; format .text:08048461 call _printf .text:08048466 add esp, 10h .text:08048469 mov eax, 0 .text:0804846E lea esp, [ebp-8] .text:08048471 pop ecx .text:08048472 pop ebx .text:08048473 pop ebp .text:08048474 lea esp, [ecx-4] .text:08048477 retn ;计算当前指令位置 .text:08048340 public __x86_get_pc_thunk_bx .text:08048340 __x86_get_pc_thunk_bx proc near ; CODE XREF: _init_proc+4 .text:08048340 mov ebx, [esp+0] .text:08048343 retn .text:08048343 __x86_get_pc_thunk_bx endp
14 ELF程序加载流程(参考linux4.13.6源码)
- 在线源码:https://lxr.missinglinkelectronics.com/
- 国内源码下载:http://ftp.sjtu.edu.cn/sites/ftp.kernel.org/pub/linux/kernel/
- 官方源码:https://www.kernel.org/
14.1 运行一个程序的过程函数
函数调用过程 | 说明 | 文件位置 |
---|---|---|
execve() | 运行可执行文件 | linux/fs/exec.c line:1910 |
do_execve() | 系统无关 | linux/fs/exec.c line:1828 |
do_execveat_common() | 启动进程相关数据 | linux/fs/exec.c line:1682 |
exec_binprm () | 识别可执行文件格式 | linux/fs/exec.c line:1657 |
search_binary_handler () | 查找可执行文件的处理函数 | linux/fs/exec.c line:1604 |
load_binary() | 加载可执行文件(linux 支持多种格式目前只看elf) | linux/fs/exec.c line:1604 |
load_elf_binary() | 加载elf文件格式 | linux/fs/binfmt_elf.c line 679 |
start_thread() | 启动进程 | linux/fs/binfmt_elf.c line 1148 |
其中可执行文件存放与linux/fs/binfmts.h , 常见格式
格式 | 格式定义 | 加载函数 |
---|---|---|
elf | elf_format | load_elf_binary |
script脚本 | script_format | load_scrip |
a.out | aout_format | load_aout_binary |
14.2 加载ELF文件
-
ElF文件的加载从load_elf_binary函数开始
-
load_elf_binary加载流程概述
- 01 读取目标程序并且检查ELF头部
- 02 load_elf_phdrs加载目标程序的程序头表
- 03 如果需要动态链接器(ld-linux.so), 则寻找和处理解释器段
- 04 检查并读取解释器的程序表头
- 05 装入目标程序的段(PT_LOAD) segment
- **06 填写程序的入口地址 elf_entry **
- 07 create_elf_tables填写目标文件的参数环境变量等必要信息
- 08 start_thread 进入新的程序入口
-
01 读取目标程序并且检查ELF头部 ( linux/fs/exec.c line:1604)
struct pt_regs *regs = current_pt_regs(); struct { struct elfhdr elf_ex; struct elfhdr interp_elf_ex; } *loc; struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE; loc = kmalloc(sizeof(*loc), GFP_KERNEL); if (!loc) { retval = -ENOMEM; goto out_ret; } /* Get the exec-header 使用映像文件的前128个字节对bprm->buf进行了填充 */ loc->elf_ex = *((struct elfhdr *)bprm->buf); retval = -ENOEXEC; /* First of all, some simple consistency checks 比较文件头的前四个字节 。*/ if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) goto out; /* 还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库 */ if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) goto out;
-
02 load_elf_phdrs加载目标程序的程序头表
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file); if (!elf_phdata) goto out;
/** * load_elf_phdrs() - load ELF program headers * @elf_ex: ELF header of the binary whose program headers should be loaded * @elf_file: the opened ELF binary file * * Loads ELF program headers from the binary file elf_file, which has the ELF * header pointed to by elf_ex, into a newly allocated array. The caller is * responsible for freeing the allocated data. Returns an ERR_PTR upon failure. */ static struct elf_phdr *load_elf_phdrs(struct elfhdr *elf_ex, struct file *elf_file) { struct elf_phdr *elf_phdata = NULL; int retval, size, err = -1; /* * If the size of this structure has changed, then punt, since * we will be doing the wrong thing. */ if (elf_ex->e_phentsize != sizeof(struct elf_phdr)) goto out; /* Sanity check the number of program headers... */ if (elf_ex->e_phnum < 1 || elf_ex->e_phnum > 65536U / sizeof(struct elf_phdr)) goto out; /* ...and their total size. */ size = sizeof(struct elf_phdr) * elf_ex->e_phnum; if (size > ELF_MIN_ALIGN) goto out; elf_phdata = kmalloc(size, GFP_KERNEL); if (!elf_phdata) goto out; /* Read in the program headers */ retval = kernel_read(elf_file, elf_ex->e_phoff, (char *)elf_phdata, size); if (retval != size) { err = (retval < 0) ? retval : -EIO; goto out; } /* Success! */ err = 0; out: if (err) { kfree(elf_phdata); elf_phdata = NULL; } return elf_phdata; }
-
03 如果需要动态链接, 则寻找和处理解释器段
- 使用动态编译时才会存在解释器(PT_INTERP段) -dynamic-linker
- 使用静态编译时不会出现解释器(PT_INTERP段) -static
- PT_INTERP 通常是lib/ld-linux-xxxx.so.2,程序运行后主动权交给这个文件
for (i = 0; i < loc->elf_ex.e_phnum; i++) { /* 3.1 检查是否有需要加载的解释器 如果存在PT_INIERP就需要保存解释器 */ if (elf_ppnt->p_type == PT_INTERP) { /* This is the program interpreter used for * shared libraries - for now assume that this * is an a.out format binary */ /* 3.2 根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区 */ retval = kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz); if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0') goto out_free_interp; /* 3.3 通过open_exec()打开解释器文件 */ interpreter = open_exec(elf_interpreter); /* Get the exec headers 3.4 通过kernel_read()读入解释器的前128个字节,即解释器映像的头部。*/ retval = kernel_read(interpreter, 0, (void *)&loc->interp_elf_ex, sizeof(loc->interp_elf_ex)); break; } elf_ppnt++; }
-
04 检查并读取解释器的程序表头
/* 4. 检查并读取解释器的程序表头 */ /* Some simple consistency checks for the interpreter 4.1 检查解释器头的信息 */ if (elf_interpreter) { retval = -ELIBBAD; /* Not an ELF interpreter */ /* Load the interpreter program headers 4.2 读入解释器的程序头 */ interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex, interpreter); if (!interp_elf_phdata) goto out_free_dentry;
-
05 装入目标程序的段segment
*/ for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { /* 5.1 搜索PT_LOAD的段, 这个是需要装入的 */ if (elf_ppnt->p_type != PT_LOAD) continue; /* 5.2 检查地址和页面的信息 */ // ...... /* 5.3 虚拟地址空间与目标映像文件的映射 确定了装入地址后, 就通过elf_map()建立用户空间虚拟地址空间 与目标映像文件中某个连续区间之间的映射, 其返回值就是实际映射的起始地址 */ error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size); }
-
06 填写程序的入口地址
if (elf_interpreter) { unsigned long interp_map_addr = 0; elf_entry = load_elf_interp(&loc->interp_elf_ex, interpreter, &interp_map_addr, load_bias, interp_elf_phdata); /* 入口地址是解释器映像的入口地址 */ } else { /* 入口地址是目标程序的入口地址 */ elf_entry = loc->elf_ex.e_entry; } }
-
07 create_elf_tables填写目标文件的参数环境变量等必要信息
install_exec_creds(bprm); retval = create_elf_tables(bprm, &loc->elf_ex, load_addr, interp_load_addr); if (retval < 0) goto out; /* N.B. passed_fileno might not be initialized? */ current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->start_data = start_data; current->mm->end_data = end_data; current->mm->start_stack = bprm->p;
-
08 start_thread 进入新的程序入口
start_thread(regs, elf_entry, bprm->p); retval = 0; out: kfree(loc); out_ret: return retval; /* error cleanup */ out_free_dentry: kfree(interp_elf_phdata); allow_write_access(interpreter); if (interpreter) fput(interpreter); out_free_interp: kfree(elf_interpreter); out_free_ph: kfree(elf_phdata); goto out; }
评论区