通过gdb跟踪Linux内核装载和启动可执行程序过程
作者:吴乐 山东师范大学
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
实验目的:通过对一个简单的可执行程序用gdb进行代码的跟踪,剖析linux内核是如何动态和静态装载和启动程序的,进而总结linux内核可执行程序加载的过程。
一、实验过程
1、编写一个简单的Exec的创建进程的函数
2、打开gdb,并设置好如下断点
3、开始跟踪,找到第一个断点。
(主程序还未创建子进程)
4、继续在此断点处逐步跟踪
5、找到设置的第二个断点,并列出
6、跟踪到装载new_ip处,查看其地址
7、明显看到,此处加载的IP地址与程序入口地址相同
8、结束跟踪,观察其他断点方法类似。
二、可执行文件的加载和运行
1、execve()系统调用的入口是sys_execve().代码如下:
int sys_execve(struct pt_regs regs) { int error; char * filename; //将用户空间的第一个参数(也就是可执行文件的路径)复制到内核 filename = getname((char __user *) regs.ebx); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; error = do_execve(filename, (char __user * __user *) regs.ecx, (char __user * __user *) regs.edx, ®s); if (error == 0) { task_lock(current); current->ptrace &= ~PT_DTRACE; task_unlock(current); /* Make sure we don't return using sysenter.. */ set_thread_flag(TIF_IRET); } //释放内存 putname(filename); out: return error; }
由此可见进行系统调用时,把参数依次放在ebx,ecx,edx,esi,edi,ebp寄存器.
注意其中第一个参数为可执行文件路径,第二个参数为参数的个数,第三个参数为可执行文件对应的参数.
2、do_execve()是这个系统调用的主要部分,它的代码如下:
int do_execve(char * filename, char __user *__user *argv, char __user *__user *envp, struct pt_regs * regs) { //linux_binprm:保存可执行文件的一些参数 struct linux_binprm *bprm; struct file *file; unsigned long env_p; int retval; retval = -ENOMEM; bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); if (!bprm) goto out_ret; //在内核中打开这个可执行文件 file = open_exec(filename); retval = PTR_ERR(file); //如果打开失败 if (IS_ERR(file)) goto out_kfree; sched_exec(); bprm->file = file; bprm->filename = filename; bprm->interp = filename; //bprm初始化,主要是初始化bprm->mm retval = bprm_mm_init(bprm); if (retval) goto out_file; //计算参数个数 bprm->argc = count(argv, MAX_ARG_STRINGS); if ((retval = bprm->argc) goto out_mm; //环境变量个数 bprm->envc = count(envp, MAX_ARG_STRINGS); if ((retval = bprm->envc) goto out_mm; retval = security_bprm_alloc(bprm); if (retval) goto out; //把要加载文件的前128 读入bprm->buf retval = prepare_binprm(bprm); if (retval goto out; //copy第一个参数filename retval = copy_strings_kernel(1, &bprm->filename, bprm); if (retval goto out; //bprm->exec:参数的起始地址(从上往下方向) bprm->exec = bprm->p; //copy环境变量 retval = copy_strings(bprm->envc, envp, bprm); if (retval goto out; //环境变量存放的起始地址 env_p = bprm->p; //copy可执行文件所带参数 retval = copy_strings(bprm->argc, argv, bprm); if (retval goto out; //环境变量的长度 bprm->argv_len = env_p - bprm->p; //到链表中寻找合适的加载模块 retval = search_binary_handler(bprm,regs); if (retval >= 0) { /* execve success */ free_arg_pages(bprm); security_bprm_free(bprm); acct_update_integrals(current); kfree(bprm); return retval; } out: free_arg_pages(bprm); if (bprm->security) security_bprm_free(bprm); out_mm: if (bprm->mm) mmput (bprm->mm); out_file: if (bprm->file) { allow_write_access(bprm->file); fput(bprm->file); } out_kfree: kfree(bprm); out_ret: return retval; }
3、在加载可执文件的时候,需要遍历formats这个链表,search_binary_handler()实现了这一功能。代码如下:
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs) { int try,retval; struct linux_binfmt *fmt; #ifdef __alpha__ /* handle /sbin/loader.. */ { struct exec * eh = (struct exec *) bprm->buf; if (!bprm->loader && eh->fh.f_magic == 0x183 && (eh->fh.f_flags & 0x3000) == 0x3000) { struct file * file; unsigned long loader; allow_write_access(bprm->file); fput(bprm->file); bprm->file = NULL; loader = bprm->vma->vm_end - sizeof(void *); file = open_exec("/sbin/loader"); retval = PTR_ERR(file); if (IS_ERR(file)) return retval; /* Remember if the application is TASO. */ bprm->sh_bang = eh->ah.entry bprm->file = file; bprm->loader = loader; retval = prepare_binprm(bprm); if (retval return retval; /* should call search_binary_handler recursively here, but it does not matter */ } } #endif retval = security_bprm_check(bprm); if (retval) return retval; /* kernel module loader fixup */ /* so we don't try to load run modprobe in kernel space. */ set_fs(USER_DS); retval = audit_bprm(bprm); if (retval) return retval; retval = -ENOENT; //这里会循环两次.待模块加载之后再遍历一次 for (try=0; try read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { //加载函数 int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary; if (!fn) continue; if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); //运行加载函数,如果加载末成功,则继续遍历 retval = fn(bprm, regs); //加载成功了 if (retval >= 0) { put_binfmt(fmt); allow_write_access(bprm->file); if (bprm->file) fput(bprm->file); bprm->file = NULL; current->did_exec = 1; proc_exec_connector(current); return retval; } read_lock(&binfmt_lock); put_binfmt(fmt); if (retval != -ENOEXEC || bprm->mm == NULL) break; if (!bprm->file) { read_unlock(&binfmt_lock); return retval; } } read_unlock(&binfmt_lock); //所有模块加载这个可执行文件失败,则加载其它模块再试一次 if (retval != -ENOEXEC || bprm->mm == NULL) { break; //CONFIG_KMOD:动态加载模块标志 #ifdef CONFIG_KMOD }else{ #define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20 if (printable(bprm->buf[0]) && printable(bprm->buf[1]) && printable(bprm->buf[2]) && printable(bprm->buf[3])) break; /* -ENOEXEC */ request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2])); #endif } } return retval; }
4、唤醒父进程的过程以及栈空间的布局代码如下.
static int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs) { …… …… current->mm->start_stack = (unsigned long) create_aout_tables((char __user *) bprm->p, bprm); #ifdef __alpha__ regs->gp = ex.a_gpvalue; #endif start_thread(regs, ex.a_entry, current->mm->start_stack); …… } Creat_aout_tables()代码如下: static unsigned long __user *create_aout_tables(char __user *p, struct linux_binprm * bprm) { char __user * __user *argv; char __user * __user *envp; unsigned long __user *sp; //可执行文件的参数个数 int argc = bprm->argc; //环境变量的个数 int envc = bprm->envc; //sp初始化成p,也即bprm->p sp = (void __user *)((-(unsigned long)sizeof(char *)) & (unsigned long) p); #ifdef __sparc__ /* This imposes the proper stack alignment for a new process. */ sp = (void __user *) (((unsigned long) sp) & ~7); if ((envc+argc+3)&1) --sp; #endif #ifdef __alpha__ /* whee.. test-programs are so much fun. */ put_user(0, --sp); put_user(0, --sp); if (bprm->loader) { put_user(0, --sp); put_user(0x3eb, --sp); put_user(bprm->loader, --sp); put_user(0x3ea, --sp); } put_user(bprm->exec, --sp); put_user(0x3e9, --sp); #endif sp -= envc+1; envp = (char __user * __user *) sp; sp -= argc+1; argv = (char __user * __user *) sp; #if defined(__i386__) || defined(__mc68000__) || defined(__arm__) || defined(__arch_um__) put_user((unsigned long) envp,--sp); put_user((unsigned long) argv,--sp); #endif put_user(argc,--sp); current->mm->arg_start = (unsigned long) p; while (argc-->0) { char c; put_user(p,argv++); do { get_user(c,p++); } while (c); } put_user(NULL,argv); current->mm->arg_end = current->mm->env_start = (unsigned long) p; while (envc-->0) { char c; put_user(p,envp++); do { get_user(c,p++); } while (c); } put_user(NULL,envp); current->mm->env_end = (unsigned long) p; return sp; }
ip这里已经指向main函数入口地址了,此后的工作都由start_thread()函数完成。具体过程可参见我的另一片博客:
http://www.cnblogs.com/wule/p/4404504.html
三、总结linux内核可执行程序加载的过程
首先创建父进程,然后通过调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件。 主进程继续返回等待新进程执行结束,然后重新等待用户输入命令。execve()系统调用被定义在unistd.h,它的原型如下:
int execve(const char *filenarne, char *const argv[], char *const envp[]);
它的三个参数分别是被执行的程序文件名、执行参数和环境变最。Glibc对execvp()系统调用进行了包装,提供了execl(), execlp(), execle(), execv()和execvp()等5个不同形式的exec系列API,它们只是在调用的参数形式上有所区别,但最终都会调用到execve()这个系统中。
调用execve()系统调用之后,再调用内核的入口sys_execve()。 sys_execve()进行一些参数的检查复制之后,调用do_execve()。 因为可执行文件不止ELF一种,还有java程序和以“#!”开始的脚本程序等, 所以do_execve()会首先检查被执行文件,读取前128个字节,特别是开头4个字节的魔数,用以判断可执行文件的格式。 如果是解释型语言的脚本,前两个字节“#!"就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定程序解释器的路径。
当do_execve()读取了这128个字节的文件头部之后,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并且调用相应的装载处理过程。如ELF用load_elf_binary(),a.out用load_aout_binary(),脚本用load_script()。其中ELF装载过程的主要步骤是:
①检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(Segment)的数量。
②寻找动态链接的”.interp”段(该段保存可执行文件所需要的动态链接器的路径),设置动态链接器路径。
③根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
④初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址(结束代码地址)。
⑤将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_enEry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。
当ELF被load_elf_binary()装载完成后,函数返回至do_execve()在返回至sys_execve()。在load_elf_binary()中(第5步)系统调用的返回地址已经被改成ELF程序的入口地址了。 所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。