码迷,mamicode.com
首页 > 其他好文 > 详细

5.execve()到底干了啥?

时间:2016-05-04 13:32:53      阅读:292      评论:0      收藏:0      [点我收藏+]

标签:

       

导语

很多童鞋有分析阅读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上下文环境

5.execve()到底干了啥?

标签:

原文地址:http://blog.csdn.net/chengonghao/article/details/51313567

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!