zoukankan      html  css  js  c++  java
  • C程序运行的背后(2)

    话说上回说到,C程序运行之前,必须要加载到其进程地址空间中。今儿咱就扯扯这个加载到底是怎么加载的。 一图胜前言,这个图简单说明了可执行文件加载过程的逻辑流,在此只做粗粒度概要说明。需要准确描述的,请出门左转,看源码去吧。

    1.  程序总是运行在进程上下文(context)中的,当输入./memlayout时,shell会创建一个子进程。除每个进程独有的专属信息外,子进程会继承父进程的大部分资源,如环境变量、进程空间映像等。也就是说,如果不重置子进程的内容,子进程会运行与父进程一样的程序。为了让子进程可以运行别的程序,就要通过execve这个系统调用来指定。

    int   execve( char *pathname, char *argv[], char *envp[] )
    int   do_execve( char *pathname, char *argv[], char *envp[] ,struct pt_regs *regs )
    
    // pathname -> 可执行文件名指针
    // argv   -> 参数指针
    // envp   -> 环境变量指针
    // pt_regs  -> 用来保存切换到内核前用户空间的寄存器值

    就像春风秋雨一样,原本一切都是那么自然,自然到你忍不住向女神表白,然后女神跟你说“你是个好人”。当execvedo_execve说我要你时,却凭空多出了一个变量struct pt_regs,这绝不不可以说不任性!这是因为,do_execve中的参数命令行参数指针、环境变量指针,会被内核用来设置子进程的用户栈。而根据系统调用约定(calling conventions),Linux和Unix在系统调用时有一个不同,那就是Linux是用寄存器来传递系统调用参数的,而Unix是通过栈。所以在切换到内核时,就有必要保存传递到寄存器中的参数。而pt_regs这个结构体就是用来保存CPU的寄存器状态的。原来还是那么自然,只是女神已成路人。

    // include/asm-i386/ptrace.h
     struct pt_regs {     
        long ebx;    // pathname
        long ecx;     // argv
        long edx;     // envp
        long esi;     
        long edi; 
        long eax;  
        long eip;
        ……
    }

    2.  那么do_execve要大发神威了吧?切莫急先。物理学告诉我们,力都是有一个作用对象的,就好比我们追的都是女神。而do_execve的操作对象是一个叫struct linux_binprm的结构体,它用来保存要执行的文件的相关信息。do_execve会调用load_binprm,将需要的可执行文件信息都加载到linux_binprm中,包括可执行文件的ELF头信息、路径名、参数字符串、环境变量字符串等等。(注意:load_binprm并不是一个真正意义上的函数,为了方便理解,用它来概括表示由do_execve完成的填充任务。)

    struct linux_binprm {     
      char buf[BINPRM_BUF_SIZE]; //保存可执行文件的头128字节
      struct page *page[MAX_ARG_PAGES]; // 保存参数、环境变量   
      struct mm_struct *mm;     
      unsigned long p;    //当前内存页最高地址
      int sh_bang;     
      struct file * file;     //要执行的文件
      int e_uid, e_gid;    //要执行的进程的有效用户ID和有效组ID     
      kernel_cap_t cap_inheritable, cap_permitted, cap_effective;     
      void *security;     
      int argc, envc;     //命令行参数和环境变量数目
      char * filename;    //要执行的文件的名称
      char * interp;      //要执行的文件的真实名称,通常和filename相同     
      unsigned interp_flags;     
      unsigned interp_data;     
      unsigned long loader, exec; 
    }

    接下来,do_execve调用search_binary_handler()来查找可执行文件内容的处理程序。对于ELF格式的文件,则调用相应的load_elf_binary(),如果是a.out格式,则调用load_aout_binary()。在根据可执行文件中的section信息并将它们加载到进程空间前,load_elf_binary会首先将进程空间清空,然后将可执行文件映像加载到进程空间中。另外,load_elf_binary还会设置好用户栈:

    调用setup_arg_pages,将linux_binprm.page中的参数、环境变量等字符串映射到用户栈中;

    调用create_elf_tables,将argc、argv、envp以及一些的“辅助向量(auxiliary vector)”压入到用户栈中。

    辅助向量,是内核向用户空间的应用程序传递信息的机制之一,主要供动态链接器(ld-linux.so)使用。

    之前的图是从《深入理解计算机系统》拷过来的,并不详细,于是重新画了一个:

     argc下面的地址才是栈真正开始的地方。

    3.  终于草原已经准备好了,可以策马奔腾啦。一切又回到load_elf_binary()中,不过接下来就是见证神奇的时候啦,因为load_elf_binary要调用start_thread啦。不要小瞧这个调用,它不亚于数学老师跟我们说”我要变形了“的效果。

    #define start_thread(regs, new_eip, new_esp) do {          
           __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));     
           set_fs(USER_DS);                             
           regs->xds = __USER_DS;                   
           regs->xes = __USER_DS;                   
           regs->xss = __USER_DS;                   
           regs->xcs = __USER_CS;                   
           regs->eip = new_eip;                     
           regs->esp = new_esp;                     
    } while (0)

    start_thread的实际调用是这样的start_thread(regs,elf_entry, bprm->p),其中__USER_*是前面提到的进程切换到内核前的寄存器值;elf_entry就是可执行文件的入口点,也就是C启动代码的入口位置,赋给eip;bprm->p就是用户栈的栈顶位置,赋给esp。接下来就会跳转到eip指向的C启动代码起始位置开始运行。

     

    至于从C启动代码到main的距离,咱以后再表。

     

    参考:

    1 do_execve的具体内容:http://wenku.baidu.com/view/e97820ee4afe04a1b071de32.html

    2 ELF文件加载详细流程:http://www.longene.org/techdoc/0328130001224576708.html

    3 辅助向量:http://www.tuicool.com/articles/MNRJVj

  • 相关阅读:
    递归和回溯的区别
    N皇后问题
    c输出格式
    python sublime run快捷键设置
    八皇后问题
    动态规划---从左上角到右下角的价值最大的路径
    莫队算法详解和c实现
    Shell 常用命令总结
    WeakHashMap和HashMap的区别
    【 Jquery插件】引导用户如何操作网站功能的向导
  • 原文地址:https://www.cnblogs.com/chenwu128/p/4194638.html
Copyright © 2011-2022 走看看