很多童鞋有分析阅读Linux源代码的强烈愿望,可是Linux内核代码量庞大,大部分人不知道如何下手,以下是我分析Linux源代码的一些经验,仅供参考,有不实之处请大神指正!
1.要想阅读内核首先要进入内核,其中用户态程序进入内核态的主要方式是int 0x80中断,搞懂这条指令的执行过程是我们学习内核的第一步;
2.Linux中最重要的结构体莫过于task_struct,没错,这就是大名鼎鼎的进程描述符(PCB,process control block),task_struct是Linux这个大轮子能转起来的关键,对task_struct的掌握程度基本上反应了你对内核的掌握程度,task_struct中包含了内存管理,IO管理,文件系统等操作系统的基本模块。task_struct位于linux-3.18.6/include/linux/sched.h中,约400行。
3.读万卷书不如行万里路,光是读内核代码是不够的,有精力的童鞋可以试着打断点看看内核中一个函数是怎么执行的,而Linux下的调试神器就是gdb,在Linux下开发过应用程序的童鞋肯定或多或少用过gdb,经常使用图形化IDE调试工具的童鞋初涉gdb可能会有些不适应,我也只是会常用的几个命令而已。具体怎么用gdb调试Linux内核,网上这方面的教程不少,请自行Google;
4.开gdb调试时我认为有一个很重要的方法就是搞懂函数栈,Linux内核中函数不停的调用和跳转,很容易让你迷失其中,调试时清楚知晓函数调用堆栈这点很重要~
5.打蛇打七寸,擒贼先擒王,Linux代码中有不少错误处理之类的分支,调试时千万不要陷入其中,陷进去往往不能自拔。我们要抓住主要矛盾,忽略次要矛盾。错误处理一般是Linux hacker关注的重点,hacker期望从错误处理中找到漏洞以便对内核发起攻击,而我们作为Linux 内核的reader看看函数实现就足矣;
execve系统调用的作用是执行一个新的程序,可执行程序的文件格式有许多种,这里我们就分析的对象是ELF文件格式。
execve系统调用进入内核后调用的是do_execve()这个函数,do_execve()被调用的地方出现在linux-3.18.6\fs\exec.c文件中。我们一起来看一下调用do_execve()它的代码。
SYSCALL_DEFINE3(execve, constchar __user *, filename, constchar __user *const __user *, argv, constchar __user *const __user *, envp) { returndo_execve(getname(filename), argv, envp); }
getname(filename)获得可执行文件的文件名,argv和envp是shell命令行传递过来的命令行参数和shell上下文环境变量。
我们深入到do_execve()一探究竟。do_execve()位于linux-3.18.6\fs\exec.c文件中。进入do_execve()我们的函数栈样子是:execve-> do_execve()
intdo_execve(struct filename *filename, const char __user *const __user*__argv, const char __user *const __user*__envp) { struct user_arg_ptr argv = {.ptr.native = __argv }; struct user_arg_ptr envp = {.ptr.native = __envp }; return do_execve_common(filename, argv,envp); }
const char__user *const __user *表示用户态指针,这里我们也可以知道__argv和__envp是由用户态传递进来的执行条件。
structuser_arg_ptr argv = { .ptr.native = __argv }; // 把命令行参数转换为相应的结构体
structuser_arg_ptr envp = { .ptr.native = __envp }; // 把shell上下文环境转换为结构体
以上代码可以看出do_execve()的主要作用是封装好执行条件(argv和envp),接着继续调用do_execve_common(),do_execve_common()位于linux-3.18.6\fs\exec.c文件中。进入do_execve_common()后的函数栈样子是:execve -> do_execve() –> do_execve_common()。
static intdo_execve_common(struct filename *filename, structuser_arg_ptr argv, struct user_arg_ptrenvp) { struct linux_binprm *bprm; struct file *file; struct files_struct *displaced; int retval; if (IS_ERR(filename)) // 判断文件名是否合法 return PTR_ERR(filename); …………………………………..// 主要是错误检查,不用管 file = do_open_exec(filename); …………………………………….. bprm->file = file; bprm->filename = bprm->interp =filename->name; …………………………………………… retval= copy_strings(bprm->envc, envp, bprm); //把传入的shell上下文拷贝到bprm中 if (retval < 0) goto out; retval =copy_strings(bprm->argc, argv, bprm); // 把传入的命令行参数拷贝到bprm中 if (retval < 0) goto out; retval = exec_binprm(bprm); if (retval < 0) goto out; ………………………….. out_ret: putname(filename); return retval; }
do_execve_common()稍微复杂一点了,do_open_exec(filename)打开要加载的可执行文件,file结构体包含了打开的可执行文件信息。do_open_exec(filename)之后就是对bprm结构体的初始化了,每做一项初始化都要检查成功与否,初始化错误就要及时处理。要初始化的东西很多,不一一列出来,说几个重要的。
retval =copy_strings(bprm->argc, argv, bprm); // 把传入的命令行参数拷贝到bprm中
retval =copy_strings(bprm->envc, envp, bprm); //把传入的shell上下文拷贝到bprm中
retval = exec_binprm(bprm);// 对可执行文件的处理,比较关键的一句
我们跳入到exec_binprm(bprm)中,看看内核是怎么处理可执行文件的,exec_binprm()同样位于linux-3.18.6\fs\exec.c文件中,进入exec_binprm()我们的函数栈变为:execve -> do_execve() –> do_execve_common() -> exec_binprm()。
static intexec_binprm(struct linux_binprm *bprm) { pid_t old_pid, old_vpid; int ret; /* Need to fetch pid before load_binarychanges it */ old_pid = current->pid; rcu_read_lock(); old_vpid = task_pid_nr_ns(current,task_active_pid_ns(current->parent)); rcu_read_unlock(); ret = search_binary_handler(bprm); if (ret >= 0) { audit_bprm(bprm); trace_sched_process_exec(current,old_pid, bprm); ptrace_event(PTRACE_EVENT_EXEC,old_vpid); proc_exec_connector(current); } return ret; }
exec_binprm()中关键的代码是ret =search_binary_handler(bprm);寻找可执行文件的处理函数(可执行文件的类型不止一种),从search_binary_handler的名字不难发现,我们的可执行文件都是二进制文件(这不废话吗~)。
我们去看看search_binary_handler()发生了什么,search_binary_handler()位于linux-3.18.6\fs\exec.c文件中,跳入search_binary_handler()后我们函数栈的样子为:execve-> do_execve() –> do_execve_common() -> exec_binprm() -> search_binary_handler()。
intsearch_binary_handler(struct linux_binprm *bprm) { bool need_retry =IS_ENABLED(CONFIG_MODULES); struct linux_binfmt *fmt; int retval; …………………………………….. list_for_each_entry(fmt, &formats,lh) { if(!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); bprm->recursion_depth++; retval =fmt->load_binary(bprm); read_lock(&binfmt_lock); put_binfmt(fmt); bprm->recursion_depth--; if (retval < 0 &&!bprm->mm) { /* we got toflush_old_exec() and failed after it */ read_unlock(&binfmt_lock); force_sigsegv(SIGSEGV,current); return retval; } if (retval != -ENOEXEC ||!bprm->file) { read_unlock(&binfmt_lock); return retval; } } …………………………… return retval; }
关键代码为list_for_each_entry这个循环,在循环体内部寻找可执行文件的解析函数,如果找到了就加载。
retval =fmt->load_binary(bprm); // 加载可执行文件的处理函数
load_binary()是一个函数指针,以ELF格式的可执行文件为例,load_binary()实际上调用的是load_elf_binary(),load_elf_binary这个函数指针被包含在一个名为elf_format的结构体中,而elf_format在linux-3.18.6\fs\binfmt_elf.c文件中定义。
到linux-3.18.6\fs\binfmt_elf.c中找到load_elf_binary:
static structlinux_binfmt elf_format = { .module =THIS_MODULE, .load_binary = load_elf_binary, //函数指针 .load_shlib = load_elf_library, .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, };
elf_format结构体由init_elf_binfmt(void)函数注册到文件解析链表中。init_elf_binfmt(void)函数位于linux-3.18.6\fs\binfmt_elf.c文件中,代码为:
static int__init init_elf_binfmt(void) { register_binfmt(&elf_format); return 0; }
search_binary_handler()函数的工作就是用list_for_each_entry遍历文件解析链表,找到文件的解析函数。
接下来我们可以全文检索一下Linux下的register_binfmt()函数,打开网址:http://codelab.shiyanlou.com/search?q=register_binfmt&project=linux-3.18.6
可以看到register_binfmt()函数被调用9次,注册了9种不同的文件解析函数。
前面说了文件解析函数的注册,似乎有些跑题了,赶紧拉回来,回到search_binary_handler()函数,在search_binary_handler()的list_for_each_entry循环中找到ELF文件的解析函数load_elf_binary(),我们进入load_elf_binary()看看内核是怎么解析ELF文件的。load_elf_binary()位于/linux-3.18.6/fs/binfmt_elf.c文件中,进入load_elf_binary()后函数栈的样子为:execve-> do_execve() –> do_execve_common() -> exec_binprm() -> search_binary_handler()-> load_elf_binary()。
static intload_elf_binary(struct linux_binprm *bprm) { ……………………………….. if (elf_interpreter) { ………………………………. // 动态链接的处理 } else { // 静态链接的处理 elf_entry =loc->elf_ex.e_entry; if (BAD_ADDR(elf_entry)) { retval = -EINVAL; gotoout_free_dentry; } } ………………………………….. 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; …………………………………… start_thread(regs, elf_entry,bprm->p); retval = 0; …………………………………… }
load_elf_binary()的作用不仅是解析ELF文件,更重要的是把ELF文件映射到进程空间中去。
current->mm->end_code = end_code; current->mm->start_code =start_code; current->mm->start_data =start_data; current->mm->end_data = end_data;
以上四句话把当前进程的代码段、数据段起始和终止位置改为ELF文件中指明的数据段和代码段位置,execve系统调用返回用户态后进程就拥有了新的代码段、数据段。
if(elf_interpreter),如果需要依赖动态库,要做动态链接,需要执行if中的代码,这里我们不考虑动态链接的执行过程,只考虑静态链接。如果是静态链接的话执行else中的代码。
一般来说ELF文件中的ELFHeader中的Entry point address字段(第四讲)指明了程序入口地址(main函数的地址),这个地址一般是0x8048000(0x8048000以上的是内核段内存)。该入口地址被解析后存放在elf_ex.e_entry中,elf_entry = loc->elf_ex.e_entry;就是把ELF文件中的入口地址赋值给elf_entry变量。所以静态链接程序的起始位置一般是0x8048000。
我们接着往下读,来到start_thread(regs,elf_entry, bprm->p);,这是关键的一个函数,位于linux-3.18.6\arch\x86\kernel\ process_32.c文件中,我们跳进去看看。进入start_thread()后函数张的样子为:execve -> do_execve() –> do_execve_common() -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。
start_thread(structpt_regs *regs, unsigned long new_ip, unsigned long new_sp) { set_user_gs(regs, 0); regs->fs = 0; regs->ds = __USER_DS; regs->es = __USER_DS; regs->ss = __USER_DS; regs->cs = __USER_CS; regs->ip = new_ip; regs->sp = new_sp; regs->flags = X86_EFLAGS_IF; /* * force it to the iret return path by makingit look as if there was * some work pending. */ set_thread_flag(TIF_NOTIFY_RESUME); }
pt_regs结构体在在linux-3.18.6\arch\x86\include\asm\ptrace.h中定义:
struct pt_regs { unsignedlong r15; unsignedlong r14; unsignedlong r13; unsignedlong r12; unsignedlong bp; unsignedlong bx; /* arguments:non interrupts/non tracing syscallsonly save up to here*/ unsignedlong r11; unsignedlong r10; unsignedlong r9; unsignedlong r8; unsignedlong ax; unsignedlong cx; unsignedlong dx; unsignedlong si; unsignedlong di; unsignedlong orig_ax; /* end ofarguments */ /* cpu exceptionframe or undefined */ unsignedlong ip; unsignedlong cs; unsignedlong flags; unsignedlong sp; unsignedlong ss; /* top of stackpage */ };
进程执行execve系统调用,CPU往进程的内核堆栈压入了很多寄存器值。struct pt_regs表示进程内核堆栈的系统调用时SAVE_ALL宏(传送门:第一讲)压入内核栈的部分。
egs->ip = new_ip;
从start_thread()的实参可以得知new_ip的值是我们新加载的可执行文件的elf_entry的位置,也就是ELF文件中main函数的位置。egs->ip = new_ip;把ELF文件中定义的main函数起始地址赋值给eip寄存器,进程返回到用户态时的执行位置从原来的int 0x80的下一条指令变成了new_ip的位置。
regs->sp = new_sp;
修改内核堆栈的栈顶指针。
当系统调用返回后,CPU拿到新的ip指针和新的用户态堆栈,新的用户态堆栈中包含新程序的命令行参数和shell上下文环境,就可以放心的执行新程序啦~
总结execve系统调用的过程:
1. execve系统调用陷入内核,并传入命令行参数和shell上下文环境
2. execve陷入内核的第一个函数:do_execve,do_execve封装命令行参数和shell上下文
3. do_execve调用do_execve_common,do_execve_common打开ELF文件并把所有的信息一股脑的装入linux_binprm结构体
4. do_execve_common中调用search_binary_handler,寻找解析ELF文件的函数
5. search_binary_handler找到ELF文件解析函数load_elf_binary
6. load_elf_binary解析ELF文件,把ELF文件装入内存,修改进程的用户态堆栈(主要是把命令行参数和shell上下文加入到用户态堆栈),修改进程的数据段代码段
7. load_elf_binary调用start_thread修改进程内核堆栈(特别是内核堆栈的ip指针)
8. 进程从execve返回到用户态后ip指向ELF文件的main函数地址,用户态堆栈中包含了命令行参数和shell上下文环境
原文:http://blog.csdn.net/chengonghao/article/details/51313567