前段时间分析了qemu中ELF文件的加载过程,个人感觉通过这个分析不但可以加深对ELF文件格式的理解,而且能够从侧面了解操作系统加载器的工作过程。
一、ELF相关的背景知识
1. ELF格式文件相关概念
ELF格式文件主要包括以下三种类型的文件:
- 可重定位的目标文件(.o文件) --> 用于链接生成可执行文件或动态链接库文件(.so)
从链接和执行的角度来讲,ELF文件存在两种视图:链接视图和执行视图。为了区分两种视图,只需记住链接视图由多个section组成,而执行视图由多个segment组成即可。另外,section是程序员可见的,是给链接器使用的概念,汇编文件中通常会显示的定义.text,.data等section,相反,segment是程序员不可见的,是给加载器使用的概念。下图形象的描述了ELF文件两种不同的视图的结构以及二者之间的联系。

二者之间的联系在于:一个segment包含一个或多个section。
注意:Section Header Table和Program Header Table并不是一定要位于文件的开头和结尾,其位置由ELF Header指出,上图这么画只是为了清晰。-- ELF文件每个部分的详细介绍参见《ELF 文件格式分析》。
TN05.ELF.Format.Summary.pdf
2. ELF文件主要数据结构
上面讲了ELF的相关概念,但是要想用计算机语言(C语言)来实现,必须对应相应的数据结构。linux下通过三个数据结构描述了ELF文件的相关概念。
(1) ELF Header
ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,每个成员的解释参见注释及附件。
#define EI_NIDENT 16
typedef struct{
/*ELF的一些标识信息,固定值*/
unsigned char e_ident[EI_NIDENT];
/*目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等*/
Elf32_Half e_type;
/*文件的目标体系结构类型:3-intel 80386*/
Elf32_Half e_machine;
/*目标文件版本:1-当前版本*/
Elf32_Word e_version;
/*程序入口的虚拟地址,如果没有入口,可为0*/
Elf32_Addr e_entry;
/*程序头表(segment header table)的偏移量,如果没有,可为0*/
Elf32_Off e_phoff;
/*节区头表(section header table)的偏移量,没有可为0*/
Elf32_Off e_shoff;
/*与文件相关的,特定于处理器的标志*/
Elf32_Word e_flags;
/*ELF头部的大小,单位字节*/
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 header中每个数据成员对应的值,下面是hello world的ELF文件头,在linux下可以通过"readelf -h ELF文件名"来获得。

ELF Header用数据结构
Elf32_Ehdr来表示,描述了操作系统是UNIX,体系结构是80386。Section Header Table中有30个Section Header,从文件地址4412开始,每个Section Header占40字节,Segment Header Table中有9个segment,每个segment header占32个字节,此ELF文件的类型是可执行文件(EXEC),入口地址是0x8048320。
(2) Section Header Table Entry
从ELF Header中可知,每个ELF文件有个Section Header Table,其中每一个表项对应一个section,由数据结构Elf32_Shdr来描述,每个成员的含义参见注释及附件。在linux下可以通过"readelf -S ELF文件名"来查看。
typedef struct{
/*节区名称*/
Elf32_Word sh_name;
/*节区类型:PROGBITS-程序定义的信息,NOBITS-不占用文件空间(bss),REL-重定位表项*/
Elf32_Word sh_type;
/*每一bit位代表一种信息,表示节区内的内容是否可以修改,是否可执行等信息*/
Elf32_Word sh_flags;
/*如果节区将出现在进程的内存影响中,此成员给出节区的第一个字节应处的位置*/
Elf32_Addr sh_addr;
/*节区的第一个字节与文件头之间的偏移*/
Elf32_Off sh_offset;
/*节区的长度,单位字节,NOBITS虽然这个值非0但不占文件中的空间*/
Elf32_Word sh_size;
/*节区头部表索引链接*/
Elf32_Word sh_link;
/*节区附加信息*/
Elf32_Word sh_info;
/*节区带有地址对齐的约束*/
Elf32_Word sh_addralign;
/*某些节区中包含固定大小的项目,如符号表,那么这个成员给出其固定大小*/
Elf32_Word sh_entsize;
}Elf32_Shdr;
(3) Program Header Table Entry
从ELF Header中可知,每个ELF文件有个Program Header Table,其中每一个表项对应一个segment,由数据结构Elf32_phdr来描述,每个成员的含义参见注释及附件。在linux下可以通过"readelf -l ELF文件名"来查看。
typedef struct
{
/*segment的类型:PT_LOAD= 1 可加载的段*/
Elf32_Word p_type;
/*从文件头到该段第一个字节的偏移*/
Elf32_Off p_offset;
/*该段第一个字节被放到内存中的虚拟地址*/
Elf32_Addr p_vaddr;
/*在linux中这个成员没有任何意义,值与p_vaddr相同*/
Elf32_Addr p_paddr;
/*该段在文件映像中所占的字节数*/
Elf32_Word p_filesz;
/*该段在内存映像中占用的字节数*/
Elf32_Word p_memsz;
/*段标志*/
Elf32_Word p_flags;
/*p_vaddr是否对齐*/
Elf32_Word p_align;
} Elf32_phdr;
二、qemu中ELF文件的加载过程
在了解了ELF文件的基本结构之后,大体可以想到ELF文件的加载过程就是一个查表的过程,即通过ELF Header得到ELF文件的基本信息-Section Header Table和program Header Table,然后再根据Section Header Table和program Header Table的信息加载ELF文件中的相应部分。上面也提到过,section是从链接器的角度来讲的概念,所以,ELF文件的加载过程中,只有segment是有效的,加载器根据program Header Table中的信息来负责ELF文件的加载。
首先,从感性上认识一下segment,还是以上面的hello world为例,其对应的program header table如下。

第一列type即每个segment的类型,每个类型的具体含义参见附件。通常我们之关心程序的代码段(.text section)和数据段(.date section),这两个section组成LOAD类型的segment。
Offset:当前segment加载到的地址的偏移
VirAddr:当前segment加载到的虚拟地址
PhysAddr:当前segment加载到的物理地址(x86平台上,此值没有意义,并不指物理地址)
FileSiz:当前segment在ELF文件中的偏移
MemSiz:当前segment在内存页中的偏移
Flg:segment的权限,R-可读,W-可写, E-可执行
Align:x86平台内存页面的大小
在了解了segment的相关信息后,分析下qemu代码中ELF文件的加载过程,印证下上面提到的ELF文件的加载的思想。
ret = loader_exec(filename, target_argv, target_environ, regs,info,&bprm);
filename:要加载的ELF文件的名称
target_argv:qemu运行的参数,在这里即hello(hello是生成的可执行文件名, $qemu hello)
target_environ:执行qemu的shell的环境变量
regs,info,bprm是ELF文件加载过程中涉及的三个重要数据结构,下面会详细分析。
loader_exec函数的功能及含义参见代码注释。
int loader_exec(const char* filename, char** argv, char** envp,
struct target_pt_regs * regs, struct image_info*infop,
struct linux_binprm *bprm)
{
int retval;
int i;
bprm->p= TARGET_PAGE_SIZE*MAX_ARG_PAGES-sizeof(unsignedint); /*MAX_ARG_PAGES= 33*/
memset(bprm->page, 0, sizeof(bprm->page));
retval = open(filename, O_RDONLY); /*返回打开文件的fd*/
if (retval< 0)
return retval;
bprm->fd= retval;
bprm->filename= (char *)filename;
bprm->argc= count(argv);
bprm->argv= argv;
bprm->envc= count(envp);
bprm->envp= envp;
/*1. 要加载文件的属性判断:是否常规文件,是否可执行文件,是否ELF文件; 2. 读取ELF文件的前1024个字节*/
retval = prepare_binprm(bprm);
if(retval>=0){ /*prepare_binrpm函数已经读出了目标文件的前1024个字节,先判断下这个文件是否是ELF文件,即前4个字节*/
if (bprm->buf[0]== 0x7f
&& bprm->buf[1]== 'E'
&& bprm->buf[2]== 'L'
&& bprm->buf[3]== 'F'){
retval = load_elf_binary(bprm, regs, infop);
#if defined(TARGET_HAS_BFLT)
} elseif (bprm->buf[0]== 'b'
&& bprm->buf[1]== 'F'
&& bprm->buf[2]== 'L'
&& bprm->buf[3]== 'T'){
retval = load_flt_binary(bprm,regs,infop);
#endif
} else{
fprintf(stderr,"Unknown binary format\n");
return -1;
}
}
if(retval>=0){
/* success. Initialize important registers*/
do_init_thread(regs, infop);
return retval;
}
/* Something went wrong, return the inodeand free the argument pages*/
for (i=0; i<MAX_ARG_PAGES; i++){
g_free(bprm->page[i]);
}
return(retval);
}
int load_elf_binary(struct linux_binprm* bprm, struct target_pt_regs* regs,
struct image_info * info)
{
struct image_info interp_info;
struct elfhdr elf_ex;
char *elf_interpreter = NULL;
info->start_mmap= (abi_ulong)ELF_START_MMAP; /*ELF_START_MMAP= 0x80000000*/
info->mmap= 0;
info->rss= 0;
/*主要工作就是初始化info,申请进程虚拟地址空间,将ELF文件映射到这段虚拟地址空间上*/
load_elf_image(bprm->filename, bprm->fd, info,
&elf_interpreter, bprm->buf);
... ... ... ...
return 0;
}
static void load_elf_image(const char*image_name,int image_fd,
struct image_info *info, char**pinterp_name,
char bprm_buf[BPRM_BUF_SIZE])
{
struct elfhdr *ehdr = (struct elfhdr *)bprm_buf;
struct elf_phdr *phdr;
abi_ulong load_addr, load_bias, loaddr, hiaddr,error;
int i, retval;
const char *errmsg;
/* First of all, some simple consistency checks*/
errmsg = "Invalid ELF image for this architecture";
if (!elf_check_ident(ehdr)){/*ELF头检查*/
goto exit_errmsg;
}
bswap_ehdr(ehdr); /*当前为空,是不是主机和目标机大小尾端不一致时才会swap*/
if (!elf_check_ehdr(ehdr)){
goto exit_errmsg;
}
/*下面的代码即读出ELF文件的程序头表,首先判断下是否已经被完全读出*/
i = ehdr->e_phnum* sizeof(struct elf_phdr); /*program header 表的大小*/
if (ehdr->e_phoff+ i <= BPRM_BUF_SIZE){
phdr = (struct elf_phdr *)(bprm_buf+ ehdr->e_phoff);
} else{
phdr = (struct elf_phdr *) alloca(i); /*申请i个程序头部*/
retval = pread(image_fd, phdr, i, ehdr->e_phoff); /*从文件image_id的偏移为ehdr->e_phoff处读取i个字节到phdr中,即phdr存放program header*/
if (retval!= i){
goto exit_read;
}
}
bswap_phdr(phdr, ehdr->e_phnum);
#ifdef CONFIG_USE_FDPIC
info->nsegs= 0;
info->pt_dynamic_addr= 0;
#endif
/* Find the maximum size of the imageand allocate an appropriate
amount of memory to handle that.*/
loaddr = -1, hiaddr= 0;
for (i= 0; i < ehdr->e_phnum;++i){/*遍历每一个program header*/
if (phdr[i].p_type== PT_LOAD){
abi_ulong a = phdr[i].p_vaddr;
if (a < loaddr){ /*loaddr= -1而且是unsigned 类型的,所以loaddr是个很大的数*/
loaddr = a; /*loaddr记录segment的起始地址*/
}
a += phdr[i].p_memsz; /*这个segment在内存中的偏移地址*/
if (a > hiaddr){ /*hiaddr记录segment的结束地址*/
hiaddr = a;
}
#ifdef CONFIG_USE_FDPIC
++info->nsegs;
#endif
}
}
load_addr = loaddr; /*计算出来的需要加载的起始地址*/
if (ehdr->e_type== ET_DYN){ /*共享目标文件(.so)*/
/* The image indicates that it can be loaded anywhere. Find a
location that can hold the memoryspace required.If the
image is pre-linked, LOADDR will be non-zero. Since wedo
not supply MAP_FIXED here we'll use that addressif and
only if it remains available.*/
load_addr = target_mmap(loaddr, hiaddr- loaddr, PROT_NONE,
MAP_PRIVATE | MAP_ANON| MAP_NORESERVE,
-1, 0);
if (load_addr== -1) {
goto exit_perror;
}
} elseif (pinterp_name!= NULL) {
/* Thisis the main executable. Make sure that the low
address does not conflict with MMAP_MIN_ADDRor the
QEMU application itself. */
probe_guest_base(image_name, loaddr, hiaddr);
}
load_bias = load_addr - loaddr;
#ifdef CONFIG_USE_FDPIC
{
struct elf32_fdpic_loadseg *loadsegs= info->loadsegs=
g_malloc(sizeof(*loadsegs)* info->nsegs);
for (i= 0; i < ehdr->e_phnum;++i){
switch (phdr[i].p_type){
case PT_DYNAMIC:
info->pt_dynamic_addr= phdr[i].p_vaddr+ load_bias;
break;
case PT_LOAD:
loadsegs->addr= phdr[i].p_vaddr+ load_bias;
loadsegs->p_vaddr= phdr[i].p_vaddr;
loadsegs->p_memsz= phdr[i].p_memsz;
++loadsegs;
break;
}
}
}
#endif
info->load_bias= load_bias; /*真实的加载地址和计算出来(读ELF头信息)的加载地址之差*/
info->load_addr= load_addr; /*真实的加载地址*/
info->entry= ehdr->e_entry+ load_bias; /*重新调整下程序的入口*/
info->start_code= -1;
info->end_code= 0;
info->start_data= -1;
info->end_data= 0;
info->brk= 0;
for (i= 0; i < ehdr->e_phnum; i++){
struct elf_phdr *eppnt = phdr + i;
if (eppnt->p_type== PT_LOAD){
abi_ulong vaddr, vaddr_po, vaddr_ps, vaddr_ef, vaddr_em;
int elf_prot = 0;
/*记录PT_LOAD类型segment的权限:读/写/可执行*/
if (eppnt->p_flags& PF_R) elf_prot= PROT_READ;
if (eppnt->p_flags& PF_W) elf_prot|= PROT_WRITE;
if (eppnt->p_flags& PF_X) elf_prot|= PROT_EXEC;
vaddr = load_bias + eppnt->p_vaddr;
vaddr_po = TARGET_ELF_PAGEOFFSET(vaddr);/*((vaddr)& ((1<< 12)-1)),目的是取页内偏移*/
vaddr_ps = TARGET_ELF_PAGESTART(vaddr); /*((vaddr)& ~(unsigned long)((1<< 12)-1)),向下页对齐,目的取页对齐的地址*/
/*将ELF文件映射到进程地址空间中*/
error= target_mmap(vaddr_ps, eppnt->p_filesz+ vaddr_po, /*映射的时候从页内偏移vaddr_po开始映射,即保持原来的偏移量*/
elf_prot, MAP_PRIVATE| MAP_FIXED,
image_fd, eppnt->p_offset- vaddr_po);
if (error ==-1) {
goto exit_perror;
}
vaddr_ef = vaddr + eppnt->p_filesz;
vaddr_em = vaddr + eppnt->p_memsz;
/*If the load segment requests extra zeros (e.g. bss), map it.*/
if (vaddr_ef < vaddr_em){
zero_bss(vaddr_ef, vaddr_em, elf_prot);
}
/* Find the full program boundaries.*/
if (elf_prot & PROT_EXEC){
if (vaddr < info->start_code){
info->start_code= vaddr; /*代码段的起始虚拟地址(页对齐的地址)*/
}
if (vaddr_ef > info->end_code){
info->end_code= vaddr_ef; /*代码段的结束虚拟地址(页对齐的地址)*/
}
}
if (elf_prot & PROT_WRITE){
if (vaddr < info->start_data){
info->start_data= vaddr; /*数据段的起始虚拟地址*/
}
if (vaddr_ef > info->end_data){
info->end_data= vaddr_ef; /*数据段的起始虚拟地址(包括bss段的大小)*/
}
if (vaddr_em > info->brk){
info->brk= vaddr_em; /*程序内存映像的顶端(代码段+数据段+bss段)*/
}
}
} elseif (eppnt->p_type== PT_INTERP&& pinterp_name){/*内部解释程序名称:/lib/ld-linux.so.2*/
char *interp_name;
if (*pinterp_name){
errmsg = "Multiple PT_INTERP entries";
goto exit_errmsg;
}
interp_name = malloc(eppnt->p_filesz);
if (!interp_name){
goto exit_perror;
}
if (eppnt->p_offset+ eppnt->p_filesz<= BPRM_BUF_SIZE){
memcpy(interp_name, bprm_buf+ eppnt->p_offset,
eppnt->p_filesz);
} else {
retval = pread(image_fd, interp_name, eppnt->p_filesz,
eppnt->p_offset);
if (retval != eppnt->p_filesz){
goto exit_perror;
}
}
if (interp_name[eppnt->p_filesz- 1] != 0){
errmsg = "Invalid PT_INTERP entry";
goto exit_errmsg;
}
*pinterp_name = interp_name;
}
}
if (info->end_data== 0){
info->start_data= info->end_code;
info->end_data= info->end_code;
info->brk= info->end_code;
}
if (qemu_log_enabled()){
load_symbols(ehdr, image_fd, load_bias);
}
close(image_fd);
return;
exit_read:
if (retval>= 0){
errmsg = "Incomplete read of file header";
goto exit_errmsg;
}
exit_perror:
errmsg = strerror(errno);
exit_errmsg:
fprintf(stderr,"%s: %s\n", image_name, errmsg);
exit(-1);
}
分享到:
相关推荐
ELF文件格式是Unix和类Unix系统,包括Linux,用于可执行程序、共享库以及动态加载的代码的主要格式。以下是对这个主题的详细说明: 1. **ELF文件格式**:ELF是一种标准化的文件格式,包含了程序的机器码、数据、...
在 Load_kernel() 函数中,QEMU 使用 Load_elf() 函数来加载内核映像, Load_elf() 函数在 elf_ops.h 文件中定义。 Load_elf() 函数会根据目标架构的位数来选择加载 32位或 64位的内核映像。 elf_ops.h 文件 在 ...
UEFI-BootLoader概览 概览它是一个 UEFI 应用程序,它在内存中加载和执行 elf 格式和原始二进制格式文件。 使用。 它是一个最小的实现,也可以作为 UEFI 的研究。我做它的重点是对代码的理解。特征简单的容易明白...
load_module() 函数将模块文件加载到内核中,并将其初始化。 在使用 qemu 和 gdb 调试时,可以使用断点来追踪加载模块的过程,并查看加载模块的详细过程。这可以帮助我们更好地理解模块加载的过程和机理。 insmod ...
生成的二进制文件可加载到QEMU的虚拟环境中运行。启动QEMU,指定相应的系统类型和二进制文件: ```bash qemu-system-riscv32 -nographic -machine virt -kernel hello ``` `-nographic`选项表示不使用图形界面,而...
uefi-elf-bootloader 该存储库包含一个简单的... 根目录中有一个run脚本,其中包含用于使用QEMU测试引导加载程序/内核组合的脚本。建立依赖GNU Make GNU EFI PATH存在一个x86_64-elf-gcc交叉编译器工具链项目结构该项
在 Bootloader 中,加载 ELF 文件通常包括以下几个步骤: - 分析 ELF 头部:Bootloader 首先读取并解析 ELF 文件的头部信息,获取段信息和入口点地址。 - 加载段到内存:根据 ELF 头部的段表,Bootloader 将每个...
elf 可重定位文件的链接器和加载器。 为 ARM 架构用 C 语言开发。 这是我教员的“系统编程”课程的一个项目。 计算机工程与信息理论系。 塞尔维亚贝尔格莱德大学电气工程学院。 由 Marin Markić 开发。 没有...
目前,您可以加载/转储内存、设置/清除断点、从 elf 文件加载额外信息(符号、调试信息、.text 段)、在断点上调度回调、运行与 elf 文件关联的二进制文件并显示与 elf 文件关联的源代码地址。依靠pip install ...
2. **固件分析**:了解固件的加载和执行过程,这可能需要对固件格式有一定的理解,例如ELF或BIN文件格式。 3. **创建测试入口**:固件可能没有明显的命令行接口,所以需要找到一个可以注入测试用例的入口点,比如...
ELF文件包含了程序的代码、数据和加载信息,bootloader需要解析这些信息以正确加载和执行内核。 5. **实现 kdebug.c::print_stackframe**:此部分要求学生完成`kdebug.c`中的`print_stackframe`函数,以便于调试时...
- waitdisk函数等待磁盘准备就绪,readsect函数读取单个扇区,而readseg函数则读取连续的扇区并加载到内存中,对应于ELF文件的各个段。 在进行ucore实验时,理解和掌握这些基本概念和技术是至关重要的,因为它们...
【清华大学操作系统实验lab1】是针对操作系统基础知识的一次实践性学习,主要涵盖了操作系统镜像文件的生成过程以及在QEMU虚拟机中进行软件执行和调试的技巧。实验内容包括两个部分:一是理解通过`make`生成执行文件...
5. **ELF(Executable and Linkable Format)**:ELF是大多数现代Unix-like系统中使用的一种文件格式,用于可执行程序、共享库和对象文件。在Linux内核开发中,理解ELF格式对于理解程序加载、链接和执行的过程至关...
4. **内核配置**:在内核配置中指定 init ramfs 的源路径,并确保内核支持 ELF 可执行文件格式。 5. **编译内核**:按照常规方式编译内核,此时 init ramfs 已经被集成到内核映像中。 **三、实验环境搭建** 为了...
以提供的"HelloElfLoader-master"为例,这可能是一个用于加载和执行ELF文件的简单程序。在WSL中,你可以通过以下步骤来实现: 1. 安装WSL:在Windows设置中搜索“Turn Windows features on or off”,开启WSL功能。...
对于MIPS架构,"elf"是指Executable and Linkable Format,这是Linux和其他类UNIX系统中广泛使用的可执行文件格式。因此,"mips-elf-gcc"(或类似的命令)就是我们用来编译MIPS架构程序的交叉编译器。 在编译过程中...
2. 编译源文件:使用NASM将汇编源文件转换成二进制目标文件,例如:`nasm -f elf32 boot.s -o boot.o` 和 `nasm -f elf32 head.s -o head.o`。 3. 链接目标文件:接下来,使用链接器(如`ld`)将目标文件链接成可...
# 基于x86架构的操作系统实现 ## 项目简介 本项目是一个基于x86架构的操作系统实现,涵盖了从系统引导、设备驱动、内存管理到任务调度、... 用户程序支持ELF文件解析和加载运行。 系统调用提供基本的系统调用接口。
链接地址是指编译器和链接器处理代码时使用的地址,而加载地址则是指实际加载到内存中的位置。这两者之间的差异通常是由于内存布局的变化所导致的,例如使用虚拟内存管理技术时。 理解链接地址与加载地址之间的关系...