明天剖析下Linux下一个可执行文件是如何载入和执行的。

Linux下标准的可执行文件格式是ELF。

ELF(ExecutableandLinkingFormat)是一种对象文件的格式。

在linux系统中,一个ELF文件主要拿来表示3种类型的文件

明天剖析一下可执行文件类型。

当我们在Linux下的bash下输入一个命令执行可执行程序时linux执行sh文件,bash进程会调用fork()创建一个新的进程,之后新的进程调用execve()系统调用来执行指定的可执行程序。

原本的bash进程继续返回等待刚刚启动的新进程结束,之后继续等待用户的输入。

execve()系统调用原型如下:


int execve(const char *filename, char *const argv[], char *const envp[]);

她们的三个参数分别是被执行的程序文件名、执行参数和环境变量。

当调用execve()系统调用时,步入内核调用过程如下

sys_execve()
--> do_execve() // 主要根据可执行文件进行构造 linux_binprm 内核结构,该结构记录可执行文件信息,然后从formats链表中找到执行该执行文件的方法
--> load_elf_binary

do_execve主要完成linux_binprm内核结构的初始化,该结构定义如下:

struct linux_binprm{
char buf[128];
/*与可执行文件路径名的处理一样,每个参数的最大长度定为一个物理页,所以设置为一个页面指针数组,最大个数为32*/
unsigned long page[MAX_ARG_PAGES];
unsigned long p;
int sh_bang; //可执行文件的性质,当时shell脚本时为1
struct inode * inode; //可执行文件的inode
int e_uid, e_gid; //可执行文件的属性
int argc, envc; //命令行参数和环境变量数目
char * filename; //可执行文件的路径名 
};

该结构主要记录了执行可执行文件所有须要的信息。

其中page表示的是储存参数的页面字段,而p表示的是在这种字段的底部,由于这种字符串是根据栈的形式储存的linux培训学校,也就是说,先分配地址更高的字段,向低地址方向下降,p就指向栈底部。

结构如右图

执行文件是什么意思_linux执行sh文件_执行文件linux

最终执行可执行文件的插口为load_elf_binary。

具体实现如下:

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
struct file * file;
...
status = 0;
load_addr = 0;

执行文件linux_执行文件是什么意思_linux执行sh文件

elf_ex = *((struct elfhdr *) bprm->buf); /* exec-header */ //比对四个字符,必须是0x7f、‘E’、‘L’、和‘F’ if (elf_ex.e_ident[0] != 0x7f || strncmp(&elf_ex.e_ident[1], "ELF",3) != 0) return -ENOEXEC; //映像类型必须是ET_EXEC if(elf_ex.e_type != ET_EXEC || (elf_ex.e_machine != EM_386 && elf_ex.e_machine != EM_486) || (!bprm->inode->i_op || !bprm->inode->i_op->default_file_ops || !bprm->inode->i_op->default_file_ops->mmap)){ return -ENOEXEC; }; elf_phdata = (struct elf_phdr *) kmalloc(elf_ex.e_phentsize * elf_ex.e_phnum, GFP_KERNEL); old_fs = get_fs(); set_fs(get_ds()); //获取所有程序头表信息 retval = read_exec(bprm->inode, elf_ex.e_phoff, (char *) elf_phdata, elf_ex.e_phentsize * elf_ex.e_phnum); set_fs(old_fs); elf_ppnt = elf_phdata; elf_bss = 0; elf_brk = 0; elf_exec_fileno = open_inode(bprm->inode, O_RDONLY); file = current->files->fd[elf_exec_fileno]; elf_stack = 0xffffffff; elf_interpreter = NULL; start_code = 0; end_code = 0; end_data = 0; old_fs = get_fs(); set_fs(get_ds());

执行文件是什么意思_linux执行sh文件_执行文件linux

/* 处理解释器段,通过遍历每个段,找到PT_INTERP类型段,也即是解释器段,找到说明需要运行过程中的动态链接。 “解释器”段实际上只是一个字符串,即解释器的文件名,如”/lib/ld-linux.so.2”, 或者64位机器上对应的叫做”/lib64/ld-linux-x86-64.so.2” 通过命令 readelf -l 可执行文件 获取解释器段信息 type 类型为 INTERP */ for(i=0;i p_type == PT_INTERP) { // 该类型表示动态连接器 elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz, GFP_KERNEL); //根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区; //从用户程序的program header 中读取动态链接器的路径,比如 /lib64/ld-linux-x86-64.so retval = read_exec(bprm->inode,elf_ppnt->p_offset,elf_interpreter, elf_ppnt->p_filesz); if(retval >= 0) //获取连接器的inod retval = namei(elf_interpreter, &interpreter_inode); if(retval >= 0) //解释器也是一个elf格式的程序,读入解释器的前128个字节,即解释器映像的头部 retval = read_exec(interpreter_inode,0,bprm->buf,128); if(retval >= 0){ interp_ex = *((struct exec *) bprm->buf); /* exec-header */ interp_elf_ex = *((struct elfhdr *) bprm->buf); /* exec-header */ }; }; elf_ppnt++; }; set_fs(old_fs); //检查并读取解释器(也可以叫动态链接器)的程序头表 if(elf_interpreter){ ... } if (!bprm->sh_bang) { ... }

linux执行sh文件_执行文件linux_执行文件是什么意思

//在此清除掉了父进程的所有相关代码 flush_old_exec(bprm); current->mm->end_data = 0; current->mm->end_code = 0; current->mm->start_mmap = ELF_START_MMAP; current->mm->mmap = NULL; elf_entry = (unsigned int) elf_ex.e_entry; current->mm->rss = 0; //建立环境变量参数的页表映射,从虚拟地址 0xC0000000UL 处开始 bprm->p += setup_arg_pages(0, bprm->page); current->mm->start_stack = bprm->p; old_fs = get_fs(); set_fs(get_ds()); elf_ppnt = elf_phdata; for(i=0;i p_type == PT_INTERP) { set_fs(old_fs); //不装入解释器,那么这个入口地址就是目标映像本身的入口地址 if(interpreter_type & 1) elf_entry = load_aout_interp(&interp_ex, interpreter_inode); //加载text data bss段 //如果需要装入解释器,就通过load_elf_interp装入其映像, 并把将来进入用户空间的入口地址设置成load_elf_interp()的返回值,即解释器映像的入口地址 if(interpreter_type & 2) elf_entry = load_elf_interp(&interp_elf_ex, interpreter_inode); old_fs = get_fs(); set_fs(get_ds()); iput(interpreter_inode); kfree(elf_interpreter); if(elf_entry == 0xffffffff) { ... }; }; // 类型为 PT_LOAD 需要映射到进程的地址虚拟空间的 if(elf_ppnt->p_type == PT_LOAD) { error = do_mmap(file, elf_ppnt->p_vaddr & 0xfffff000, elf_ppnt->p_filesz + (elf_ppnt->p_vaddr & 0xfff), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE, elf_ppnt->p_offset & 0xfffff000); #ifdef LOW_ELF_STACK if(elf_ppnt->p_vaddr & 0xfffff000 p_vaddr & 0xfffff000; #endif if(!load_addr) load_addr = elf_ppnt->p_vaddr - elf_ppnt->p_offset; k = elf_ppnt->p_vaddr; if(k > start_code) start_code = k; k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz; if(k > elf_bss) elf_bss = k; if((elf_ppnt->p_flags | PROT_WRITE) && end_code < k) end_code = k; if(end_data p_vaddr + elf_ppnt->p_memsz; if(k > elf_brk) elf_brk = k; }; elf_ppnt++; }; set_fs(old_fs); kfree(elf_phdata); if(interpreter_type != INTERPRETER_AOUT) sys_close(elf_exec_fileno); ... current->executable = bprm->inode; bprm->inode->i_count++; #ifdef LOW_ELF_STACK current->start_stack = p = elf_stack - 4; #endif bprm->p -= MAX_ARG_PAGES*PAGE_SIZE; /* create_elf_tables填写目标文件的参数环境变量等必要信息 在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,这些信息包括常规的argc、envc等等,还有一些"辅助向量(Auxiliary Vector)"。 这些信息需要复制到用户空间,使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆栈上。这里的create_elf_tables()就起着这个作用。 */

执行文件linux_执行文件是什么意思_linux执行sh文件

bprm->p = (unsigned long)create_elf_tables((char *)bprm->p, bprm->argc, bprm->envc, (interpreter_type == INTERPRETER_ELF ? &elf_ex : NULL), load_addr, (interpreter_type == INTERPRETER_AOUT ? 0 : 1)); if(interpreter_type == INTERPRETER_AOUT) current->mm->arg_start += strlen(passed_fileno) + 1; //调整内存映射内容 current->mm->start_brk = current->mm->brk = elf_brk; current->mm->end_code = end_code; current->mm->start_code = start_code; current->mm->end_data = end_data; current->mm->start_stack = bprm->p; current->suid = current->euid = bprm->e_uid; current->sgid = current->egid = bprm->e_gid; current->mm->brk = (elf_bss + 0xfff) & 0xfffff000; sys_brk((elf_brk + 0xfff) & 0xfffff000); padzero(elf_bss); ////eip和esp改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口 start_thread(regs, elf_entry, bprm->p); if (current->flags & PF_PTRACED) send_sig(SIGTRAP, current, 0); MOD_DEC_USE_COUNT; return 0; } static inline void start_thread(struct pt_regs * regs, unsigned long eip, unsigned long esp) { regs->cs = USER_CS; regs->ds = regs->es = regs->ss = regs->fs = regs->gs = USER_DS; regs->eip = eip; regs->esp = esp; }

该函数主要完成的功能如下:

当load_elf_binary()执行完毕,返回至do_execve()再返回至sys_execve()时,因为上述步骤早已把系统调用的返回地址改成了被装载的ELF可执行程序的入口地址,所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了LF程序的入口地址linux执行sh文件linux软件下载,于是新的程序开始执行,ELF可执行文件装载完成。

Tagged:
Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复