一个程序的前世今生(二)——可执行文件如何加载进内存
二、 加载--可执行文件放入内存
通过前一章可以知道一个程序是如何从我们编写的代码变成一个可以执行的文件的。但是此时它仍是放在磁盘上的一个文件,并不是我们通常理解的程序--在内存上运行的一段代码。
程序运行在内存上,所以首先我们需要了解虚拟内存的一些基本知识,然后我们以linux上在shell会话中执行./hello这一命令来跟踪可执行文件是如何在内存中运行起来的。
2.1 执行shell时发生了什么
我们可以使用strace命令跟踪程序执行的过程,在shell执行脚本时其实是根据文件开头的脚本类型声明调用对应的脚本解释器,如果shell发现执行的是可执行程序则会调用execv系统调用。
可以看出shell调用了一个叫做execve的系统调用来执行hello这个程序,系统调用如何执行到的下一篇再分析,先在这里留个坑。最终会执行内核的d__do_execve_file这个函数,我们接下来分析它时如何执行ELF格式的文件的。
2.2 execve
函数位置: fs/exec.c,基本调用关系如下图,前边流程是准备过程,最后一步红线标注的为调用可执行文件的过程,函数的简化流程见下方
static int __do_execve_file(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags, struct file *file) { char *pathbuf = NULL; struct linux_binprm *bprm; struct files_struct *displaced; int retval; 。。。 retval = unshare_files(&displaced); //不使用shell程序打开文件 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); retval = prepare_bprm_creds(bprm); //对文件进行安全策略检测 if (!file) file = do_open_execat(fd, filename, flags); //因为传入的file为null,在此处就根据filename打开可执行文件 sched_exec(); //在这个函数中使用调度类对当前的进程进行调度 bprm->file = file; bprm->filename = filename->name; bprm->interp = bprm->filename; retval = bprm_mm_init(bprm); //初始化bprm的mm结构体,即内存相关分配,主要是初始化了mm_struct retval = prepare_arg_pages(bprm, argv, envp); //计算出入参和环境变量的数量 retval = prepare_binprm(bprm); //填充gid和uid用于权限管理,并且使用elf的前128字节填充buf数组 retval = copy_strings_kernel(1, &bprm->filename, bprm); //拷贝文件名到新分配的页面中 bprm->exec = bprm->p; retval = copy_strings(bprm->envc, envp, bprm); //拷贝环境变量,因为栈向下增长所以先拷贝环境变量会使它处在栈中入参的后方 retval = copy_strings(bprm->argc, argv, bprm); //拷贝入参 would_dump(bprm, bprm->file); retval = exec_binprm(bprm); //这里开始执行可执行文件 /* execve succeeded */ 。。。 //下方是执行错误或成功后的处理流程,暂不分析 }
程序中对文件的操作都使用到了这个结构体linux_binprm,在调用实际执行文件的函数时入参也是这个,结构体定义在binfmts.h中
struct linux_binprm{ char buf[BINPRM_BUF_SIZE]; //保存课执行文件的头128个字节 #ifdef CONFIG_MMU struct vm_area_struct *vma; unsigned long vma_pages; //当前内存页的最高地址 #else # define MAX_ARG_PAGES 32 struct page *page[MAX_ARG_PAGES]; #endif struct mm_struct *mm; unsigned long p; /* current top of mem */ unsigned int cred_prepared:1,/* true if creds already prepared (multiple * preps happen for interpreters) */ cap_effective:1;/* true if has elevated effective capabilities, * false if not; except for init which inherits * its parent's caps anyway */ #ifdef __alpha__ unsigned int taso:1; #endif unsigned int recursion_depth; struct file * file; //要执行的文件 struct cred *cred; /* new credentials */ int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */ unsigned int per_clear; /* bits to clear in current->personality */ int argc, envc; //命令行参数和环境变量参数 char * filename; /* Name of binary as seen by procps */ //要被执行的文件的名的二进制 char * interp; /* Name of the binary really executed. Most of the time same as filename, but could be different for binfmt_{misc,script} */ //要被执行的文件的真实名,通常和filename相同 unsigned interp_flags; unsigned interp_data; unsigned long loader, exec; };
2.3 execve-->load_elf_binary
上一节找到了"__do_execve_file" -> "exec_binbprm" -> "search_binary_handler" -> "fmt->load_binary"这一条路径,那怎么到了load_elf_binary这个函数呢?我们返回search_binary_handler函数查看一下
int search_binary_handler(struct linux_binprm *bprm) { bool need_retry = IS_ENABLED(CONFIG_MODULES); struct linux_binfmt *fmt; //我们之前说的用于文件操作的结构体 int retval; //省略部分无关代码 retry: read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { //从formats中遍历找到符合条件的文件格式:fmt if (!try_module_get(fmt->module)) //如果一个模块处于活动状态 continue; read_unlock(&binfmt_lock); bprm->recursion_depth++; retval = fmt->load_binary(bprm); //调用对应格式注册的load_binary函数,bprm格式和遍历到的不一致内部会返回错误并继续搜寻剩余的,我们简化为这里找到了对应的文件,即ELF格式 bprm->recursion_depth--; read_lock(&binfmt_lock); } read_unlock(&binfmt_lock); //省略无关代码 return retval; }
可以看到,这里根据fomats为头的链表逐个遍历,找到和bprm的fmt一致的已经注册到内核的结构进行load操作,那么formats从哪儿来的呢?
在上节的结构体linux_binprm的文件中可以找到来历,其实在search_binary_handler中就可以看到,判断文件类型是否相符的入参就是linux_binprm类型,所以formats的来历从它找准没错,我们到binfmts.h中可以找到如下代码:
/* * This structure defines the functions that are used to load the binary formats that * linux accepts. */ struct linux_binfmt { struct list_head lh; struct module *module; int (*load_binary)(struct linux_binprm *); int (*load_shlib)(struct file *); int (*core_dump)(struct coredump_params *cprm); unsigned long min_coredump; /* minimal dump size */ } __randomize_layout; extern void __register_binfmt(struct linux_binfmt *fmt, int insert); /* Registration of default binfmt handlers */ static inline void register_binfmt(struct linux_binfmt *fmt) { __register_binfmt(fmt, 0); } /* Same as above, but adds a new binfmt at the top of the list */ static inline void insert_binfmt(struct linux_binfmt *fmt) { __register_binfmt(fmt, 1); }
__register_binfmt函数在exec.c中,所以从这里基本就可以看出来,例如处理ELF格式的模块在模块初始化时就把模块名和加载方法通过注册方式添加到formts的链表中,所以在执行文件的时候就可以根据遍历formats来寻找系统可用的格式。
static LIST_HEAD(formats);
static DEFINE_RWLOCK(binfmt_lock);
void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
BUG_ON(!fmt);
if (WARN_ON(!fmt->load_binary))
return;
write_lock(&binfmt_lock);
insert ? list_add(&fmt->lh, &formats) :
list_add_tail(&fmt->lh, &formats);
write_unlock(&binfmt_lock);
}
那怎么注册进来的呢? 在fs/binfmt_elf.c可以找到答案。这部分需要linux模块加载的基本知识,不了解的可以去搜一下。简单理解就是一个模块加载进linux系统的时候会先执行一个module_init的程序初始化自己,elf注册到formats的过程就在elf模块的初始化函数处。
static int __init init_elf_binfmt(void) { register_binfmt(&elf_format); return 0; } static void __exit exit_elf_binfmt(void) { /* Remove the COFF and ELF loaders. */ unregister_binfmt(&elf_format); } core_initcall(init_elf_binfmt);
module_exit(exit_elf_binfmt);
好了,讲完来历下面我们可以来看一下这个函数是如何加载和执行我们的输入文件了。
2.4 load_elf_binary
实在太长了,先加个注释吧,有时间了再试着画个图梳理下。几个相对复杂的调用下方做一些分析,可以先看下面涉及的函数辅助注释进行理解。static int load_elf_binary(struct linux_binprm *bprm){struct file *interpreter = NULL; /* to shut gcc up */
unsigned long load_addr = 0, load_bias = 0;
int load_addr_set = 0;
unsigned long error;
struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
unsigned long elf_bss, elf_brk;
int bss_prot = 0;
int retval, i;
unsigned long elf_entry;
unsigned long interp_load_addr = 0;
unsigned long start_code, end_code, start_data, end_data;
unsigned long reloc_func_desc __maybe_unused = 0;
int executable_stack = EXSTACK_DEFAULT;
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
} *loc;
struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
struct pt_regs *regs;
loc = kmalloc(sizeof(*loc), GFP_KERNEL);
if (!loc) {
retval = -ENOMEM;
goto out_ret;
}
/* 填充ELF头信息
在load_elf_binary之前内核已经使用映像文件的前128个字节对bprm->buf进行了填充,
这里使用这此信息填充映像的文件头,参考上一节内容
*/
loc->elf_ex = *((struct elfhdr *)bprm->buf);
retval = -ENOEXEC;
/* First of all, some simple consistency checks
比较文件头的前四个字节,查看是否是ELF文件类型定义的"\177ELF"*/
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out;
/*除前4个字符以外,还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库
*/
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
goto out;
/* 检查特定的目标机器标识 */
if (!elf_check_arch(&loc->elf_ex))
goto out;
if (elf_check_fdpic(&loc->elf_ex))
goto out;
if (!bprm->file->f_op->mmap)
goto out;
/*
load_elf_phdrs 加载程序头表
load_elf_phdrs函数就是通过kernel_read读入整个program header table
从函数代码中可以看到,一个可执行程序必须至少有一个段(segment),
而所有段的大小之和不能超过64K。
*/
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
if (!elf_phdata)
goto out;
/*
3. 寻找和处理解释器段
这个for循环的目的在于寻找和处理目标映像的"解释器"段。
"解释器"段的类型为PT_INTERP,
找到后就根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区。
"解释器"段实际上只是一个字符串,
即解释器的文件名,如"/lib/ld-linux.so.2"。
有了解释器的文件名以后,就通过open_exec()打开这个文件,
再通过kernel_read()读入其开关128个字节,即解释器映像的头部。*/
elf_ppnt = elf_phdata;
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { //根据段数目逐条遍历各个段
char *elf_interpreter;
loff_t pos;
/* 3.1 检查段类型是否为PT_INTERP即解释器段,不是则遍历下一个 */
if (elf_ppnt->p_type != PT_INTERP)
continue;
/*
* This is the program interpreter used for shared libraries -
* for now assume that this is an a.out format binary.
*/
retval = -ENOEXEC;
if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2)
goto out_free_ph;
retval = -ENOMEM;
/* 为动态连接器分配空间并读取加载 */
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
if (!elf_interpreter)
goto out_free_ph;
/* 3.2 根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区 */
pos = elf_ppnt->p_offset;
retval = kernel_read(bprm->file, elf_interpreter,
elf_ppnt->p_filesz, &pos);
if (retval != elf_ppnt->p_filesz) {
if (retval >= 0)
retval = -EIO;
goto out_free_interp;
}
/* make sure path is NULL terminated */
retval = -ENOEXEC;
if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
goto out_free_interp;
/* 3.3 通过open_exec()打开解释器文件
内核把新进程的堆栈中设置一些标记对,
以指示动态链接器的相关操作,详见open_exec实现 */
interpreter = open_exec(elf_interpreter);
kfree(elf_interpreter);
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter))
goto out_free_ph;
/*
* If the binary is not readable then enforce mm->dumpable = 0
* regardless of the interpreter's permissions.
*/
would_dump(bprm, interpreter);
/* Get the exec headers */
pos = 0;
/* 3.4 通过kernel_read()读入解释器的前128个字节,即解释器映像的头部。*/
retval = kernel_read(interpreter, &loc->interp_elf_ex,
sizeof(loc->interp_elf_ex), &pos);
if (retval != sizeof(loc->interp_elf_ex)) {
if (retval >= 0)
retval = -EIO;
goto out_free_dentry;
}
break;
out_free_interp:
kfree(elf_interpreter);
goto out_free_ph;
}
elf_ppnt = elf_phdata;
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
/* 检查堆栈可执行性,保存在executable_stack */
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;
/* PT_LOPROC和PT_HIPROC类型的Segment用来提供给特定的计算机体系进行检查 */
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt,
bprm->file, false,
&arch_state);
if (retval)
goto out_free_dentry;
break;
}
/* 4. 检查并读取解释器的程序表头 */
/* Some simple consistency checks for the interpreter */
/* 4.1 检查解释器头的信息
检查是否由动态连接器,无论是否有动态连接器都会执行elf文件 */
if (interpreter) {
retval = -ELIBBAD;
/* Not an ELF interpreter */
if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out_free_dentry;
/* Verify the interpreter has a valid arch */
if (!elf_check_arch(&loc->interp_elf_ex) ||
elf_check_fdpic(&loc->interp_elf_ex))
goto out_free_dentry;
/* Load the interpreter program headers
4.2 读入解释器的程序头
*/
interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
interpreter);
if (!interp_elf_phdata)
goto out_free_dentry;
/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
elf_ppnt = interp_elf_phdata;
for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(&loc->interp_elf_ex,
elf_ppnt, interpreter,
true, &arch_state);
if (retval)
goto out_free_dentry;
break;
}
}
/*
* Allow arch code to reject the ELF at this point, whilst it's
* still possible to return an error to the code that invoked
* the exec syscall.
*/
retval = arch_check_elf(&loc->elf_ex,
!!interpreter, &loc->interp_elf_ex,
&arch_state);
if (retval)
goto out_free_dentry;
/* Flush all traces of the currently running executable
在此清除掉了父进程的所有相关代码 */
retval = flush_old_exec(bprm);
if (retval)
goto out_free_dentry;
/* Do this immediately, since STACK_TOP as used in setup_arg_pages
may depend on the personality. */
/* 设置elf可执行文件的特性 */
SET_PERSONALITY2(loc->elf_ex, &arch_state);
if (elf_read_implies_exec(loc->elf_ex, executable_stack))
current->personality |= READ_IMPLIES_EXEC;
if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
current->flags |= PF_RANDOMIZE;
setup_new_exec(bprm);
install_exec_creds(bprm);
/* Do this so that we can load the interpreter, if need be. We will
change some of these later
为下面的动态连接器执行获取内核空间page */
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
if (retval < 0)
goto out_free_dentry;
/* bss段,brk段先初始化为0 */
elf_bss = 0;
elf_brk = 0;
/* code代码段 */
start_code = ~0UL;
end_code = 0;
/* data数据段 */
start_data = 0;
end_data = 0;
/* Now we do a little grungy work by mmapping the ELF image into
the correct location in memory.
5 装入目标程序的段segment
这段代码从目标映像的程序头中搜索类型为PT_LOAD的段(Segment)。在二进制映像中,只有类型为PT_LOAD的段才是需要装入的。
当然在装入之前,需要确定装入的地址,只要考虑的就是页面对齐,还有该段的p_vaddr域的值(上面省略这部分内容)。
确定了装入地址后,就通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址。
*/
/*如果要加载的文件数据类型为ET_EXEC,则在固定地址上分配虚拟内存,因此要加上MAP_FIXED标志,
而如果要加载的数据类型为ET_DYN,则需要从ELF_ET_DYN_BASE地址处开始映射时,在设置了PF_RANDOMIZE标志位时,需要加上arch_mmap_rnd()随机因子,将偏移记录到load_bias中。total_size为计算的需要映射的内存大小。
再往下就通过elf_map函数将文件映射到虚拟内存中。
如果是第一次映射,则需要记录虚拟的elf文件装载地址load_addr,如果是ET_DYN类型的数据,需要加上偏移load_bias。
每次映射后,都要修改bss段、代码段、数据段、堆的起始位置,
对同一个elf文件而言,start_code向上增长,start_data向下增长,elf_bss向上增长,end_code 向上增长,end_data 向上增长,elf_brk向上增长,
因此从虚拟内存中看,从低地址到高地址依次为代码段,数据段,bss段和堆的起始地址。当装载完毕退出循环后需要将这些变量加上偏移load_bias。
最后通过set_brk在elf_bss到elf_brk之间分配内存空间。*/
/* 按照先前获取的程序头表,循环将所有的可执行文件加载到内存中 */
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot, elf_flags, elf_fixed = MAP_FIXED_NOREPLACE;
unsigned long k, vaddr;
unsigned long total_size = 0;
/* 5.1 搜索PT_LOAD的段, 这个是需要装入的 */
if (elf_ppnt->p_type != PT_LOAD)
continue;
if (unlikely (elf_brk > elf_bss)) {
unsigned long nbyte;
/* 5.2 检查地址和页面的信息 */
/* There was a PT_LOAD segment with p_memsz > p_filesz
before this one. Map anonymous pages, if needed,
and clear the area. */
retval = set_brk(elf_bss + load_bias,
elf_brk + load_bias,
bss_prot);
if (retval)
goto out_free_dentry;
nbyte = ELF_PAGEOFFSET(elf_bss);
if (nbyte) {
nbyte = ELF_MIN_ALIGN - nbyte;
if (nbyte > elf_brk - elf_bss)
nbyte = elf_brk - elf_bss;
if (clear_user((void __user *)elf_bss +
load_bias, nbyte)) {
/*
* This bss-zeroing can fail if the ELF
* file specifies odd protections. So
* we don't check the return value
*/
}
}
/*
* Some binaries have overlapping elf segments and then
* we have to forcefully map over an existing mapping
* e.g. over this newly established brk mapping.
*/
elf_fixed = MAP_FIXED;
}
elf_prot = make_prot(elf_ppnt->p_flags);
elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
vaddr = elf_ppnt->p_vaddr;
/*
* If we are loading ET_EXEC or we have already performed
* the ET_DYN load_addr calculations, proceed normally.
*/
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
elf_flags |= elf_fixed;
} else if (loc->elf_ex.e_type == ET_DYN) {
/*
* This logic is run once for the first LOAD Program
* Header for ET_DYN binaries to calculate the
* randomization (load_bias) for all the LOAD
* Program Headers, and to calculate the entire
* size of the ELF mapping (total_size). (Note that
* load_addr_set is set to true later once the
* initial mapping is performed.)
*
* There are effectively two types of ET_DYN
* binaries: programs (i.e. PIE: ET_DYN with INTERP)
* and loaders (ET_DYN without INTERP, since they
* _are_ the ELF interpreter). The loaders must
* be loaded away from programs since the program
* may otherwise collide with the loader (especially
* for ET_EXEC which does not have a randomized
* position). For example to handle invocations of
* "./ld.so someprog" to test out a new version of
* the loader, the subsequent program that the
* loader loads must avoid the loader itself, so
* they cannot share the same load range. Sufficient
* room for the brk must be allocated with the
* loader as well, since brk must be available with
* the loader.
*
* Therefore, programs are loaded offset from
* ELF_ET_DYN_BASE and loaders are loaded into the
* independently randomized mmap region (0 load_bias
* without MAP_FIXED).
*/
if (interpreter) {
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
elf_flags |= elf_fixed;
} else
load_bias = 0;
/*
* Since load_bias is used for all subsequent loading
* calculations, we must lower it by the first vaddr
* so that the remaining calculations based on the
* ELF vaddrs will be correctly offset. The result
* is then page aligned.
*/
load_bias = ELF_PAGESTART(load_bias - vaddr);
total_size = total_mapping_size(elf_phdata,
loc->elf_ex.e_phnum);
if (!total_size) {
retval = -EINVAL;
goto out_free_dentry;
}
}
/* 5.3 虚拟地址空间与目标映像文件的映射
确定了装入地址后,
就通过elf_map()建立用户空间虚拟地址空间
与目标映像文件中某个连续区间之间的映射,
其返回值就是实际映射的起始地址 */
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
if (BAD_ADDR(error)) {
retval = IS_ERR((void *)error) ?
PTR_ERR((void*)error) : -EINVAL;
goto out_free_dentry;
}
if (!load_addr_set) {
load_addr_set = 1;
load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);
if (loc->elf_ex.e_type == ET_DYN) {
load_bias += error -
ELF_PAGESTART(load_bias + vaddr);
load_addr += load_bias;
reloc_func_desc = load_bias;
}
}
k = elf_ppnt->p_vaddr;
if (k < start_code)
start_code = k;
if (start_data < k)
start_data = k;
/*
* Check to see if the section's size will overflow the
* allowed task size. Note that p_filesz must always be
* <= p_memsz so it is only necessary to check p_memsz.
*/
if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||
elf_ppnt->p_memsz > TASK_SIZE ||
TASK_SIZE - elf_ppnt->p_memsz < k) {
/* set_brk can never work. Avoid overflows. */
retval = -EINVAL;
goto out_free_dentry;
}
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if (k > elf_bss)
elf_bss = k;
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k;
if (end_data < k)
end_data = k;
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
if (k > elf_brk) {
bss_prot = elf_prot;
elf_brk = k;
}
}
/* 更新读入内存中相关信息的记录 */
loc->elf_ex.e_entry += load_bias;
elf_bss += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;
/* Calling set_brk effectively mmaps the pages that we need
* for the bss and break sections. We must do this before
* mapping in the interpreter, to make sure it doesn't wind
* up getting placed where the bss needs to go.
*/
/* 使用set_brk调整bss段的大小 */
retval = set_brk(elf_bss, elf_brk, bss_prot);
if (retval)
goto out_free_dentry;
if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {
retval = -EFAULT; /* Nobody gets to see this, but.. */
goto out_free_dentry;
}
/*
6 填写程序的入口地址
这段程序的逻辑非常简单:
如果需要装入解释器,就通过load_elf_interp装入其映像,
并把将来进入用户空间的入口地址设置成load_elf_interp()的返回值,
即解释器映像的入口地址。
而若不装入解释器,那么这个入口地址就是目标映像本身的入口地址。
*/
if (interpreter) {
/* 存在动态链接器
内核把控制权传递给动态链接器。
动态链接器检查程序对共享库的依赖性,
并在需要时对其进行加载,由load_elf_interp完成 */
unsigned long interp_map_addr = 0;
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
if (!IS_ERR((void *)elf_entry)) {
/*
* load_elf_interp() returns relocation
* adjustment
*/
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;
}
if (BAD_ADDR(elf_entry)) {
retval = IS_ERR((void *)elf_entry) ?
(int)elf_entry : -EINVAL;
goto out_free_dentry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
} else {
elf_entry = loc->elf_ex.e_entry;
if (BAD_ADDR(elf_entry)) {
retval = -EINVAL;
goto out_free_dentry;
}
}
kfree(interp_elf_phdata);
kfree(elf_phdata);
set_binfmt(&elf_format);
#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES
retval = arch_setup_additional_pages(bprm, !!interpreter);
if (retval < 0)
goto out;
#endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */
/* 7 create_elf_tables填写目标文件的参数环境变量等必要信息
在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,
这些信息包括常规的argc、envc等等,还有一些"辅助向量(Auxiliary Vector)"。
这些信息需要复制到用户空间,使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆栈上。
这里的create_elf_tables()就起着这个作用。
*/
retval = create_elf_tables(bprm, &loc->elf_ex,
load_addr, interp_load_addr);
if (retval < 0)
goto out;
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
/*
* For architectures with ELF randomization, when executing
* a loader directly (i.e. no interpreter listed in ELF
* headers), move the brk area out of the mmap region
* (since it grows up, and may collide early with the stack
* growing down), and into the unused ELF_ET_DYN_BASE region.
*/
if (IS_ENABLED(CONFIG_ARCH_HAS_ELF_RANDOMIZE) &&
loc->elf_ex.e_type == ET_DYN && !interpreter)
current->mm->brk = current->mm->start_brk =
ELF_ET_DYN_BASE;
current->mm->brk = current->mm->start_brk =
arch_randomize_brk(current->mm);
#ifdef compat_brk_randomized
current->brk_randomized = 1;
#endif
}
if (current->personality & MMAP_PAGE_ZERO) {
/* Why this, you ask??? Well SVr4 maps page 0 as read-only,
and some applications "depend" upon this behavior.
Since we do not have the power to recompile these, we
emulate the SVr4 behavior. Sigh. */
error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, 0);
}
/* 读取寄存器数据 */
regs = current_pt_regs();
#ifdef ELF_PLAT_INIT
/*
* The ABI may specify that certain registers be set up in special
* ways (on i386 %edx is the address of a DT_FINI function, for
* example. In addition, it may also specify (eg, PowerPC64 ELF)
* that the e_entry field is the address of the function descriptor
* for the startup routine, rather than the address of the startup
* routine itself. This macro performs whatever initialization to
* the regs structure is required as well as any relocations to the
* function descriptor entries when executing dynamically links apps.
*/
ELF_PLAT_INIT(regs, reloc_func_desc);
#endif
finalize_exec(bprm);
/* 8 最后,start_thread()这个宏操作会将eip和esp改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口。
如果存在解释器映像,那么这就是解释器映像的程序入口,否则就是目标映像的程序入口。
那么什么情况下有解释器映像存在,什么情况下没有呢?
如果目标映像与各种库的链接是静态链接,因而无需依靠共享库、即动态链接库,那就不需要解释器映像;
否则就一定要有解释器映像存在。
对于一个目标程序, gcc在编译时,除非显示的使用static标签,否则所有程序的链接都是动态链接的,也就是说需要解释器。
由此可见,我们的程序在被内核加载到内存,内核跳到用户空间后并不是执行我们程序的,
而是先把控制权交到用户空间的解释器,由解释器加载运行用户程序所需要的动态库(比如libc等等),
然后控制权才会转移到用户程序。
*/
/* 开始执行程序,这时已经是子进程了 */
start_thread(regs, elf_entry, bprm->p);
retval = 0;
out:
kfree(loc);
out_ret:
return retval;
/* error cleanup */
out_free_dentry:
kfree(interp_elf_phdata);
allow_write_access(interpreter);
if (interpreter)
fput(interpreter);
out_free_ph:
kfree(elf_phdata);
goto out;
}
① load_elf_binary->flush_old_exec: 主要用来进行新进程地址空间的替换,并删除同线程组中的其他线程
int flush_old_exec(struct linux_binprm * bprm)
{
de_thread(current);
set_mm_exe_file(bprm->mm, bprm->file);
exec_mmap(bprm->mm);
bprm->mm = NULL;
set_fs(USER_DS);
current->flags &= ~(PF_RANDOMIZE | PF_FORKNOEXEC | PF_KTHREAD |
PF_NOFREEZE | PF_NO_SETAFFINITY);
flush_thread();
current->personality &= ~bprm->per_clear;
return 0;
}
因为即将要替换新进程的地址空间,所以首先通过de_thread函数用来删除同线程组中的其他线程。
set_mm_exe_file函数设置新进程的路径,即mm_struct中的exe_file成员变量。
通过exec_mmap函数将新进程的地址空间设置为bprm中创建并设置好的地址空间。
flush_thread函数主要用来初始化thread_struct中的TLS元数据信息。
最后设置进程的标志位flags和personality,personality用来兼容linux的旧版本或者BSD等其他版本。
② load_elf_binary->setup_new_exec: 对新进程的新进程的mm_struct结构进行设置
void setup_new_exec(struct linux_binprm * bprm)
{
arch_pick_mmap_layout(current->mm);
current->sas_ss_sp = current->sas_ss_size = 0;
if (uid_eq(current_euid(), current_uid()) && gid_eq(current_egid(), current_gid()))
set_dumpable(current->mm, SUID_DUMP_USER);
else
set_dumpable(current->mm, suid_dumpable);
perf_event_exec();
__set_task_comm(current, kbasename(bprm->filename), true);
current->mm->task_size = TASK_SIZE;
if (!uid_eq(bprm->cred->uid, current_euid()) ||
!gid_eq(bprm->cred->gid, current_egid())) {
current->pdeath_signal = 0;
} else {
would_dump(bprm, bprm->file);
if (bprm->interp_flags & BINPRM_FLAGS_ENFORCE_NONDUMP)
set_dumpable(current->mm, suid_dumpable);
}
current->self_exec_id++;
flush_signal_handlers(current, 0);
do_close_on_exec(current->files);
}
arch_pick_mmap_layout函数对设置了mmap的起始地址和分配函数。
然后更新mm的标志位,通过kbasename函数根据文件路径bprm->filename获得最后的文件名,再调用__set_task_comm函数设置进程的文件路径,最终设置到task_struct的comm变量中。
flush_signal_handlers用于清空信号的处理函数。最后调用do_close_on_exec关闭对应的文件。
③ load_elf_binary->setup_arg_pages;
int setup_arg_pages(struct linux_binprm *bprm,
unsigned long stack_top,
int executable_stack)
{
unsigned long ret;
unsigned long stack_shift;
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma = bprm->vma;
struct vm_area_struct *prev = NULL;
unsigned long vm_flags;
unsigned long stack_base;
unsigned long stack_size;
unsigned long stack_expand;
unsigned long rlim_stack;
stack_top = arch_align_stack(stack_top);
stack_top = PAGE_ALIGN(stack_top);
stack_shift = vma->vm_end - stack_top;
bprm->p -= stack_shift;
mm->arg_start = bprm->p;
bprm->exec -= stack_shift;
...
if (stack_shift) {
shift_arg_pages(vma, stack_shift);
}
stack_expand = 131072UL;
stack_size = vma->vm_end - vma->vm_start;
rlim_stack = rlimit(RLIMIT_STACK) & PAGE_MASK;
if (stack_size + stack_expand > rlim_stack)
stack_base = vma->vm_end - rlim_stack;
else
stack_base = vma->vm_start - stack_expand;
current->mm->start_stack = bprm->p;
expand_stack(vma, stack_base);
}
传入的参数stack_top添加了随机因子,首先对该stack_top进行页对齐,然后计算位移stack_shift,再将该位移添加到栈的指针bprm->p也即当前参数的存放地址mm->arg_start。省略的部分是对标志位的修改,再往下既然修改了栈的指针,就要通过shift_arg_pages函数修改堆栈对应的虚拟内存了。最后需要通过expand_stack函数拓展栈的大小,默认为stack_expand即4个页面。
④ load_elf_binary->elf_map:
static unsigned long elf_map(struct file *filep, unsigned long addr,
struct elf_phdr *eppnt, int prot, int type,
unsigned long total_size)
{
unsigned long map_addr;
unsigned long size = eppnt->p_filesz + ELF_PAGEOFFSET(eppnt->p_vaddr);
unsigned long off = eppnt->p_offset - ELF_PAGEOFFSET(eppnt->p_vaddr);
addr = ELF_PAGESTART(addr);
size = ELF_PAGEALIGN(size);
if (!size)
return addr;
if (total_size) {
total_size = ELF_PAGEALIGN(total_size);
map_addr = vm_mmap(filep, addr, total_size, prot, type, off);
if (!BAD_ADDR(map_addr))
vm_munmap(map_addr+size, total_size-size);
} else
map_addr = vm_mmap(filep, addr, size, prot, type, off);
return(map_addr);
}
传入的参数filep是文件指针,addr是即将映射的内存中的虚拟地址,size是文件映像的大小,off是映像在文件中的偏移。elf_map函数主要通过vm_mmap为文件申请虚拟空间并进行相应的映射,然后返回虚拟空间的起始地址map_addr。
⑤ load_elf_binary->start_thread
#define start_thread(regs,pc,sp) \ ({ \ unsigned long r7, r8, r9; \ \ if (IS_ENABLED(CONFIG_BINFMT_ELF_FDPIC)) { \ r7 = regs->ARM_r7; \ r8 = regs->ARM_r8; \ r9 = regs->ARM_r9; \ } \ memset(regs->uregs, 0, sizeof(regs->uregs)); \ if (IS_ENABLED(CONFIG_BINFMT_ELF_FDPIC) && \ current->personality & FDPIC_FUNCPTRS) { \ regs->ARM_r7 = r7; \ regs->ARM_r8 = r8; \ regs->ARM_r9 = r9; \ regs->ARM_r10 = current->mm->start_data; \ } else if (!IS_ENABLED(CONFIG_MMU)) \ regs->ARM_r10 = current->mm->start_data; \ if (current->personality & ADDR_LIMIT_32BIT) \ regs->ARM_cpsr = USR_MODE; \ else \ regs->ARM_cpsr = USR26_MODE; \ if (elf_hwcap & HWCAP_THUMB && pc & 1) \ regs->ARM_cpsr |= PSR_T_BIT; \ regs->ARM_cpsr |= PSR_ENDSTATE; \ regs->ARM_pc = pc & ~1; /* pc */ \ regs->ARM_sp = sp; /* sp */ \ })
传入的参数regs为保存的寄存器,new_ip为解释器或者应用程序的起始代码地址,new_sp为用户空间的堆栈指针。设置完这些变量后,最后通过force_iret强制返回,跳到new_ip指向的地址处开始执行。对于glibc而言,最终就会跳转到_start函数中