二、 加载--可执行文件放入内存
通过前一章可以知道一个程序是如何从我们编写的代码变成一个可以执行的文件的。但是此时它仍是放在磁盘上的一个文件,并不是我们通常理解的程序--在内存上运行的一段代码。
程序运行在内存上,所以首先我们需要了解虚拟内存的一些基本知识,然后我们以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] != '