ELF文件作用:
第一个是告诉内核将磁盘上的 ELF 文件中的内容放置在内存中的位置,以及提供调用动态加载器函数的方法,甚至可能帮助提供一些调试信息。
ELF Header and Identification fields(ELF 标头和标识字段)
typedef struct {
uint8_t e_ident[16];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint64_t e_entry;
uint64_t e_phoff;
uint64_t e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} Elf64_Ehdr; e_ident文件之后的下一个文件是e_type 。在上面的示例中,我声称该类型是EXEC之一(因为它读取0x02 0x00 ) - 根据 ELF 标准,这意味着它要被执行(检查标准将确认这一点)。
e_entry字段列出了文件中程序应开始执行的偏移量。通常它指向您的_start方法(当然,如果您使用通常的东西编译它)。您可以将e_entry指向您喜欢的任何位置,
e_flags字段包含许多特定于体系结构的值,有时还指示有关 ABI 的信息。
e_shstrndx字段保存节头表中.shstrtab的索引。该节只是节名称的数组(也由 readelf 使用),提供一些解释语义。
Program Headers
typedef struct {
uint32_t p_type;
uint32_t p_flags;
uint64_t p_offset;
uint64_t p_vaddr;
uint64_t p_paddr;
uint64_t p_filesz;
uint64_t p_memsz;
uint64_t p_align;
} Elf64_Phdr; 程序头(以及其他功能)通常用于告诉内存加载器将东西放在哪里。
程序头有多种类型。他们每个人都有不同的目的:
- 0x00000006 PHDR - 指示程序头表本身的开始。根据文档,本节需要一个可加载段条目,但在这里我们看到它是通过PT_INTERP进行的,这意味着这不是真的!
- 0x00000003 PT_INTERP - 此部分指示将作为 ELF 解释器调用的程序路径名(如果它是可执行文件)。
- 0x00000001 PT_LOAD - 最重要的程序头类型。定义必须将文件的一部分放置在内存中的方式。
Section Headers
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
uint64_t sh_addr;
uint64_t sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
} Elf64_Shdr; sh_name - 包含节名称的.strtab的索引
sh_type - 节类型( SHT_NULL、SHT_DYN 、...)
sh_flags - 执行期间此部分的内存属性( SHF_WRITE、SHF_ALLOC 、...)
sh_addr - 文件中此部分开始的地址
sh_link - 将一个节与此节关联,字段值可以取决于sh_type
.symtab 主符号表
.interp 解释器的路径名
.dynsym 动态符号表
.shstrtab实际上是用来引用各部分的名称的。
sh_type 的字段
- SHT_NULL - 纯粹用于存储空字节,文档将其称为直接将节标记为未使用,并且很可能会被大多数语义驱动的 ELF 实用程序跳过。
- SHT_PROGBITS - 这只是一个部分的标记,表示它可以包含任何内容,其格式通常由正在执行的程序决定。 PROGBIT几乎适用于程序特定的行为 - 可以是任何东西 - 字面上任何东西,甚至图灵完成任何东西!这些通常用于标记包含要执行的实际代码、数据部分、初始化/终结过程的部分(或者甚至可能是特定于 ABI 或生成可执行代码部分和伴奏的编译器的更狂野的概念 - 同样,此部分类型不确实施加了很多格式控制)
- SHT_SYMTAB -这提供了一个指向应该具有符号表格式的部分的指针
- SHT_STRTAB - 保存以 null 结尾的字符串列表的部分。
- SHT_HASH -此部分用于保存哈希表,通常是为了加快查找符号的速度。事实上,文档表明,如果可执行文件参与动态链接,它必须具有这些部分之一。
- SHT_NOTE类型部分是简单的整数列表,为供应商提供版本控制和类型。
看看十六进制转储中典型的SHT_STRTAB是什么样的:
正如您所看到的,字符串很好,并且由空字节整齐地分隔,在 C 中读取字符串时非常容易不会弄乱它:)))。.shstrtab包含节名称,
通过移动shstrtab部分的开头,我们看到字符串对于每个条目向下跳转 8 个字节。更准确地说,我们可以说它们都是从 8 个字节开始,但因为它们是字符串,所以readelf会读取字节直到遇到空字节。例如,我们可以看到第一个节名称而不是原来位于0x1910的.interp现在指向0x1917 。 .interp部分通常是第一个有效部分,现在称为.note.ABI-tag。接下来的节名称(从 8 个字节开始)是I-tag (因为它从0x191F开始),然后读取直到它到达0x1924处的空字节。其余的节遵循相同的模式
C Start Up
_start方法本质上需要做的是对__libc_start_main执行函数调用,这意味着我们需要一个指向 main 函数的指针作为__libc_start_main 的参数
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end) - int (*main) 这是指向二进制文件中 main 方法的指针
- int argc 从命令行传递给二进制文件的参数数量,包括二进制文件的名称
- char **argv 保存实际字符串的数组,记住这里的一些术语很重要,argv 本质上是通过堆栈指针传递给_start函数的。
- __typeof (main) init 指向处理调用初始化程序或构造函数的函数 ( __libc_csu_init ) 的指针。我将其称为构造函数“调用处理程序”
- void (*fini) (void) 这是一个类似的函数指针,指向处理调用析构函数的函数指针。
- void (*rtld_fini) 动态链接器的析构函数调用处理程序,该值通过edx从加载器传递给_start
- void *stack_end 堆栈结束标记
__libc_start_main在此之后所做的事情还有一些其他有趣的细节,其中一些涉及深层精灵魔法,例如读取 argv 的值来查找 envp。互联网上有关于这方面的精彩文章,并且__libc_start_main的代码也可用。我想你们会喜欢确认它按描述工作的练习。
总结__libc_start_main ,并将.init_array和.fini_array引入上下文。本质上,start_main 所做的事情是这样的:
- Setup stack guard(设置堆栈保护):
- Register destructors(注册析构函数(包括 rtld 的析构函数),以便调用它们):
- 检查文件描述符STDIO STDERR STDIN设置是否正确:
其他一些很酷的东西,当然最终会调用(*init) ,它在start_main的上下文中意味着__libc_csu_init。正如我们在脚注中看到的,这个函数实际上调用了我们定义的 init 函数。
Symbol Table and Relocations
typedef struct {
uint32_t st_name;
uint8_t st_info;
uint8_t st_other;
uint16_t st_shndx;
uint64_t st_value;
uint64_t st_size;
} Elf64_Sym; 有两个符号表,即主符号表(节标题中的.symtab )和动态符号表.dynsym ,它只是主符号表中条目的较小子集。这是一个较小的副本,仅与动态链接器相关。
- st_name - .strtab中的索引,保存符号的空终止名称中的第一个字节。并非所有符号都有名称,如果没有名称,则此部分的值为0x0000 。
- st_info - 确定符号的一些属性的位字段。即 C 程序中的“范围”和符号类型,旨在帮助重定位。它将指示它是函数、变量还是其他东西。其工作方式与真正的 C 风格中的每个位字段非常相似,它通过宏传递。该宏应用位掩码、移位来隔离专用于某些属性的位字段中的偏移量。
- st_other - 这是一个用于确定符号可见性的位字段。控制如何允许代码在特定上下文中引用变量的属性。这是 glibc 用于提取可见性值的宏:
617 /* How to extract and insert information held in the st_other field. */
618
619 #define ELF32_ST_VISIBILITY(o) ((o) & 0x03)
620
621 /* For ELF64 the definitions are the same. */
622 #define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY (o) 符号的可见性类型包括(也可从上图中获得):
- STV_DEFAULT 0x00 - 这意味着这是默认的可见性规则
- STV_INTERNAL 0x01 - 处理器特定的隐藏类
- STV_HIDDEN 0x02 - 表示该符号不可用于其他模块中的参考
- STV_PROTECTED 0x03 - 文档将其称为受保护符号。我相信这个符号和普通的STV_DEFAULT符号之间唯一的区别是,当从它自己的共享库中引用它时,它不允许被覆盖。
- st_shndx字段指示与该符号关联的节索引。符号以这种方式与节相关联,因为定义为符号的所有内容都可能有一个关联的节 -例如变量值将在哪里定义?可能是.data *-esq 部分不是?有几个特殊的节号,表示与符号相关的节的一些信息,这些节号可以采用几个值,请查看glibc/elf.h:414+以了解这些值的范围。
- st_value符号的值,根据符号类型有不同的解释:
- 在可执行文件和共享对象中,该文件保存符号定义的虚拟地址。
- 对于可重定位文件,该值在很大程度上指示定义符号的位置的偏移量。
- 对于st_shndx是SHN_COMMON的符号, st_value将在重新定位时保留对齐约束
- st_size符号的大小,指示该符号所代表的内容将占用多少字节,这又取决于符号类型 -大多数情况下是变量数据字段的大小或函数代码的大小。