导语
很多童鞋有分析阅读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文件格式,关于ELF格式请看4讲(传送门:第4讲)。
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字段(ELF的格式见第4讲,传送门:第4讲)指明了程序入口地址(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上下文环境