前面章节我们了解了ELF文件的头部结构,这次我们深入了解另一个非常重要的数据结构,那就是程序表头。操作系统严重依赖该结构来加载ELF文件或是实现动态链接。程序表头反映的是当ELF加载到内存后所形成的“视图”或结构,也就是说ELF文件存在硬盘上或者被加载到内存,它展现出来的形态不一致。
我们先看程序表头的数据结构:
typedef struct {
unit32_t p_type; #数据类型
uint332_t p_flags; #标志位
uint64_t p_offset; #在ELF文件中的偏移
uint64_t p_vaddr; #虚拟地址
uint64_t p_paddr; #物理地址
uint64_t p_fllesz; #在硬盘上的大小
uint64_t p_memsz; #在内存中大小
uint64_t p_align; #内存对齐方式
} Elf64_Phdr;
使用命令 readelf —wide —segments a.out可以读取程序表头内容信息:
这里需要注意的是,程序表头其实没有什么新意,它其实对应前面说过的若干个段所形成的集合。接下来我们看每个字段的含义。
p_type对应表头的类型,常用的数值有PT_LOAD, PT_DYNAMIC, PT_INTER。如果取值PT_LOAD,意味着表头对应的段需要加装到内存中;从上图看到有两个表头的类型为PT_LOAD,分别为第3和4,而第3个表头对应段的集合为.init_array .fini_array等,第4个表头对应段集合为.dynamic,这意味着这些段需要加载到内存中,同时每个表头对应的段都要合成一个整体加载到表头中所指定的位置。
PT_FLAGS对应段加载到内存后的读写权限,常用的值有PF_X,PF_W,PF_R。PF_X表示表头对应的段可以被执行,PF_W对应加载的那些段可以被修改,PT_R表示加载的段可以被读取。p_offset表示表头对应那些段的起始地址,p_vaddr表示表头对应段该加载的虚拟位置,p_filesz表示表头对应段在硬盘上的大小,p_memsz表示表头对应段在加载到内存后的大小。
你可能会困惑,为何p_filesz和p_memsz的值不一样。这是因为有些段在硬盘上不占据容量,只有加载到内存时才分配容量。最后p_align表示内存对齐方式,它的取值为2的指数,同时p_vaddr必须等于(p_offset % p_align)
了解了ELF二进制内部原理后,我们需要实现手动加载ELF文件,实现这个目标,我们需要依赖一个库叫libbfd,这个库提供很多功能让调用者能解读X86架构下的通用二进制可执行文件。其安装可以使用如下命令:
sudo apt-get install -y libbfd-dev
基本上所有版本的Linux都会附带这个代码库,该代码库提供了一个类叫Binary,用于对可执行二进制文件的抽象,同时还有Section类,它是对前面我们提到的段数据结构的抽象;同时它还提供Symbol类,这是对符号表的抽象,接下来我们先看看其基本使用方法:
#include <stdio.h>
#include <stdint.h>
#include <string>
#include "../inc/loader.h"
int main(int argc, char *argv[]) {
size_t i;
Binary bin; //represent elf file
Section *sec;
Symbol *sym;
std::string fname;
if (argc < 2) {
printf("need to set binary file name");
return 1;
}
fname.assign(argv[1]);
if (load_binary(fname, &bin, Binary::BIN_TYPE_AUTO) < 0) {
printf("load binary fail!");
return 1;
}
printf("loaded binary file name:%s", bin.filename.c_str());
printf("loaded binary file type: %s", bin.type_str.c_str());
printf("loaded binary file entry@0X%016jx\n: ", bin.entry);
printf("loaded binary file bits: %u", bin.bits);
for(i = 0; i < bin.sections.size(); i++) {
sec = &bin.sections[i];
printf(" 0x%16jx %-8ju %-20s %s\n", sec->vma, sec->size, sec->name.c_str(),
sec->type == Section::SEC_TYPE_CODE?"CODE":"DATA");
}
if (bin.symbols.size() > 0) {
printf("scanned symbol tables:\n");
for (i = 0; i < bin.symbols.size(); i++) {
sym = &bin.symbols[i];
printf(" %-40s 0x%016jx %s\n", sym->name.c_str(), sym->addr,
(sym->type & Symbol::SYM_TYPE_FUNC)? "FUNC":"");
}
}
unload_binary(&bin);
return 0;
}
代码中需要注意的是,loader.h是来自libbfd库的头文件,读者需要修改代码中该文件的路径以对应你电脑上libbfd的安装路径。Binary类用于对整个elf文件的抽象,通过它可以访问ELF文件相关信息,Section是对前面章节描述的段对象的抽象,Symbol是对前面章节符号表对象的抽象。
load_binary是来自libbfd库提供的函数,它将elf文件加载到内存中。上面代码编译时对应的Makefile内容为:
CXX=g++
OBJ=my_loader
.PHONY: all clean
all: $(OBJ)
loader.o: ../inc/loader.cc
$(CXX) -std=c++11 -c ../inc/loader.cc
my_loader: loader.o my_loader.cc
$(CXX) -std=c++11 -o my_loader my_loader.cc loader.o -lbfd
clean:
rm -f $(OBJ) *.o
执行make命令编译后,在本地目录会有my_loader可执行文件,使用命令./my_load a.out即可让程序加载a.out文件并输出一系列信息:
对于libbfd更加详细的使用方法,我们在后续章节会详细介绍。