ELF Tutorial

来自osdev
跳到导航 跳到搜索

这个页面正在建设中! 此页面或部分内容仍在改进中,因此可能还不完整。 其内容可能会在不久的将来更改。

难度等级
Difficulty 2.png
中等
可执行文件格式
Microsoft

16 bit:
COM
MZ
NE
32/64 bit:
PE
Mixed (16/32 bit):
LE

*nix

本教程介绍了加载针对i386(32位体系结构,小端字节顺序)的ELF文件的步骤。 教程中的所有代码都是C兼容C++的形式,并通过示例使用简化的(有时是过于简化的)、整洁的和功能性的代码片段来尝试教学。 它以后可能会扩展到其他类型的ELF文件,或针对其他体系结构或机器类型的格式。

ELF数据类型

# include <stdint.h>

typedef uint16_t Elf32_Half;	// Unsigned half int
typedef uint32_t Elf32_Off;	// Unsigned offset
typedef uint32_t Elf32_Addr;	// Unsigned address
typedef uint32_t Elf32_Word;	// Unsigned int
typedef int32_t  Elf32_Sword;	// Signed int

ELF文件格式适用于多种不同的体系结构,其中许多体系结构支持不同的数据宽度。 对于跨多个机器类型的支持,ELF格式为固定宽度类型提供了一组指导原则,这些类型构成了节的布局和对象文件中表示的数据。 您可以选择以不同的方式命名类型,或者使用stdint.h中定义的类型,但应符合上述要求。

ELF文件头

ELF文件格式只有一个固定位置的头:ELF文件头,出现在每个文件的开头。 格式本身非常灵活,每个头(保存ELF头)的位置、大小和用途由文件中的另一个头描述。

# define ELF_NIDENT	16

typedef struct {
	uint8_t		e_ident[ELF_NIDENT];
	Elf32_Half	e_type;
	Elf32_Half	e_machine;
	Elf32_Word	e_version;
	Elf32_Addr	e_entry;
	Elf32_Off	e_phoff;
	Elf32_Off	e_shoff;
	Elf32_Word	e_flags;
	Elf32_Half	e_ehsize;
	Elf32_Half	e_phentsize;
	Elf32_Half	e_phnum;
	Elf32_Half	e_shentsize;
	Elf32_Half	e_shnum;
	Elf32_Half	e_shstrndx;
} Elf32_Ehdr;

ELF文件头是ELF文件中的第一个头,它提供有关文件的重要信息(例如机器类型、体系结构和字节顺序等),以及识别和检查文件是否有效。 ELF文件头还提供有关文件中其他部分的信息,它们可以以任何顺序出现、大小不同,或者可能完全不在文件中。 所有ELF文件通用的前4个字节(魔法数字)用于标识文件。 通过上面定义的Elf32_Ehdr类型处理文件时,可以从“e_ident”字段的索引0-3访问这4个字节。

enum Elf_Ident {
	EI_MAG0		= 0, // 0x7F
	EI_MAG1		= 1, // 'E'
	EI_MAG2		= 2, // 'L'
	EI_MAG3		= 3, // 'F'
	EI_CLASS	= 4, // Architecture (32/64)
	EI_DATA		= 5, // Byte Order
	EI_VERSION	= 6, // ELF Version
	EI_OSABI	= 7, // OS Specific
	EI_ABIVERSION	= 8, // OS Specific
	EI_PAD		= 9  // Padding
};

# define ELFMAG0	0x7F // e_ident[EI_MAG0]
# define ELFMAG1	'E'  // e_ident[EI_MAG1]
# define ELFMAG2	'L'  // e_ident[EI_MAG2]
# define ELFMAG3	'F'  // e_ident[EI_MAG3]

# define ELFDATA2LSB	(1)  // Little Endian
# define ELFCLASS32	(1)  // 32-bit Architecture

文件头中的第一个字段由16个字节组成,其中许多字节提供了有关ELF文件的重要信息,如预期的体系结构、字节顺序和ABI(译者注:Application Binary Interface)信息。 由于本教程的重点是实现与x86兼容的加载程序,因此只包含了相关的值定义。

enum Elf_Type {
	ET_NONE		= 0, // Unkown Type
	ET_REL		= 1, // Relocatable File
	ET_EXEC		= 2  // Executable File
};

# define EM_386		(3)  // x86 Machine Type
# define EV_CURRENT	(1)  // ELF Current Version

文件头还提供有关机器类型和文件类型的信息。同样,上面只包含了本文相关的定义。

检查ELF头

因为在加载、链接、重新定位或以其他方式处理ELF文件之前,务必要确保执行上述操作的机器能够执行。 这意味着需要检查该文件是一个针对本地机器的体系结构、字节顺序和CPU类型的有效ELF文件,并且满足其它任何操作系统特定的语义。

bool elf_check_file(Elf32_Ehdr *hdr) {
	if(!hdr) return false;
	if(hdr->e_ident[EI_MAG0] != ELFMAG0) {
		ERROR("ELF Header EI_MAG0 incorrect.\n");
		return false;
	}
	if(hdr->e_ident[EI_MAG1] != ELFMAG1) {
		ERROR("ELF Header EI_MAG1 incorrect.\n");
		return false;
	}
	if(hdr->e_ident[EI_MAG2] != ELFMAG2) {
		ERROR("ELF Header EI_MAG2 incorrect.\n");
		return false;
	}
	if(hdr->e_ident[EI_MAG3] != ELFMAG3) {
		ERROR("ELF Header EI_MAG3 incorrect.\n");
		return false;
	}
	return true;
}

假设一个ELF文件已经被加载到内存中(无论是通过引导加载程序还是其他方式),加载ELF文件的第一步是检查ELF头中应该出现在文件开头的固定Magic number。 这种方法的一个最小实现可以简单地将内存中文件的映像视为一个串,并与预定义的串进行比较。 在上面的示例中,通过ELF头类型逐字节进行比较,并在方法遇到错误时提供详细的反馈。

bool elf_check_supported(Elf32_Ehdr *hdr) {
	if(!elf_check_file(hdr)) {
		ERROR("Invalid ELF File.\n");
		return false;
	}
	if(hdr->e_ident[EI_CLASS] != ELFCLASS32) {
		ERROR("Unsupported ELF File Class.\n");
		return false;
	}
	if(hdr->e_ident[EI_DATA] != ELFDATA2LSB) {
		ERROR("Unsupported ELF File byte order.\n");
		return false;
	}
	if(hdr->e_machine != EM_386) {
		ERROR("Unsupported ELF File target.\n");
		return false;
	}
	if(hdr->e_ident[EI_VERSION] != EV_CURRENT) {
		ERROR("Unsupported ELF File version.\n");
		return false;
	}
	if(hdr->e_type != ET_REL && hdr->e_type != ET_EXEC) {
		ERROR("Unsupported ELF File type.\n");
		return false;
	}
	return true;
}

加载ELF对象的下一步是检查相关文件是否打算在加载该文件的机器上运行。 同样,ELF头提供了有关文件预期目标的必要信息。 上面的代码假设您已经实现了一个名为elf_check_file()(或使用了上面提供的函数),并且本地机器是i386、little endian和32位的。 上面的代码还只允许加载可执行文件和可重定位文件,尽管这可以根据需要进行更改。

加载ELF文件

static inline void *elf_load_rel(Elf32_Ehdr *hdr) {
	int result;
	result = elf_load_stage1(hdr);
	if(result == ELF_RELOC_ERR) {
		ERROR("Unable to load ELF file.\n");
		return NULL;
	}
	result = elf_load_stage2(hdr);
	if(result == ELF_RELOC_ERR) {
		ERROR("Unable to load ELF file.\n");
		return NULL;
	}
	// TODO:解析程序头(如果存在)
	return (void *)hdr->e_entry;
}

void *elf_load_file(void *file) {
	Elf32_Ehdr *hdr = (Elf32_Ehdr *)file;
	if(!elf_check_supported(hdr)) {
		ERROR("ELF File cannot be loaded.\n");
		return;
	}
	switch(hdr->e_type) {
		case ET_EXEC:
			//TODO: 待实现
			return NULL;
		case ET_REL:
			return elf_load_rel(hdr);
	}
	return NULL;
}

ELF节头(Section Header)

ELF格式定义了许多不同类型的节及其相关的标题,并不是所有这些都出现在每个文件中,也不能保证它们出现的顺序。 因此,为了解析和处理这些节,该格式还定义了节头,其中包含节名、大小、位置和其他相关信息等信息。 ELF映像中所有节头的列表称为节头表。

typedef struct {
	Elf32_Word	sh_name;
	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字段并不直接指向字符串,而是给出了节名字符串表中字符串的偏移量 (表本身的索引在ELF头中由字段e_shstrndx定义)。 每个头还将文件映像中实际节在sh_offset字段中的位置定义为距文件开头的偏移量。

# define SHN_UNDEF	(0x00) // 未定义/不存在

enum ShT_Types {
	SHT_NULL	= 0,   //空段
	SHT_PROGBITS	= 1,   // 程序信息
	SHT_SYMTAB	= 2,   //符号表(Symbol table)
	SHT_STRTAB	= 3,   //字符串表(String table)
	SHT_RELA	= 4,   //重新定位(w/ addend)
	SHT_NOBITS	= 8,   //不在文件中
	SHT_REL		= 9,   //重新定位(无addend)
};

enum ShT_Attributes {
	SHF_WRITE	= 0x01, // 可写部分
	SHF_ALLOC	= 0x02  // 存在于内存中
};

以上是一些与本教程相关的常数(还有很多)。 枚举ShT_Types定义了许多不同类型的节,这些节对应于节头中sh_type字段中存储的值。 类似地,ShT_Attributes对应于字段sh_flags,但是仅限于其中几个位标志,而不是独立的值。

访问节头

访问节头本身并不困难: 它在文件映像中的位置由ELF头中的e_shoff定义,节头的数量由e_shnum定义。 值得注意的是,节头中的第一个条目是空条目;也就是说,头中的字段是0。 多个节头是连续的,因此给定指向第一个条目的指针,可以通过简单的指针算术或数组操作访问后续条目。

static inline Elf32_Shdr *elf_sheader(Elf32_Ehdr *hdr) {
	return (Elf32_Shdr *)((int)hdr + hdr->e_shoff);
}

static inline Elf32_Shdr *elf_section(Elf32_Ehdr *hdr, int idx) {
	return &elf_sheader(hdr)[idx];
}

上述两种方法使用上述原则,在索引的基础上方便地访问节头,它们将在下面的示例代码中经常使用。

节名

一个值得注意的过程是访问节名(因为,如前所述,头只提供到节名字符串表的偏移量),这也是相当简单的。 整个操作可以分解为一系列简单的步骤:

  1. 从ELF头(存储在e_shstrndx中)获取字符串表的节头索引。 确保对照SHN_UNDEF检查索引,因为该表可能不存在。
  2. 访问给定索引处的节头,并查找表偏移量(存储在sh_offset中)。
  3. 使用偏移量计算字符串表在内存中的位置。
  4. 在字符串表中创建一个指向名称偏移量的指针。

下面两种便捷方法展示了该过程的示例。

static inline char *elf_str_table(Elf32_Ehdr *hdr) {
	if(hdr->e_shstrndx == SHN_UNDEF) return NULL;
	return (char *)hdr + elf_section(hdr, hdr->e_shstrndx)->sh_offset;
}

static inline char *elf_lookup_string(Elf32_Ehdr *hdr, int offset) {
	char *strtab = elf_str_table(hdr);
	if(strtab == NULL) return NULL;
	return strtab + offset;
}

请注意,在尝试访问节的名称之前,应首先检查该节是否有名称(sh_name给出的偏移量不等于SHN_UNDEF)。

ELF节说明

ELF目标文件可以有大量的节,但是,需要注意的是,在程序加载期间,只需要处理一些节,并且并非所有节都存在于目标文件本身(即BSS)中。(译者注:BSS即Block Started by Symbol,需静态分配内存区域) 本节将描述在程序加载过程中应该处理的一些部分(假设它们存在)。

符号表(Symbol Table)

符号表是ELF文件中存在的一个多个节,用于定义原始源文件中声明的、在编译或链接期间创建的或以其他方式存在于文件中的各种符号的位置、类型、可见性和其他特征。 由于ELF目标可以有多个符号表,因此需要迭代文件的节头,或者跟随另一节的引用来访问一个节。

typedef struct {
	Elf32_Word		st_name;
	Elf32_Addr		st_value;
	Elf32_Word		st_size;
	uint8_t			st_info;
	uint8_t			st_other;
	Elf32_Half		st_shndx;
} Elf32_Sym;

每个符号表条目都包含许多值得注意的信息位,例如符号名称(st_name,可能是STN_UNDEF)、符号值(st_value,可能是绝对或相对值地址),以及包含符号类型和绑定的字段st_info。 另一方面,每个符号表中的第一个条目都是空条目,因此它的所有字段都是0。

# define ELF32_ST_BIND(INFO)	((INFO) >> 4)
# define ELF32_ST_TYPE(INFO)	((INFO) & 0x0F)

enum StT_Bindings {
	STB_LOCAL		= 0, // 局部作用域(Local scope)
	STB_GLOBAL		= 1, // 全局作用域(Global scope)
	STB_WEAK		= 2  // Weak, (即 __attribute__((weak))) (译者注:__attribute__是GCC的一种编译调整机制,这里weak作用请自行查阅)
};

enum StT_Types {
	STT_NOTYPE		= 0, //无类型
	STT_OBJECT		= 1, // 变量、数组等。
	STT_FUNC		= 2  //方法或函数
};

如上所述,st_info包含符号类型和符号绑定(在不同的位),因此上面的两个宏提供了对单个值的访问。 枚举StT_Types提供了许多可能的符号类型,StB_Bindings提供了可能的符号绑定。

访问符号的值

一些操作,例如链接和重新定位,需要符号的值(或者更确切地说,是其地址)。 虽然符号表条目确实定义了字段st_value,但它可能只包含相对地址。 下面是一个如何计算符号值的绝对地址的示例。 代码被分成多个较小的部分,以便更容易理解。

static int elf_get_symval(Elf32_Ehdr *hdr, int table, uint idx) {
	if(table == SHN_UNDEF || idx == SHN_UNDEF) return 0;
	Elf32_Shdr *symtab = elf_section(hdr, table);

	uint32_t symtab_entries = symtab->sh_size / symtab->sh_entsize;
	if(idx >= symtab_entries) {
		ERROR("Symbol Index out of Range (%d:%u).\n", table, idx);
		return ELF_RELOC_ERR;
	}

	int symaddr = (int)hdr + symtab->sh_offset;
	Elf32_Sym *symbol = &((Elf32_Sym *)symaddr)[idx];

上面对符号表索引和符号索引执行检查;如果其中一个未定义,则返回0。 否则,将访问给定索引处符号表的节头条目。 然后检查符号表索引是否不在符号表的边界之外。 如果检查失败,将显示错误消息并返回错误代码,否则将检索给定索引处的符号表条目。

	if(symbol->st_shndx == SHN_UNDEF) {
		// External symbol, lookup value
		Elf32_Shdr *strtab = elf_section(hdr, symtab->sh_link);
		const char *name = (const char *)hdr + strtab->sh_offset + symbol->st_name;

		extern void *elf_lookup_symbol(const char *name);
		void *target = elf_lookup_symbol(name);

		if(target == NULL) {
			// Extern symbol not found
			if(ELF32_ST_BIND(symbol->st_info) & STB_WEAK) {
				// Weak symbol initialized as 0
				return 0;
			} else {
				ERROR("Undefined External Symbol : %s.\n", name);
				return ELF_RELOC_ERR;
			}
		} else {
			return (int)target;
		}

如果符号相对的部分(由st_shndx给出)等于“SHN_unde”,则符号是外部的,必须与其定义相链接。 将为当前符号表检索字符串表(给定符号表的字符串表可在sh_link中的表的节头中找到),并在字符串表中找到符号的名称。 接下来,使用函数elf_lookup_symbol()按名称查找符号定义(还不提供此函数,最小实现总是返回NULL)。 如果找到符号定义,则返回该定义。 如果符号具有STB_WEAK标志(是弱符号),则返回0,否则将显示错误消息并返回错误代码。

	} else if(symbol->st_shndx == SHN_ABS) {
		// Absolute symbol
		return symbol->st_value;
	} else {
		// Internally defined symbol
		Elf32_Shdr *target = elf_section(hdr, symbol->st_shndx);
		return (int)hdr + symbol->st_value + target->sh_offset;
	}
}


如果sh_ndx的值等于SHN_ABS,则符号的值为绝对值,并立即返回。 如果sh_ndx不包含特殊值,则表示符号是在本地ELF对象中定义的。 由于sh_value给出的值与sh_ndx定义的节有关,因此访问相关的节头条目,并通过将内存中的文件地址与其节偏移量相加来计算符号的地址。

字符串表

字符串表在概念上非常简单: 它只是一些连续的以零结尾的字符串。 程序中使用的字符串文本存储在其中一个表中。 ELF目标中可能存在许多不同的字符串表,例如.strtab(默认字符串表).shstrtab(段字符串表)和.dynstr(用于动态链接的字符串表)。 每当加载过程需要访问字符串时,它都会在其中一个字符串表中使用偏移量。 偏移可以指向零终止字符串的开始或中间的某个位置,甚至指向零终止符本身,这取决于使用和场景。 字符串表本身的大小由相应的节头条目中的sh_size 指定。

最简单的程序加载器可以将所有字符串表复制到内存中,但更完整的解决方案会忽略运行时不需要的任何字符串表,尤其是那些在各自的节头中没有标记SHF_ALLOC的字符串表(例如.shstrtab,因为程序运行时不使用节名)。

BSS和SHT_NOBITS

BSS(名为“.BSS”的节)用最简单的方式描述: 已归零的内存块。 BSS是内存中存储尚未初始化(或已初始化为0或NULL)的全局生存期变量的区域。 BSS的节头将其sh_type定义为SHT_NOBITS,这意味着它不在文件映像中,必须在运行时分配。 分配BSS的一种简单而直接的方法是malloc一些内存,然后用memset将其归零。 未能将BSS归零可能会导致任何加载的程序出现意外行为。 另一件需要注意的事情是,在执行任何依赖于相对寻址(例如重新定位)的操作之前,应该先分配BSS,否则可能会导致代码引用垃圾内存或错误。

虽然BSS是一个具体的例子,但任何类型为SHT_NOBITS且具有SHF_ALLOC属性的节都应该在程序加载期间尽早分配。 由于本教程是一般而非具体的,下面的示例将遵循趋势,并使用最简单的示例来分配部分。

static int elf_load_stage1(Elf32_Ehdr *hdr) {
	Elf32_Shdr *shdr = elf_sheader(hdr);

	unsigned int i;
	// 对节头的迭代器
	for(i = 0; i < hdr->e_shnum; i++) {
		Elf32_Shdr *section = &shdr[i];

		// 如果文件中没有出现该节
		if(section->sh_type == SHT_NOBITS) {
			//如果该部分为空,则跳过
			if(!section->sh_size) continue;
			// 如果该节应该出现在内存中
			if(section->sh_flags & SHF_ALLOC) {
				// 分配并清空一些内存
				void *mem = malloc(section->sh_size);
				memset(mem, 0, section->sh_size);

				// 将内存偏移量指定给段偏移量
				section->sh_offset = (int)mem - (int)hdr;
				DEBUG("Allocated memory for a section (%ld).\n", section->sh_size);
			}
		}
	}
	return 0;
}

上面的示例为节分配了所需的内存,由节头的sh_size字段描述。 虽然本例中的函数只查找需要分配的部分,但可以对其进行修改,以执行加载过程早期应执行的其他操作。

重定位节

可重定位ELF文件在内核编程中有很多用途,尤其是作为可在启动时加载的模块和驱动程序,并且特别有用,因为它们是位置独立的,因此可以很容易地放置在内核之后或在某个方便的地址启动,并且不需要自己的地址空间来运行。 重定位过程本身在概念上很简单,但随着复杂重定位类型的引入,可能会变得更加困难。

重定位从一个重定位条目表开始,该表可以使用相关的节头进行定位。 实际上有两种不同的重定位结构体;一个带有显式添加的(节类型SHT_RELA),一个没有(节类型SHT_REL)。 表中的重定位条目是连续的,给定表中的条目数可以通过将表的大小(在节头中用sh_size给出)除以每个条目的大小(用sh_entsize给出)来得到。 每个重定位表都特定于单个节,因此单个文件可能有多个重定位表(但给定表中的所有条目都将是相同的重定位结构类型)。

typedef struct {
	Elf32_Addr		r_offset;
	Elf32_Word		r_info;
} Elf32_Rel;

typedef struct {
	Elf32_Addr		r_offset;
	Elf32_Word		r_info;
	Elf32_Sword		r_addend;
} Elf32_Rela;

以上是重定位的不同结构体类型的定义。 值得注意的是,如果存储在r_info中的值作为高位字节指定了符号表中应用重定位的条目,而低位字节存储了应应用的重定位类型。 请注意,ELF文件可能有多个符号表,因此可以在该重定位表的节头上的sh_link字段中找到引用这些重定位应用的符号表的节头表的索引。 r_offset中的值给出了要重新定位的符号在其截面内的相对位置。

# define ELF32_R_SYM(INFO)	((INFO) >> 8)
# define ELF32_R_TYPE(INFO)	((uint8_t)(INFO))

enum RtT_Types {
	R_386_NONE		= 0, // No relocation
	R_386_32		= 1, // Symbol + Offset
	R_386_PC32		= 2  // Symbol + Offset - Section Offset
};

如前所述,Elf32_Rel(a)中的r_info字段指的是两个单独的值,因此可以使用上面的宏函数集来获得单独的值; ELF32_R_SYM()提供对符号索引的访问,ELF32_R_TYPE()提供对重定位类型的访问。 枚举RtT_Types定义了本教程将包含的重新定位类型。

重新定位示例

加载一个可重定位的ELF文件需要处理文件中的所有重定位条目(记住先分配所有SHT_NOBITS节!)。 这个过程从查找文件中的所有重新定位表开始,这在下面的示例代码中完成。

# define ELF_RELOC_ERR -1

static int elf_load_stage2(Elf32_Ehdr *hdr) {
	Elf32_Shdr *shdr = elf_sheader(hdr);

	unsigned int i, idx;
	// Iterate over section headers
	for(i = 0; i < hdr->e_shnum; i++) {
		Elf32_Shdr *section = &shdr[i];

		// If this is a relocation section
		if(section->sh_type == SHT_REL) {
			// Process each entry in the table
			for(idx = 0; idx < section->sh_size / section->sh_entsize; idx++) {
				Elf32_Rel *reltab = &((Elf32_Rel *)((int)hdr + section->sh_offset))[idx];
				int result = elf_do_reloc(hdr, reltab, section);
				// On error, display a message and return
				if(result == ELF_RELOC_ERR) {
					ERROR("Failed to relocate symbol.\n");
					return ELF_RELOC_ERR;
				}
			}
		}
	}
	return 0;
}

请注意,上面的代码只处理Elf32_Rel条目,但也可以修改为处理带有显式加数的条目。 代码还依赖于名为elf_do_reloc的函数,该函数将在下一个示例中显示。 此示例函数停止,显示错误消息,如果无法处理重新定位,则返回错误代码。

重新定位符号

由于下面的函数相当复杂,所以将其分解为更小的可管理块,并进行了详细解释。 请注意,下面显示的代码假定要重新定位的文件是可重新定位的ELF文件(ELF可执行文件和共享目标也可能包含重新定位条目,但处理方式有所不同)。 还请注意,对于类型为SHT_RELSHT_RELA的节头,sh_info存储了重新定位适用的节头。

# define DO_386_32(S, A)	((S) + (A))
# define DO_386_PC32(S, A, P)	((S) + (A) - (P))

static int elf_do_reloc(Elf32_Ehdr *hdr, Elf32_Rel *rel, Elf32_Shdr *reltab) {
	Elf32_Shdr *target = elf_section(hdr, reltab->sh_info);

	int addr = (int)hdr + target->sh_offset;
	int *ref = (int *)(addr + rel->r_offset);

上述代码定义了用于完成重新定位计算的宏函数。 它还检索符号存在的节的节头,并计算对符号的引用。 变量addr表示符号部分的开始,而ref是通过将偏移量从重新定位条目添加到符号中来创建的。

	// Symbol value
	int symval = 0;
	if(ELF32_R_SYM(rel->r_info) != SHN_UNDEF) {
		symval = elf_get_symval(hdr, reltab->sh_link, ELF32_R_SYM(rel->r_info));
		if(symval == ELF_RELOC_ERR) return ELF_RELOC_ERR;
	}

接下来,访问要重新定位的符号的值。 如果r_info中存储的符号表索引未定义,则该值默认为0。 代码还引用了一个名为elf_get_symval()的函数,该函数是以前实现的。 如果函数返回的值等于ELF_RELOC_ERR,则停止重新定位并返回所述错误代码。

	// Relocate based on type
	switch(ELF32_R_TYPE(rel->r_info)) {
		case R_386_NONE:
			// No relocation
			break;
		case R_386_32:
			// Symbol + Offset
			*ref = DO_386_32(symval, *ref);
			break;
		case R_386_PC32:
			// Symbol + Offset - Section Offset
			*ref = DO_386_PC32(symval, *ref, (int)ref);
			break;
		default:
			// Relocation type not supported, display error and return
			ERROR("Unsupported Relocation Type (%d).\n", ELF32_R_TYPE(rel->r_info));
			return ELF_RELOC_ERR;
	}
	return symval;
}

最后,这段代码详细介绍了实际的重新定位过程,执行必要的重新定位符号计算,并在成功时返回其值。 如果不支持重新定位类型,将显示错误消息,重新定位将停止,函数将返回错误代码。 假设没有发生错误,重新定位现在已经完成。

ELF程序头

程序头是一种结构,定义了ELF程序加载后的行为信息,以及运行时链接信息。 ELF程序头(很像节头)都被分组在一起,组成程序头表。

typedef struct {
	Elf32_Word		p_type;
	Elf32_Off		p_offset;
	Elf32_Addr		p_vaddr;
	Elf32_Addr		p_paddr;
	Elf32_Word		p_filesz;
	Elf32_Word		p_memsz;
	Elf32_Word		p_flags;
	Elf32_Word		p_align;
} Elf32_Phdr;

程序头表包含一组连续的程序头(因此可以像访问数组一样访问它们)。 可以使用ELF头中定义的e_phoff字段访问表本身,前提是它存在。 程序头本身定义了许多有用的字段,比如p_type用来区别不同的头,以及包含p_offsetp_vaddr,前者存储报头所指段的偏移量,后者定义位置相关代码应该存在的地址。

待完成:展开并详述。

另见

文章

外部链接