elf文件格式
我们的程序是通过gcc编译的,在linux下,gcc编译出来的可执行文件是elf格式的二进制文件。那么肯定要elf文件进行解析才能正确的得到进程可执行数据的位置。
下面介绍一下elf格式的几个基本概念
一个程序中最重要的部分是段和节,他们是真正的程序体,存储程序执行所需要的数据,程序中有很多段,常见的有代码段和数据段,段是由节组成的。多个节经过链接之后被合并成一个段。
段和节的信息用header来描述,程序头是program header,节头是section header。
程序中段的大小和数量不固定,节也是如此,因此需要一个专门的数据结构来描述他们,这个就是程序头表和节头表,他们用来存储多个程序头和节头,相当于数组的概念。
由于程序中段和节的数量不固定,程序头表和节头表的大小也就不固定。并且各表在程序中存储的先后顺序不同,所有这些表在程序中存储的位置也是不固定了,为了能方便的找到这些表的位置,获取其信息,需要一个固定的结构来描述他们,记录其存储的位置和大小等信息,这个结构就是elf header
elf格式的作用体现在两方面,一是链接阶段,二是运行阶段。下图是这两方面elf格式数据的布局
下面重点说一下elf header中的数据
elf header
/* 32位elf头 */
struct Elf32_Ehdr
{
unsigned char e_ident[16];
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;
};
上面是elf header中存储的数据,里面涉及这几种数据类型,数据类型的本质是其所占的字节数,赋予数据类型意义是为了方便里面,下图是这几种类型代表的意义。
接下来是e_ident这个成员所表示的意义,见下图
下面是e_type所代表的意义
elf目标文件类型 | 取值 | 意义 |
---|---|---|
ET_NONE | 0 | 未知目标文件格式 |
ET_REL | 1 | 可重定位文件 |
ET_EXEC | 2 | 可执行文件 |
ET_DYN | 3 | 动态共享目标文件 |
ET_CORE | 4 | core文件 |
ET_LOPROC | 0xff00 | 特定处理器文件的扩展下边界 |
ET_HIPROC | 0xffff | 特定处理器文件的扩展上边界 |
虽然这里有很多的类型,但我们使用的只有ET_EXEC.
接下来是e_machine,它表示该文件在哪种硬件平台上才能运行
后面还有的数据不一一描述,这里从书上截图来说明
program header
程序头是专门用来描述段信息的,这个段不是内存中的段,内存中的段是记录在全局描述符表中的。程序头描述的段是磁盘上程序中的一个段,常见的如代码段和数据段,下面是其结构
struct Elf32_Phdr
{
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;
};
p_type所表示的意义如下
p_offset表示本段在文件的偏移量
p_vaddr表示本段在内存中起始的虚拟地址
p_paddr仅用于与物理地址相关的系统中
p_fiesz表示本段在文件中的大小
p_memsz表示本段在内存中的大小
p_flags的意义见下图
p_align表示本段在文件和内存中的对齐方式。
目标文件在链接之后代码和数据等资源都是在段中,有了上面这些结构来记录相关信息,程序在加载的时候就根据这些信息从磁盘的某个位置将程序运行所需的资源加载到内存中,接下来通过一个实例对elf文件进行分析一下。
这是我随便找的一个可执行文件查看的数据,用路线标出来的属于elf header中的数据,红线标出来的属于一个program header的数据,具体的意义可以对照着上面结构的字段去看。
接下来的工作就是要实现一个加载器,将一个可执行文件的段数据加载到内存中去。使cs:eip指向其入口地址,一个进程就运行起来了。
实现进程加载器
elf header和program header的数据结构
typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;
/* 32位elf头 */
struct Elf32_Ehdr
{
unsigned char e_ident[16];
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;
};
/* 程序头表Program header.就是段描述头 */
struct Elf32_Phdr
{
Elf32_Word p_type; // 见下面的enum segment_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;
};
/* 段类型 */
enum segment_type
{
PT_NULL, // 忽略
PT_LOAD, // 可加载程序段
PT_DYNAMIC, // 动态加载信息
PT_INTERP, // 动态加载器名称
PT_NOTE, // 一些辅助信息
PT_SHLIB, // 保留
PT_PHDR // 程序头表
};
加载一个段
/* 将文件描述符fd指向的文件中,偏移为offset,大小为filesz的段加载到虚拟地址为vaddr的内存 */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz, uint32_t vaddr)
{
uint32_t vaddr_first_page = vaddr & 0xfffff000; // vaddr地址所在的页框
uint32_t size_in_first_page = PG_SIZE - (vaddr & 0x00000fff); // 加载到内存后,文件在第一个页框中占用的字节大小
uint32_t occupy_pages = 0;
/* 若一个页框容不下该段 */
if (filesz > size_in_first_page)
{
uint32_t left_size = filesz - size_in_first_page;
occupy_pages = DIV_ROUND_UP(left_size, PG_SIZE) + 1; // 1是指vaddr_first_page
}
else
{
occupy_pages = 1;
}
/* 为进程分配内存 */
uint32_t page_idx = 0;
uint32_t vaddr_page = vaddr_first_page;
while (page_idx < occupy_pages)
{
uint32_t *pde = pde_ptr(vaddr_page);
uint32_t *pte = pte_ptr(vaddr_page);
/* 如果pde不存在,或者pte不存在就分配内存.
* pde的判断要在pte之前,否则pde若不存在会导致
* 判断pte时缺页异常 */
if (!(*pde & 0x00000001) || !(*pte & 0x00000001))
{
if (get_a_page(PF_USER, vaddr_page) == NULL)
{
return false;
}
} // 如果原进程的页表已经分配了,利用现有的物理页,直接覆盖进程体
vaddr_page += PG_SIZE;
page_idx++;
}
sys_lseek(fd, offset, SEEK_SET);
sys_read(fd, (void *)vaddr, filesz);
return true;
}
/* 从文件系统上加载用户程序pathname,成功则返回程序的起始地址,否则返回-1 */
static int32_t load(const char *pathname)
{
int32_t ret = -1;
struct Elf32_Ehdr elf_header;
struct Elf32_Phdr prog_header;
memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));
int32_t fd = sys_open(pathname, O_RDONLY);
if (fd == -1)
{
return -1;
}
if (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) != sizeof(struct Elf32_Ehdr))
{
ret = -1;
goto done;
}
/* 校验elf头 */
if (memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) || elf_header.e_type != 2 || elf_header.e_machine != 3 || elf_header.e_version != 1 || elf_header.e_phnum > 1024 || elf_header.e_phentsize != sizeof(struct Elf32_Phdr))
{
ret = -1;
goto done;
}
Elf32_Off prog_header_offset = elf_header.e_phoff;
Elf32_Half prog_header_size = elf_header.e_phentsize;
/* 遍历所有程序头 */
uint32_t prog_idx = 0;
while (prog_idx < elf_header.e_phnum)
{
memset(&prog_header, 0, prog_header_size);
/* 将文件的指针定位到程序头 */
sys_lseek(fd, prog_header_offset, SEEK_SET);
/* 只获取程序头 */
if (sys_read(fd, &prog_header, prog_header_size) != prog_header_size)
{
ret = -1;
goto done;
}
/* 如果是可加载段就调用segment_load加载到内存 */
if (PT_LOAD == prog_header.p_type)
{
if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz, prog_header.p_vaddr))
{
ret = -1;
goto done;
}
}
/* 更新下一个程序头的偏移 */
prog_header_offset += elf_header.e_phentsize;
prog_idx++;
}
ret = elf_header.e_entry;
done:
sys_close(fd);
return ret;
}