The-ELF-Binary-Format

这篇博客深入探讨了ELF(Executable and Linkable Format)二进制文件的结构,包括文件类型、程序头、节头、符号和重定位。文章详细介绍了PT_LOAD、PT_DYNAMIC等程序头类型,以及.text、.data、.got.plt等节,强调了它们在程序执行和动态链接中的作用。此外,还讨论了动态链接器如何使用辅助向量、PLT和GOT进行动态链接,并解析了动态段在程序加载过程中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

The ELF Binary Format

1. ELF file types

一个ELF文件可以被标记为以下几种类型之一。

  • ET_NONE: 未知类型,不确定或未定义
  • ET_REL:重定位文件, ELF被标记为relocatable意味者该文件被标记一段可重定位的代码,或目标文件。可重定位目标文件通常是还未被链接到可执行程序的一段位置独立的代码Position independent code (PIC)。编译完代码后可以看到.o格式的文件,这种文件包含了创建可执行文件所需要的代码和数据。
  • ET_EXEC: 可执行文件, ELF类型为executable,意味着该文件被标记为可执行文件。 这些类型的文件也称为程序,是一个进程开始执行的入口。
  • ET_DYN: 共享目标文件,ELF类型为dynamic,意味着该文件被标记为了一个动态的可链接的目标文件,也称为共享库。这类共享库会在程序运行时被装载并链接到程序的进程镜像中。
  • ET_CORE: 核心文件,在程序崩溃或者进程传递了一个SIGSEGV信号(分段违规)时,会在核心文件中记录整个进程的镜像消息,可以用GDB读取这类文件来辅助调试并查找程序崩溃的原因。

使用readelf -h 可以查看ELF文件,查看原始的ELF文件头。ELF文件头从文件的0偏移开始,是除了文件头之后剩余部分文件的一个映射。随便找了一个在BSD编辑的EXEC文件,readelf -h 之后如下图

这里写图片描述

ELF头部的结构定义如下:

#define EI_NIDENT 16
           typedef struct {
               unsigned char e_ident[EI_NIDENT];
               uint16_t      e_type;
               uint16_t      e_machine;
               uint32_t      e_version;
               ElfN_Addr     e_entry;
               ElfN_Off      e_phoff;
               ElfN_Off      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;
           } ElfN_Ehdr;

ps:readelf命令

readelf -S <object> //查询节头表
readelf -l <object> //查询程序头表
readelf -s <object> //查询符号表
readelf -e <object> //查询ELF文件头数据
readelf -r <object> //查询重定位入口
readelf -d <object> //查询动态段

2. ELF program headers

ELF程序头是对二进制文件中段的描述,是程序装载必需的一部分。段(segment)是在内存装载时被解析的,描述了磁盘上可执行文件的内存布局以及如何映射到内存中。

程序头描述了可执行文件(包括共享库)中段及其类型(为那种类型的数据或代码而保留的段)。

//ELF32_Phdr结构,构成了32位ELF可执行文件程序头表的一个程序头条目。 program header table
typedef struct {
    uint32_t   p_type;   (segment type)
    Elf32_Off  p_offset; (segment offset)
    Elf32_Addr p_vaddr;   (segment virtual address)
    Elf32_Addr p_paddr;    (segment physical address)
    uint32_t   p_filesz;   (size of segment in the file)
    uint32_t   p_memsz; (size of segment in memory)
    uint32_t   p_flags; (segment flags, I.E execute|read|read)
    uint32_t   p_align;  (segment alignment in memory)
  } Elf32_Phdr;
// 这些变量描述了段在文件和内存中的布局

下面讨论5个常见的程序头类型。

1.PT_LOAD

一个可执行文件至少有一个PT_LOAD类型的段

这类程序头描述的是可装载的段(a loadable segment),即这种类型的段将被装载或映射到内存中。

一个需要动态链接的ELF可执行文件通常包含以下两个可装载的段(类型为PT_LOAD)

  • 存放程序代码的text段;
  • 存放全局变量和动态链接信息的data段。

上面的两个段将会被映射到内存中,并根据p_align中存放的值在内存中对齐。

2.PT_DYNAMIC-动态段的Phdr

动态段是动态链接可执行文件所特有的,包含了动态链接器所必需的一些信息。在动态段中包含了一些标记值和指针,包括但不限于以下内容:

  • 运行时需要链接的共享库列表;
  • 全局偏移表(GOT,Global offset table)的地址;
  • 重定位条目的相关信息。

Following is complete list of the tag names:完整的标记名列表

Tag name/标记名描述
DT_HASH符号散列表的地址
DT_STRTAB字符串表的地址
DT_SYMTAB符号表的地址
DT_RELA相对地址重定位表的地址
DT_RELASZRela表的字节大小
DT_RELAENTRela表条目的字节大小
DT_STRSZ字符串表的字节大小
DT_SYMENT符号表条目的字节大小
DT_INIT初始化函数的地址
DT_FINT终止函数的地址
DT_SONAME共享目标文件名的字符串表偏移量
DT_RPATH库搜索路径的字符串表偏移量
DT_SYMBOLIC修改链接器,在可执行文件之前的共享目标文件中搜索符号
DT_RELRel relocs表的地址
DT_RELSZRel表的字节大小
DT_RELENTRel表条目的字节大小
DT_PLTRELPLT引用的reloc类型(Rela或Rel)
DT_DEBUG还未进行定义,为测试保留
DT_TEXTREL缺少此项表明重定位只能应用于可写段
DT_JMPREL仅用于PLT的重定位条目地址
DT_BIND_NOW指示动态链接器在将控制权交给可执行文件之前处理所有的重定位
DT_RUNPATH库搜索路径的字符串表偏移量

动态段包含了一些结构体,在这些结构中存放着与动态链接相关的信息。

//32位ELF文件的动态段结构体:d_tag成员变量控制着d_un的含义。
typedef struct {
Elf32_Sword    d_tag;
    union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
    } d_un;
} Elf32_Dyn;
extern Elf32_Dyn _DYNAMIC[];

3. PT_NOTE

PT_NOTE类型的段可能保存了与特定供应商或者系统相关的附加消息。

//ELF规范中的定义
Sometimes a vendor or system builder needs to mark an object file with special information that other programs will check for conformance, compatibility, and so on. Sections of type SHT_NOTE and program header elements of type PT_NOTE can be used for this purpose. (SHT_NOTE类型的节section和PT_NOTE类型的程序头元素可以用于这一目的)The note information in sections and program header elements holds any number of entries, each of which is an array of 4-byte words in the format of the target processor. Labels appear below to help explain note information organization, but they are not part of the specification.

这一段只保存了操作系统的规范信息,在可执行文件运行时是不需要这个段的。(since the system will just assume the executable is native either way)。 容易成为病毒感染的一个地方。
NOTE段病毒(http://vxheavens.com/lib/vhe06.html

4. PT_INTERP

PT_INTERP段只将位置和大小信息存放在一个以null为终止符的字符串中,是对程序解释器位置的描述。例如 /lib/linux-ld.so.2一般是指动态链接器的位置,即程序解释器的位置。

5. PT_PHDR

PT_PHDR段保存了程序头表本身的位置和大小。

Phdr表保存了所有的Phdr对文件(以及内存镜像)中段的描述信息。

使用readelf -l 命令查看文件的Phdr表:

这里写图片描述

中间部分的PT_LOAD段,从最左边的偏移量到最右边的权限标识和对齐标识。

text段是可读可执行的,data段是可读可写的,这两个段都有0x1000(4096)对齐标识,刚好是32位可执行文件一页的大小,该标识用于在程序装载时对齐。

3. ELF section headers

段是程序执行的必要组成部分,在每个段中,会有代码或者数据被划分为不同的节。节头表是对这些节的位置和大小的描述,主要用于链接和调试。

节头对于程序的执行来说不是必需的,没有节头表,程序仍可以正常执行,因为节头表没有对程序的内存布局进行描述,程序头表才对程序内存布局的描述。节头是对程序头的补充。(The section headers are really just complimentary to the program headers. )

同样可以使用readelf -l 查看节

如果二进制文件缺少节头,不是说节不存在,只是没办法通过节头来引用节,对于调试器或反编译程序,只是可参考的信息变少。

每一个ELF文件都有节,但是不一定有节头,可故意将节头从节头表中删去,默认是有节头的。

All of GNU’s binutils such as objcopy, objdump, and other tools such as gdb rely on the section headers to locate symbol information that is stored in the sections specific to containing symbol data.(需要依赖节头定位到存储符号数据的节来获取符号信息) Without section headers, tools such as gdb and objdump are nearly useless.

//32位ELF节头的结构
typedef struct {
uint32_t   sh_name; // offset into shdr string table for shdr name
    uint32_t   sh_type; // shdr type I.E SHT_PROGBITS
    uint32_t   sh_flags; // shdr flags I.E SHT_WRITE|SHT_ALLOC
    Elf32_Addr sh_addr;  // address of where section begins
    Elf32_Off  sh_offset; // offset of shdr from beginning of file
    uint32_t   sh_size;   // size that section takes up on disk
    uint32_t   sh_link;   // points to another section
    uint32_t   sh_info;   // interpretation depends on section type
uint32_t   sh_addralign; // alignment for address of section
uint32_t   sh_entsize;  // size of each certain entries that may be in section
} Elf32_Shdr;

下面是一些比较重要的节和节类型

1 .text节

.text节是保存了程序代码指令的代码节。一段可执行程序,如果存在Phdr,.text节就会存在与text段中。由于.text节保存了程序代码,类型就是SHT_PROGBITS。

2 .rodata节

.rodata节保存了只读的数据。只能存在于一个可执行文件的只读段节,故只能在text段找到,而不是data段找到.rodata节。

egprintf("hello world!\n"); 这条命令就存放在.rodata节。

由于.rodata节是只读的,节类型就是SHT_PROGBITS。

3 .plt节

.plt节中包含了动态链接器调用从共享库导入的函数所必需的相关代码。

由于其存在于text段中,同样保存了代码,因此节类型为SHT_PROGBITS。

4 .data节

.data节存在于data段,保存了初始化的全局变量等数据。

由于其保存了程序的变量数据,因此类型被标记为SHT_PROGBITS。

5 .bss节

.bss节保存了未进行初始化的全局数据,是data段的一部分,占用空间不超过4字节,仅表示这个节本身的空间。

程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于.bss节未保存实际的数据,因此节类型为SHT_NOBITS。

6 .got.plt节

.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享函数的访问入口,由动态链接器在运行时进行修改。

This section in particular is often abused by attackers who gain a pointer-sized write primitive in heap or .bss exploits.

.got.plt节跟程序执行有关,因为节类型被标记为SHT_PROGBITS。

7 .dynsym节

.dynsym节保存了从共享库导入的动态符号信息,包含了描述函数名和偏移量/地址的导入/导出符号。该节保存在段中text段,节类型为SHT_DYNSYM。

8 .dynstr节

.dynstr节保存了动态符号字符串表,表中存放了一系列字符串,这些字符串代表了符号的名称,以空字符作为终止符。

9 .rel.*节

重定位节保存了重定位相关的信息,这些信息描述了如何在链接或者运行时,对ELF目标文件的某部分内容或者进程镜像进行补充或修改。

重定位节保存了重定位相关的数据,节类型被标记为SHT_REL。

10 .hash节

.hash节有时也称为.gnu.hash,保存了一个用于查找符号的散列表。

//散列算法,用来在ELF文件中查找符号名。
uint32_t
dl_new_hash(const char *s)
{
    uint32_t h=5381;
    for(unsigned char c=*s;c!='\0';c=*++s)
        h=h*33+c; //也可以为 h=((h<<5)+h)+c;
    return h;
}

11 .symtab节

.symtab节保存了ElfN_Sym类型的符号信息。

.symtab节保存了符号信息,因此类型被标记为SHT_SYMTAB。

12 .strtab节

.strtab节保存的是符号字符串表,表中的内容会被.symtab的ElfN_Sym结构中的st_name条目引用。

由于其保存了字符串表,因此节类型被标记为SHT_STRTAB。

13 .shstrtab节

.shstrtab节保存节头字符串表,该表是一个以空字符终止的字符串的集合,字符串保存了每个节的节名,如.text,.data等。

有一个名为e_shstrndx的ELF文件头条目会指向.shstrtab节,e_shstrndx中保存了.shstrtab的偏移量。This section is pointed to by the ELF file header entry called e_shstrndx that holds the offset of .shstrtab.

由于其保存了字符串表,因此节类型被标记为SHT_STRTAB。

14 .ctors和.dtors节

.ctors(构造器),.dtors(析构器)这两个节保存了指向构造函数和析构函数的函数指针,构造函数是在main函数执行之前需要执行的代码,析构函数是在main函数之前需要执行的代码。

The __constructor__ function attribute is sometimes used by hackers and virus writers to implement a function that performs an anti-debugging trick such as calling PTRACE_TRACEME so that the process traces itself and no debuggers can attach to it. This way the anti-debugging code gets executed before the program enters into main().

可执行文件是如何使用phdr和shdr进行布局排列:

text段的布局:

  • [.text]: This is the program code 程序代码
  • [.rodata]: This is read-only data 只读数据
  • [.hash]: This is the symbol hash table 符号散列表
  • [.dynsym ]: This is the shared object symbol data 共享目标文件符号数据
  • [.dynstr ]: This is the shared object symbol name 共享目标文件符号名称
  • [.plt]: This is the procedure linkage table 过程链接表
  • [.rel.got]: This is the G.O.T relocation data G.O.T重定位数据

data段的布局:

  • [.data]: These are the globally initialized variables 全局的初始化变量
  • [.dynamic]: These are the dynamic linking structures and objects 动态链接结构和对象
  • [.got.plt]: This is the global offset table 全局偏移表
  • [.bss]: These are the globally uninitialized variables 全局未初始化变量

这里写图片描述

可重定位文件(类型为ET_REL的ELF文件)中不存在程序头,因为.o类型的文件会被链接到可执行文件中,但是不会被直接加载到内存中,故 readelf -l 不能得到想要的结果。

(Linux loadable kernel modules are actually ET_REL objects and are an exception to the rule because they do get loaded directly into kernel memory and relocated on the fly.不过Linux中可加载内核模块LKM例外,LKM是ET_REL类型文件,它是被直接加载进内核的内存中并自动进行重定位。)

这里写图片描述
将test.o编译到可执行文件中,可以看到节头中新增了一些节,如.got.plt,.plt,.dynsym以及其他与动态链接及运行时重定位相关的节。

4 .ELF symbols

符号是对某些类型的数据或者代码(如全局变量或函数)的符号引用。eg:printf()函数会在动态符号表.dynsym中存在有一个指向该函数的符号条目。

在大多数共享库和动态链接可执行文件中,存在两个符号表:.dynsym和.symtab。

.dynsym保存了引用来自外部文件符号的全局符号,如printf这样的库函数。

.dynsym保存的符号是.symtab所保存符号的子集。

.symtab中还保存了可执行文件的本地符号,而.dynsym只保存了动态/全局符号。

使用readelf -S查看可执行文件的节头表输出,.dynsym是被标记了ALLOC的,而.symtab没有标记。

ALLOC表示有该标记的节会被运行时分配并装载进入内存,而.symtab不是在运行时必需的,因此不会被装载进入内存。

.dynsym保存的符号只能在运行时被解析,因此是运行时动态链接器所需要的唯一符号。.dynsym符号表对于动态链接可执行文件的执行来说是必需的。而.symtab符号表只是用来进行调试和链接的,有时候为了节约空间,会将.symtab符号表从产生二进制的文件中删去。

//64位ELF文件符号项的结构
//符号项保存在.symtab 和 .dynsym节中,因此节头项的大小与ElfN_Sym的大小相等。
typedef struct {
uint32_t      st_name;
    unsigned char st_info;
    unsigned char st_other;
    uint16_t      st_shndx;
    Elf64_Addr    st_value;
    Uint64_t      st_size;
} Elf64_Sym;
//Symbol entries are contained within the .symtab and .dynsym sections, which is why the sh_entsize (section header entry size) for those sections are equivalent to sizeof(ElfN_Sym).

1.st_name

st_name保存了指向符号表中字符串表(位于.dynstr或者.strtab)的偏移地址,偏移地址存放着符号的名称,如printf。

2. st_value

st_value存放符号的值(可能是地址或者位置偏移量)

3.st_size

st_size存放了一个符号的大小,如全局函数指针的大小,在一个32位系统中通常是4字节。

4.st_other

st_other变量定义了符号的可见性。

5.st_shndx

每个符号表条目的定义都与某些节对应。st_shndx变量保存了相关节头表的索引。

6.st_info

st_info指定符号类型及绑定属性。

符号类型以STT开头,符号绑定以STB开头。

1.符号类型

  • STT_NOTYPE: 符号类型未定义
  • STT_FUNC: 表示该符号与函数或者其他可执行代码关联
  • STT_OBJECT: 表示该符号与数据目标文件关联

2.符号绑定

  • STB_LOCAL: 本地符号在目标文件之外是不可见的,目标文件包含了符号的定义,如一个声明为static的函数
  • STB_GLOBAL: 全局符号对于所有要合并的目标文件来说都是可见的。一个全局符号在一个文件中进行定义后,另外一个文件可以对这个符号进行引用。
  • STB_WEAK: 与全局绑定类似,不过比STB_GLOBAL的优先级低,有可能会被同名的未被标记的STB_WEAK的符号覆盖。

对绑定和类型字段进行打包和解包的宏指令:

ELF32_ST_BIND(INFO) 或者 ELF64_ST_BIND(info):从st_info中提取出一个绑定。

ELF32_ST_TYPE(INFO) 或者 ELF64_ST_TYPE(info):从st_info中提取类型。

ELF32_ST_INFO(bind,type) 或者 ELF64_ST_INFO(bind,type):将一个绑定和类型转换成st_info值。

//源码符号表
static inline void foochu()
{ /* Do nothing */ }
void func1()
{ /* Do nothing */ }
_start()
{
        func1();
        foochu();
}
/**********************************************************/
showmeshell@parrot:~$ readelf -s test | egrep 'foochu|func1'
     7: 080480d8     5 FUNC    LOCAL  DEFAULT    2 foochu
     8: 080480dd     5 FUNC    GLOBAL DEFAULT    2 func1
//foochu 是一个有本地符号STB_LOCAL的函数STT_FUNC。本地绑定意味着符号在被定义的目标文件之外是不可见的,源于 static关键字声明,故为本地的。

如果去掉一个二进制文件的符号表,一个动态链接可执行文件会保留.dynsym,丢弃.symtab,因此只会显示导入库函数的符号。 如果一个二进制文件是通过静态编译(gcc -static) 得到的或者没有使用libc进行链接(gcc -nostdlib),然后使用strip命令进行了清理,那么这个二进制文件就不会有符号表。因为动态符号表对该二进制文件来说不是必需的。

The only thing to give us an idea where a new function starts is by examining the procedure prologue(过程序言), which is at the beginning of every function, unless (gcc -fomit-frame-pointer) has been used, in which case it becomes less obvious to identify.
procedure prologue:The procedure prologue just sets up the stack frame(栈帧) for each new function that has been called by backing up the base pointer on the stack and setting its value to the stack pointers before the stack pointer is adjusted to make room for local variables. This way variables can be referenced as positive offsets from a fixed address stored in the base pointer register ebp/rbp.过程序言通过备份栈上的基准指针来为每个新调用的函数设置栈帧,并在栈指针为本地变量调整空间之前给栈指针赋值(先给栈指针赋值,变量随后压栈,指针随变量压栈进行调整)。首址作为一个固定地址存放基址寄存器ebp/rbp中,通过首址的正向偏移可以依次访问栈中的变量。

5.ELF relocations

// form the elf(5) man pages
Relocation is the process of connecting symbolic references with symbolic definitions.(重定位就是将符号定义和符号引用进行连接的过程) Relocatable files must have information that describes how to modify their section contents, thus allowing executable and shared object files to hold the right information for a process's program image.(使得可执行文件和共享目标文件能够保存进程的程序镜像所需的正确信息)。

在重定位文件中,重定位记录保存了如何对给定的符号对应的代码进行补充的相关信息重定位实际上一种给二进制文件打补丁的机制,如果使用了动态链接器,可以使用重定位在内存中patch hot-patching。用于创建可执行文件和共享库的链接程序/bin/ld,需要某种类型的元数据来描述如何对特定的指令进行修改。

假设将2个目标文件链接到一起产生一个可执行文件,obj1.o文件中存放了调用函数foo()的代码,而函数foo()是存放在目标文件obj2.o中的。

链接程序会对obj1.o和obj2.o中的重定位记录进行分析并将这两个文件链接在一起产生一个可以独立运行的可执行程序。

符号引用会被解析成符号定义:目标文件中的代码会被重定位到可执行文件的段中一个给定的地址。在进行重定位之前,无法确定obj1.o或者obj2.o中的符号和代码在内存中,因此无法进行引用。只能在链接器确定了可执行文件的段中存放的指令或者符号的位置之后才能够进行修改。

//64位重定位的条目
typedef struct {
        Elf64_Addr r_offset;
        Uint64_t   r_info;
} Elf64_Rel;
//有的加上了addend字段
typedef struct {
    	Elf64_Addr r_offset;
 //r_offset指向需要进行重定位操作的位置,重定位操作详细描述了如何对存放r_offset中的代码或数据进行修改。
    	Uint64_t   r_info;
//r_info指定必须对其进行重定位的符号表索引以及要应用的重定位类型。    
    	int64_t    r_addend;
//r_addend指定常量加数,用于计算存储在可重定位字段中的值。    
}Elf64_Rela;

隐式加数/显示加数

如果重定位记录存储在不包含r_addend字段的ElfN_Rel类型结构中,就需要隐式加数,因此隐式加数存储在重定位目标本身中。64位的可执行文件一般使用ElfN_Rela的结构,显式地对加数进行存储。

//obj1
_start()
{
   foo();
}

obj1这段代码中调用了foo()函数,但是foo()函数并没有在这个源码所在的文件中进行定义(obj2中),因此,就需要创建一个重定位条目,以便在编译时进行符号引用:

$ objdump -d obj1.o
obj1.o:     file format elf32-i386
Disassembly of section .text:
00000000 <func>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 08                sub    $0x8,%esp
   6:   e8 fc ff ff ff          call 7 <func+0x7> 
   b:   c9                      leave  
   c:   c3                      ret
#// 上面强调了对foo()的函数调用,存储的值0xfffffffc(就为-4)就是隐式加数。
#// call 7中,7是将要进行修改的重定位目标的偏移量。当obj1.o与obj2.o链接来产生一个可执行文件时,链接器会对偏移为7的位置所指向的重定位条目进行处理,即需要对该位置进行修补,随后在foo()函数被包含进可执行文件后,链接器会对偏移7补齐4个字节,这样就相当于存储了foo()函数的实际偏移地址
$ readelf -r obj1.o
Relocation section '.rel.text' at offset 0x394 contains 1 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00000007  00000902 R_386_PC32        00000000   foo
#//偏移位置7处的重定位字段是由重定位条目的r_offset字段指定的。
  • R_386_PC32是重定位类型:采用“S+A-P”的方式对重定位目标进行修改。
  • S是索引位于重定位条目中的符号的值。
  • A是重定位条目中的加数。
  • P是要进行重定位(使用r_offset进行计算的)的存储单元的地址(节偏移或者地址)
#//32位系统中对obj1.o和obj2.o进行编译之后最终输出的可执行文件:
$ gcc -nostdlib obj1.o obj2.o -o relocated
$ objdump -d relocated
test:     file format elf32-i386
Disassembly of section .text:
080480d8 <func>:
 80480d8:   55                      push   %ebp
 80480d9:   89 e5                   mov    %esp,%ebp
 80480db:   83 ec 08                sub    $0x8,%esp
 80480de:   e8 05 00 00 00          call   80480e8 <foo>//重定位目标已经被修改成了32位的偏移量5,指向foo()函数。
 80480e3:   c9                      leave  
 80480e4:   c3                      ret    
 80480e5:   90                      nop
 80480e6:   90                      nop
 80480e7:   90                      nop
080480e8 <foo>:
 80480e8:   55                      push   %ebp
 80480e9:   89 e5                   mov    %esp,%ebp
 80480eb:   5d                      pop    %ebp
 80480ec:   c3                      ret
 #//R386_PC_32重定位执行之后的结果为5: S + A – P: 0x80480e8 + 0xfffffffc – 0x80480df = 5 或者 0x80480e8 + (0x80480df + sizeof(uint32_t))

要将一个偏移量计算成虚拟地址,可以用下面公式:

address_of_call + offset + 5(5是调用指令的长度)

所以才有:0x80480de + 5 + 5 = 0x80480e8

要将一个地址转化为偏移量:

address – address_of_call – 4(4是调用指令立即操作数的长度,为32位)

所以才有:0x80480e8 – 0x80480df -4 = 5

6.ELF dynamic linking

在动态链接方式实现以前,普通采用静态链接的方式来生成可执行文件,如果一个程序使用了外部的库函数,那么整个库都会被直接编译到可执行文件中。ELF支持动态链接,处理共享库的时候高效。

当一个程序被加载进内存时,动态链接器会把需要的共享库加载并绑定到该进行的地址空间中。共享库在被编译到可执行文件中时是位置独立的,因此很容易被重定位到进程的地址空间中。

一个共享库就是一个动态的ELF目标文件,在终端输入readelf -h lib.so命令,会看到e_type(ELF文件类型)是ET_DYN。动态目标文件与可执行文件非常类似,是由程序解释器加载的,通常没有PT_INTERP段,因此不会触发程序解释器。

当一个共享库被加载进一个进程的地址空间中时,一定有指向其他共享库的重定位。动态链接器会修改可执行文件中的GOT(Global Offset Table,全局偏移表)。GOT位于数据段(.got.plt节)中,GOT必须是可写的。(只读重定位可以看做一种安全特性),故而位于数据段中。

动态链接器会使用解析好的共享库地址来修改GOT。

1. The auxiliary vector

辅助向量

通过系统调用sys_execve()将程序加载到内存中,对应的可执行文件会被映射到内存的地址空间,并为该进程的地址空间分配一个栈。这个栈会用特定的方式向动态链接器传递信息。这种特点的对信息的设置和安排即为辅助向量(auxv)。栈底存放了以下信息:
这里写图片描述
[argc][argv][envp][auxiliary][.ascii data for argv/envp]

辅助向量是一系列ElfN_auxv_t的结构:

typedef struct
{
  uint64_t a_type;              /* Entry type */
  //a_type指定了辅助向量的条目类型。
  union
    {
      uint64_t a_val;           /* Integer value */
      // a_val 为辅助向量的值。
    } a_un;
} Elf64_auxv_t;
//动态链接器所需的一些最重要的条目类型
// usr/include/elf.h
#define AT_EXECFD       2       /* File descriptor of program */
#define AT_PHDR         3       /* Program headers for program */
#define AT_PHENT        4       /* Size of program header entry */
#define AT_PHNUM        5       /* Number of program headers */
#define AT_PAGESZ       6       /* System page size */
#define AT_ENTRY        9       /* Entry point of program */
#define AT_UID          11      /* Real uid */
//动态链接器从栈中检索可执行程序相关的信息,如程序头,程序的入口地址等。

辅助向量由内核函数create_elf_tables()设定,该内核函数在linux的源码/usr/src/linux/fs/binfmt_elf.c中。

In fact, the execution process from the kernel looks something like the following:事实上,内核的执行过程跟下面的描述类似。

  1. sys_execve() →.
  2. Calls do_execve_common() →.
  3. Calls search_binary_handler() →.
  4. Calls load_elf_binary() →.
  5. Calls create_elf_tables() →.

The following is some of the code from create_elf_tables() in /usr/src/linux/fs/binfmt_elf.c that adds auxv entries:(这段代码会添加辅助向量条目)

NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE);
NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff);
NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
NEW_AUX_ENT(AT_BASE, interp_load_addr);
NEW_AUX_ENT(AT_ENTRY, exec->e_entry);

ELF的入口点和程序头地址,以及其他的值,是与内核中的NEW_AUX_ENT()宏一起入栈的。

程序被加载进内存,辅助向量被填充好之后,控制权就交给了动态链接器。动态链接器会解析要链接到进程地址空间的用于共享库的符号和重定位。

默认情况下,可执行文件会动态链接GNU C库 libc.so。(ps:ldd命令能显示出一个给定的可执行文件所依赖的共享库列表)。

2. PLT/GOT

在可执行文件和共享库中可以看到PLT(procedure linkage table 过程链接表)和GOT(Global offset table 全局偏移表)。

当一个程序调用共享库中的函数(如strcpy()或者printf())时,需要到程序运行时,才能解析这些函数调用,那么一定存在动态链接共享库并解析共享函数地址的机制。编译器编译动态链接的程序时,会使用一种特定的方式来处理共享库函数调用,与简单的本地函数调用指令截然不同。

#//32位ELF可执行文件对libc.so的函数fgets()进行调用的例子
#//32位可执行文件与GOT的关系容易观察。在32位文件中没有用到IP相对地址,IP相对地址是在64位可执行文件中使用的。
objdump -d test
 ...
 8048481:       e8 da fe ff ff          call   8048360<fgets@plt>
 ...
 #//地址0x8048360对应函数fgets()的PLT条目。
********************************************************************
 #//查看0x8048360的内容,如下
 objdump -d test (grep for 8048360)
...
08048360<fgets@plt>:                    /* A jmp into the GOT */
 8048360:       ff 25 00 a0 04 08       jmp    *0x804a000
 #//有一个间接跳转指向存放在0x804a000中的地址,这个地址就是GOT条目,存放着libc共享库中函数fgets()的实际地址。
 8048366:       68 00 00 00 00          push   $0x0
 804836b:       e9 e0 ff ff ff          jmp    8048350 <_init+0x34>
#//对函数fgets()的调用会指向地址0x8048360,即函数fgets()的PLT跳转表条目。

然而,动态链接器采用默认的lazy linking (延迟链接方式)时,不会在函数第一次调用时就对地址进行解析。lazy linking 意味着动态链接器不会在程序加载时解析每一个函数,而是在调用时通过.plt和.got.plt节(分别对应各自的过程链接表和全局偏移表)来对函数进行解析。可以通过修改LD_BIND_NOW环境变量将链接方式修改为strict linking(严格加载),以便在程序加载的同时进行动态链接。

动态链接器之所以默认采用lazy linking,是因为lazy Linking能够提高装载时的性能。(PS:有些安全特性,如只读重定位,只能在严格链接的模式下使用,因为.plt.got节是只读的。在动态链接器完成对.plt.got的补充之后才能够进行只读重定位,故必须使用strict linking。

#//fgets()函数的重定位条目
$ readelf -r test
Offset   Info      Type           SymValue    SymName
...
0804a000  00000107 R_386_JUMP_SLOT   00000000   fgets
...
#//R_386_JUMP_SLOT is a relocation type for PLT/GOT entries. On x86_64, it is called R_X86_64_JUMP_SLOT.

重定位的偏移地址为0x804a000,跟fgets()函数PLT跳转的地址相同。

假设函数fgets()是第一次被调用,动态链接器需要对fgets()的地址进行解析,并把值存入fgets()的GOT条目中。

#//test的GOT:
08049ff4 <_GLOBAL_OFFSET_TABLE_>:
 8049ff4:       28 9f 04 08 00 00       sub    %bl,0x804(%edi)
 8049ffa:       00 00                   add    %al,(%eax)
 8049ffc:       00 00                   add    %al,(%eax)
 8049ffe:       00 00                   add    %al,(%eax)
 804a000:       66 83 04 08 76          addw   $0x76,(%eax,%ecx,1)
 804a005:       83 04 08 86             addl   $0xffffff86,(%eax,%ecx,1)
 804a009:       83 04 08 96             addl   $0xffffff96,(%eax,%ecx,1)
 804a00d:       83                      .byte 0x83
 804a00e:       04 08                   add    $0x8,%al

重点注意地址0x08048366,该地址存储在GOT的0x804a000中, 66 83 04 08 =》08048366。

由于链接器还没有对函数fgets()进行解析,故该地址并不是函数的地址,而是执行函数fgets()的PLT条目。

再回到PLT条目:

08048360 <fgets@plt>:
 8048360:       ff 25 00 a0 04 08       jmp    *0x804a000
 8048366:       68 00 00 00 00          push   $0x0
 #// jmp *0x804a000指令跳转到地址0x804a000中存放的0x8048366,即push $0x0指令。
 #// 该push指令的作用是将fgets的GOT条目入栈。
 804836b:       e9 e0 ff ff ff          jmp    8048350 <_init+0x34>

fgets()的GOT条目偏移地址0x0,对应的第一个GOT条目,是为一个共享库符号值保留的,0x0实际上是第4个GOT条目,即GOT[3]。

共享库的地址并不是从GOT[0]开始,而是从GOT[3]开始的,前3个条目其它用途保留。

  • GOT[0]:存放了指向可执行文件动态段的地址,动态链接器利用该地址提取动态链接相关的信息。
  • GOT[1]:存放link_map结构的地址,动态链接器利用该地址来对符号进行解析。
  • GOT[2]:存放了指向动态链接器 _dl_runtime_resolve()函数的地址,该函数用来解析共享库函数的实际符号地址。

fgets()的PLT存根(stub)的最后一条指令是 jmp 8048350 。该地址指向可执行文件的第一个PLT条目,PLT-0;

 #//PLT-0
 8048350:       ff 35 f8 9f 04 08       pushl  0x8049ff8
 #//将GOT[1]的地址压栈,存放link_map结构。
 8048356:       ff 25 fc 9f 04 08       jmp    *0x8049ffc
 #//jmp *0x8049ffc指令间接跳转到第三个GOT条目,GOT[2],存放 _dl_runtime_resolve()地址,然后将控制权转给动态链接器,解析fgets()函数的地址。
 804835c:       00 00                   add    %al,(%eax)

对函数fgets进行解析后,后续所有的对PLT条目fgets的调用都会跳转到fgets的代码本身,而不是重新指向PLT,再进行一遍延迟链接的过程。

SUM++

  1. 调用fget@PLT(即调用fgets函数)
  2. PLT代码做一个次到GOT地址的间接跳转。
  3. GOT条目存放了指向PLT的地址,该地址存放在PUSH指令中。
  4. push $0x0 指令将fgets()GOT条目的偏移量压栈。
  5. 最后的fgets()PLT指令是执行PLT-0代码的jmp指令。
  6. PLT-0的第一条指令将GOT[1]的地址压栈,GOT[1]中存放了指向fgets()的link_map结构的偏移地址。
  7. PLT-0的第二条指令会跳转到GOT[2]存放的地址,该地址执行动态链接器的_dl_runtime_resolve()函数,_dl_runtime_resolve()会通过把fgets()函数的符号值加到.got.plt节对于的GOT条目中,来处理R_386_JUMP_SLOT重定位。
  8. 下一次调用fgets函数时,PLT条目直接跳转到函数本身,而不是再执行一遍重定位过程。

3. The dynamic segment revisited

动态段前面说过,有一个节头,可以通过节头来引用动态段,还可以通过程序头来引用动态段。

动态链接器需要在程序运行时引用动态段,但是节头不能够被加载到内存中,因此动态段需要有相关的程序头。

//动态段保存了一个由类型为ElfN_Dyn的结构体组成的数组。
typedef struct {
    Elf32_Sword    d_tag;
    union {
      Elf32_Word d_val;
//d_val成员保存了一个整型值,可以存放各种不同的数据,如一个重定位条目的大小。    
      Elf32_Addr d_ptr;
//d_ptr成员保存了一个内存虚址,可以指向链接器需要的各种类型的地址,eg:d_tag DT_SYMTAB符号表的地址。        
    } d_un;
} Elf32_Dyn;

d_tag字段保存了类型的定义参数。

动态链接器常用比较重要的类型值:

1.DT_NEEDED

保存了所需的共享库名的字符串表偏移。

2.DT_SYMTAB

动态符号表的地址,对应的节名 .dynsym。

3.DT_HASH

符号散列表的地址,对应的节名 .hash (有时命名为.gnu.hash)。

4.DT_STRTAB

符号字符串表的地址,对应的节名.dynstr。

5.DT_PLTGOT

全局偏移表的地址。

动态链接器利用ElfN_Dyn的d_tag来定位动态段的不同部分,每一部分都通过d_tag保存了指向某部分可执行文件的引用。如DT_SYMTAB保存了动态符号表的地址,对于的d_ptr给出了指向该符号标的虚址。

动态链接器映射到内存中时,首先会处理自身的重定位,因为链接器本身就是一个共享库。接着**会查看可执行程序的动态段并查找DT_NEEDED参数,该参数保存了指向所需的共享库的字符串或者路径名。**当一个共享库被映射到内存后,链接器会获取到共享库的动态段,并将共享库的符号表添加到符号表链中,符号表链存储了所有映射到内存中的共享库的符号表。

链接器为每个共享库生成一个link_map结构的条目,并将其存入到一个链表中:

struct link_map
  {
    ElfW(Addr) l_addr; /* Base address shared object is loaded at.  */
    char *l_name;      /* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;   /* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */
  };

链接器构建完依赖列表后,会挨个处理每个库的重定位,同时会补充每个共享库的GOT。lazy linking (延迟链接)对共享库的PLT/GOT仍然适用。故,只有当一个函数真正被调用时,才会进行GOT重定位。(type R_386_JMP_SLOT)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值