关于fork&exec之进程的创建和可执行程序的加载过程

来源:互联网 发布:淘宝联盟怎么没有佣金 编辑:程序博客网 时间:2024/05/21 13:56

中科大SA***243-石润成

一、预备阶段

实验环境:Ubuntu10.04

分析内核版本:Linux-2.6.11

1.编写一个fork和exec程序

myfork.c

#include<stdio.h>#include<sys/types.h>#include<unistd.h>int main(){    pid_t pid;    pid = fork();    if(0 == pid)    {        execl("./son","son",NULL);    }    else        if(pid > 0)        {            execl("./parent","parent",NULL);        }        else        {            fprintf(stderr, "fork failure!\n");            return -1;        }    return 0;}
myparent.c

#include<stdio.h>int main(){    printf("I am parent process!\n");    return 0;}
myson.c

#include<stdio.h>int main(){    printf("I am son process!\n");    return 0;}
各自生成myfork、myparent、myson的ELF格式文件。

打印结果:

I am parent process!
I am son process!

二、分析fork系统调用在内核中的执行过程

1)关于系统调用fork()、vfork()、clone()

    系统调用__clone()的主要用途是创建一个线程,这个线程可以使内核线程,也可以是用户线程。创建用户空间线程是,可以给定线程用户空间堆栈的位置,还可以指定子进程运行的起点。同时还可以用__clone()创建进程,有选择地复制父进程的资源。而fork(),则是全面地复制。还有一个系统调用vfork(),其作用也是创建一个线程,但主要只是作为创建进程的中间步骤,目的在于提高创建时的效率,减少系统开销,其设计接口与fork相同。这几个系统调用的代码在arch/i386/kernel/process.c中。 

asmlinkage int sys_fork(struct pt_regs regs){return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);}asmlinkage int sys_clone(struct pt_regs regs){unsigned long clone_flags;unsigned long newsp;int __user *parent_tidptr, *child_tidptr;clone_flags = regs.ebx;newsp = regs.ecx;parent_tidptr = (int __user *)regs.edx;child_tidptr = (int __user *)regs.edi;if (!newsp)newsp = regs.esp;return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);}/* * This is trivial, and on the face of it looks like it * could equally well be done in user mode. * * Not so, for quite unobvious reasons - register pressure. * In user mode vfork() cannot have a stack frame, and if * done by calling the "clone()" system call directly, you * do not have enough call-clobbered registers to hold all * the information you need. */asmlinkage int sys_vfork(struct pt_regs regs){return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL);}
    可以看出,三个系统调用的实现都是通过do_fork来实现的,只是对do_fork()调用的参数不同。

2)下面分析fork()开始系统调用的

    当从用户态调用fork()函数进入系统调用时,CPU切换到内核态并开始执行一个内核函数,详情请见这里,这里笔者简单用个图来描述一下。

关键之处:

        fork调用调用0x80中断,寄存器eax中的值为_NR_fork这是fork产给int $0x80唯一的参数。可以看到_NR_fork的调用号是2,如下所示,然后根据系统调用表sys_call_table找到偏移地址找到对应的函数,fork系统调用的对应函数时不带参数的,所以对应的调用是syscall0。

       查看”include/asm-i386/unistd.h”可以看到各函数对应的系统调用号:

#ifndef _ASM_I386_UNISTD_H_#define _ASM_I386_UNISTD_H_/* * This file contains the system call numbers. */#define __NR_restart_syscall      0#define __NR_exit                 1#define __NR_fork                 2#define __NR_read                 3#define __NR_write                4#define __NR_open                 5#define __NR_close                6#define __NR_waitpid              7#define __NR_creat                8....#define __NR_add_key              286#define __NR_request_key          287#define __NR_keyctl               288#define NR_syscalls               289

#define _syscall0(type,name) \type name(void) \{ \long __res; \__asm__ volatile ("int $0x80" \   //调用系统中断0x80: "=a" (__res) \             //_res与eax关联,为输出操作数: "0" (__NR_##name)); \   // "0"等价于"a"即,_NR_##name保存到eax中作为传递参数__syscall_return(type,__res); \      }#define __syscall_return(type, res) \do { \if ((unsigned long)(res) >= (unsigned long)(-(128 + 1))) { \errno = -(res); \res = -1; \} \return (type) (res); \} while (0)

如果有参数的话会有

_syscall1(type,name,type1,arg1)

_syscall2(type,name,type1,arg1,type2,arg2)

_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) 

......等等。

    这7个宏是用来产生系统调用的函数名的,其中type表示系统调用的返回值类型,name表示该系统调用的名称,typeN、argN分别表示第N个参数的类型和名称,它们的数目和_syscall后面的数字一样大。

    可以看到调用forktypeintnameforkeax就会保存_NR_fork作为调用号传递参数调用对应的服务例程。

分析do_fork()函数:

long do_fork(unsigned long clone_flags,      unsigned long stack_start,      struct pt_regs *regs,      unsigned long stack_size,      int __user *parent_tidptr,      int __user *child_tidptr){struct task_struct *p;int trace = 0;long pid = alloc_pidmap(); //1.位图方式管理的进程ID,分配未使用的进程IDif (pid < 0)return -EAGAIN;/*   current->ptrace的作用描述请见http://blog.renren.com/share/248850507/4135433271    2.这里主要是为了检查当前进程即父进程的ptrace字段,从而确定父进程是否被跟踪,ptrace字段非0表示被跟踪,接着再检测子进程是否要被跟踪,如果trace为1,则对clone_flags中相应的CLONE_PTRACE标记为1。*/if (unlikely(current->ptrace)) {trace = fork_traceflag (clone_flags);if (trace)clone_flags |= CLONE_PTRACE;}/*    3.这是整个创建进程过程的核心,主要创建进程的描述符和进程执行时所需要的数据结构,然后返回创建好的进程描述符。      详见这里http://edsionte.com/techblog/archives/2141*/p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);/* * Do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. *//*copy_process函数执行成功后,会定义一个完成量vfork,关于完成量的作用是,当任务A触发某个事件时,发出信号通知任务B,B才能开始执行,否则B就一直等待。*/if (!IS_ERR(p)) {struct completion vfork;/*    如果clone_flags包含CLONE_VFORK标记,即表示调用vfork了,父进程休眠,同时将进程描述符中的vfork_done字段指向这个完成量,然后对这个完成量进行初始化。*/if (clone_flags & CLONE_VFORK) {p->vfork_done = &vfork;init_completion(&vfork);}/*    4.如果子进程被跟踪,或者设置了CLONE_STOPPED进程停止标志,则调用sigaddset为子进程增加挂起信号SIGSTOP。*/if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {/* * We'll start up with an immediate SIGSTOP. */sigaddset(&p->pending.signal, SIGSTOP);set_tsk_thread_flag(p, TIF_SIGPENDING);}/*    5.如果子进程没有设置CLONE_STOPPED标志,那么通过wake_up_new函数使得父子进程之一优先运行,否则子进程的状态设置为TASK_STOPPED*/if (!(clone_flags & CLONE_STOPPED))wake_up_new_task(p, clone_flags);elsep->state = TASK_STOPPED;/*    6.如果父进程被跟踪,那么子进程的pid号会赋给当前进程即父进程的描述符的pstrace_message,再通过ptrace_notify函数使得当前进程定制,并向父进程的父进程发送SIGCHLD信号*/if (unlikely (trace)) {current->ptrace_message = pid;ptrace_notify ((trace << 8) | SIGTRAP);}/*    7.如果设置了CLONE_VFORK标志,则通过wait操作将父进程阻塞,直到子进程调用exec函数或者退出*/if (clone_flags & CLONE_VFORK) {wait_for_completion(&vfork);if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);}}/*    8.copy_process()在执行的时候发生错误,则先释放已分配的pid;再根据PTR_ERR()的返回值得到错误代码,并保存到pid中*/else {free_pidmap(pid);pid = PTR_ERR(p);}return pid;}

三、分析exec系统调用在内核中的执行过程

        Linux提供了execl、execlp、execle、execv、execvp和execve等六个用以执行一个可执行文件的函数(统称为exec函数,其间的差异在于对命令行参数和环境变量参数的传递方式不同)。这些函数的第一个参数都是要被执行的程式的路径,第二个参数则向程式传递了命令行参数,第三个参数则向程式传递环境变量。以上函数的本质都是调用在arch/i386/kernel/process.c文件中实现的系统调用sys_execve来执行一个可执行文件。

      与fork系统调用一样,最后内核会调用do_execve系统函数

asmlinkage int sys_execve(struct pt_regs regs){int error;char * filename;/*   regs.ebx为应用程序调用相应库函数时的第一个参数,如本例的execl("./son","son",NULL);中的"./son",但由于该字符串在用户空间,所以需要将其从用户空间拷贝到内核空间,通过getname函数实现。getname()->do_getname()->strncpy_from_user()最终实现将参数字符串拷贝到内核空间。*/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;}

注:为什么要用getname()分配一个物理页面作为缓冲区呢?Linux内核源代码情景分析上有介绍,首先,这个字符串有可能非常长,比如本例的./son,这是一个相对路径,但最后会处理成一个绝对路径名,其次,进程系统空间堆栈的大小大约为7KB,不能滥用,不宜在getname()中定义一个局部的4KB的字符数组(注意,局部变量所占据的空间是在堆栈中分配的)。

上面得到了可执行文件的路径名副本filename,传入do_execve()进行调用。

/* * sys_execve() executes a new program. */int do_execve(char * filename,//需要执行的文件的绝对路径(用户空间)char __user *__user *argv, //传入系统调用的参数(用户空间)char __user *__user *envp,//环境变量参数(用户空间)struct pt_regs * regs)    //regs是系统调用时系统堆栈的信息{/*     内核为可执行程序的装入所定义的一个数据结构,以便将运行一个可执行文件时所需的信息组织到一起,见include/linux/binfmts.h */  struct linux_binprm *bprm;struct file *file;int retval;int i;retval = -ENOMEM;bprm = kmalloc(sizeof(*bprm), GFP_KERNEL);if (!bprm)goto out_ret;memset(bprm, 0, sizeof(*bprm));/*    打开指定的可执行文件filename,open_exec函数在exec.c中,file代表着读入可执行文件的上下文  */file = open_exec(filename);retval = PTR_ERR(file);if (IS_ERR(file))goto out_kfree;sched_exec();//bprm中有一个页面指针数组,数组的大小为允许的最大参数个数为MAX_AGE_PAGES  bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);bprm->file = file;bprm->filename = filename;bprm->interp = filename;bprm->mm = mm_alloc();retval = -ENOMEM;if (!bprm->mm)goto out_file;retval = init_new_context(current, bprm->mm);if (retval < 0)goto out_mm;/*    count是对字符串指针数组argv[]中参数的个数进行技术,而bprm->p/sizeof(void *)表示允许的最大值。    注意argv[]和envp[]在用户空间,所以技术操作并不那么简单*/bprm->argc = count(argv, bprm->p / sizeof(void *));if ((retval = bprm->argc) < 0)goto out_mm;bprm->envc = count(envp, bprm->p / sizeof(void *));if ((retval = bprm->envc) < 0)goto out_mm;retval = security_bprm_alloc(bprm);if (retval)goto out;/*    调用prepare_binprm()进一步做数据结构bprm的准备工作:从可执行文件中读入开头的128个字节到linux_binprm结构bprm中的缓冲区。    在读之前还要先检查当前进程是否有这个权利,以及该文件是否有可执行属性。    注:在无论是什么格式,在开头的128个字节中都包括了关于可执行文件属性的必要而从分的信息*/retval = prepare_binprm(bprm);if (retval < 0)goto out;/*    从系统空间拷贝执行参数,即上面的argv[]和envp[]拷到bprm中    注意:有些参数已存在于系统空间,可直接用copy_string_kernel()拷贝,如argv[0]表示可执行文件路径名 */retval = copy_strings_kernel(1, &bprm->filename, bprm);if (retval < 0)goto out;bprm->exec = bprm->p;/*    有些需要从用户空间拷贝,用copy_strings */retval = copy_strings(bprm->envc, envp, bprm);if (retval < 0)goto out;retval = copy_strings(bprm->argc, argv, bprm);if (retval < 0)goto out;/*    上面所有准备工作做完,接着就要装入运行程序了(exec.c)    search_binary_handler()主要作用是根据上面的执行程序的信息来找到一种可处理的该格式的方案,找到后就把目标文件装入并将其投入运行,再返    回一个正数或0。当CPU从系统调用返回时,该目标文件的执行就真正开始了,否则如果不能辨识,或者在处理的过程中出了错,就返回一个负数。这个才    是核心,我也搞不明白,太深了,建议看情景分析 */retval = search_binary_handler(bprm,regs);if (retval >= 0) {free_arg_pages(bprm);/* execve success */security_bprm_free(bprm);acct_update_integrals();update_mem_hiwater();kfree(bprm);return retval;}out:/* Something went wrong, return the inode and free the argument pages*/for (i = 0 ; i < MAX_ARG_PAGES ; i++) {struct page * page = bprm->page[i];if (page)__free_page(page);}if (bprm->security)security_bprm_free(bprm);out_mm:if (bprm->mm)mmdrop(bprm->mm);out_file:if (bprm->file) {allow_write_access(bprm->file);fput(bprm->file);}out_kfree:kfree(bprm);out_ret:return retval;}

四、ELF文件格式与进程地址空间的关系

1) 将准备阶段的程序静态编译

   gcc -static -o myfork myfork.c

2) 读取ELF格式的myfork文件头(ELF Header)

   readelf -S myfork

       

3) 读取段表(Section Header Table)

     readelf  -l myfork

    

    Segment描述了ELF文件该如何被操作系统映射到进程的虚拟空间。

    我们可以看到,fork执行文件由6个Segment,其中两个LOAD类型的Segment代表它是需要被映射的,其他的都是在装载时起辅助作用的。

    ELF可执行文件有一个专门的数据结构叫做程序头表,也是一个结构体数组,其结构体定义如下:

typedef struct{    Elf32_Word  p_type;    //Segment的类型    Elf32_Off   p_offset;  //Segment在文件中的偏移    Elf32_Addr  p_vaddr;   //Segment的第一个字节在进程虚拟地址空间的起始地址    Elf32_Addr  p_paddr;   //Segment的物理装载地址    Elf32_Word  p_filesz;  //Segment在ELF文件中所占的长度,有可能在ELF文件中不存在    Elf32_Word  p_memsz;   //Segment在进程虚拟空间所占的长度    Elf32_Word  p_flags;   //Segment的权限属性RWX    Elf32_Word  p_align;   //Segment的对齐属性}Elf32_Phdr;

    以下是本myfork可执行文件ELF与进程虚拟空间映射关系图

     

       由上图可以看到,myfork可执行程序被重新划成了三个部分,有一些段被归入可读可执行的它们被统一映射到VMA0;另外一部分段是可读可写的,它们被映射到了VMA1;还有一部分段在程序执行时没有用,所以不需要被映射。很明显,所有相同属性“Section”被归类到一个“Segment”,并且映射到同一个VMA中。(参考程序员的自我修养P163) 

五、动态链接库在ELF文件格式和进程地址空间中的表现形式

1)动态编译myfork文件

      gcc -o myfork myfork.c

2)读取段表

   readelf -S myfork

root@ubuntu:~/mylinux# readelf -S myforkThere are 30 section headers, starting at offset 0x1130:Section Headers:  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al  [ 0]                   NULL            00000000 000000 000000 00      0   0  0  [ 1] .interp           PROGBITS        08048134 000134 000013 00   A  0   0  1  [ 2] .note.ABI-tag     NOTE            08048148 000148 000020 00   A  0   0  4  [ 3] .note.gnu.build-i NOTE            08048168 000168 000024 00   A  0   0  4  [ 4] .hash             HASH            0804818c 00018c 000034 04   A  6   0  4  [ 5] .gnu.hash         GNU_HASH        080481c0 0001c0 000024 04   A  6   0  4  [ 6] .dynsym           DYNSYM          080481e4 0001e4 000080 10   A  7   1  4  [ 7] .dynstr           STRTAB          08048264 000264 00005e 00   A  0   0  1  [ 8] .gnu.version      VERSYM          080482c2 0002c2 000010 02   A  6   0  2  [ 9] .gnu.version_r    VERNEED         080482d4 0002d4 000020 00   A  7   1  4  [10] .rel.dyn          REL             080482f4 0002f4 000010 08   A  6   0  4  [11] .rel.plt          REL             08048304 000304 000028 08   A  6  13  4  [12] .init             PROGBITS        0804832c 00032c 000030 00  AX  0   0  4  [13] .plt              PROGBITS        0804835c 00035c 000060 04  AX  0   0  4  [14] .text             PROGBITS        080483c0 0003c0 0001ec 00  AX  0   0 16  [15] .fini             PROGBITS        080485ac 0005ac 00001c 00  AX  0   0  4  [16] .rodata           PROGBITS        080485c8 0005c8 000031 00   A  0   0  4  [17] .eh_frame         PROGBITS        080485fc 0005fc 000004 00   A  0   0  4  [18] .ctors            PROGBITS        08049f0c 000f0c 000008 00  WA  0   0  4  [19] .dtors            PROGBITS        08049f14 000f14 000008 00  WA  0   0  4  [20] .jcr              PROGBITS        08049f1c 000f1c 000004 00  WA  0   0  4  [21] .dynamic          DYNAMIC         08049f20 000f20 0000d0 08  WA  7   0  4  [22] .got              PROGBITS        08049ff0 000ff0 000004 04  WA  0   0  4  [23] .got.plt          PROGBITS        08049ff4 000ff4 000020 04  WA  0   0  4  [24] .data             PROGBITS        0804a014 001014 000008 00  WA  0   0  4  [25] .bss              NOBITS          0804a01c 00101c 00000c 00  WA  0   0  4  [26] .comment          PROGBITS        00000000 00101c 000025 01  MS  0   0  1  [27] .shstrtab         STRTAB          00000000 001041 0000ee 00      0   0  1  [28] .symtab           SYMTAB          00000000 0015e0 000440 10     29  45  4  [29] .strtab           STRTAB          00000000 001a20 000231 00      0   0  1Key to Flags:  W (write), A (alloc), X (execute), M (merge), S (strings)  I (info), L (link order), G (group), x (unknown)  O (extra OS processing required) o (OS specific), p (processor specific)

3)".dynamic"段

动态链接ELF中最重要的结构就是".dynamic"段,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。结构如下:

typedef struct{    Elf32_Sword d_tag;      //DT_SYMTAB、DT_STRTAB等等类型,请见(程序员的自我修养P205)    union    {        Elf32_Word d_val;        Elf32_Addr d_ptr;    } d_un;}Elf32_Dyn;
可以通过命令readelf -l myfork | grep interpreter找到动态链接器的路径,然后readelf -d 动态链接器路径查看".dynamic"段的内容。

注:动态符号表

   如上文中的静态链接中有个".symtab"保存了所有关于该目标文件的符号的定义和引用。动态链接的符号表示实际上它跟静态链接十分相似,但ELF文件有个专门的动态符号表段".dynsym",它只保存与动态链接相关的符号,".symtab"是包含".dynsym"中符号的。

   此外还有动态符号字符串表".dynstr"、符号哈希表".hash" 可通过命令readelf -sD 链接库路径查看。

4)动态链接重定位表

   从本程序的段表中可以看到两种段表,一个是".rel.dyn",相当于静态链接的".rel.text"表示代码段的重定位表。另一个是".rel.plt",相当于静态链接中的".rel.data"是数据段的重定位表。

   对数据引用的修正位置位于".got"以及数据段;对于函数引用的修正位于".got.plt"。

root@ubuntu:~/mylinux# readelf -r /lib/ld-linux.so.2Relocation section '.rel.dyn' at offset 0x714 contains 15 entries: Offset     Info    Type            Sym.Value  Sym. Name0001be48  00000008 R_386_RELATIVE   0001be54  00000008 R_386_RELATIVE   0001be58  00000008 R_386_RELATIVE   0001be5c  00000008 R_386_RELATIVE   0001be60  00000008 R_386_RELATIVE   0001be64  00000008 R_386_RELATIVE   0001be68  00000008 R_386_RELATIVE   0001be6c  00000008 R_386_RELATIVE   0001be70  00000008 R_386_RELATIVE   0001be74  00000008 R_386_RELATIVE   0001be78  00000008 R_386_RELATIVE   0001bfe4  00000008 R_386_RELATIVE   0001c83c  00000008 R_386_RELATIVE   0001bfe8  00000906 R_386_GLOB_DAT    0001c8e0   _r_debug0001bfec  00000606 R_386_GLOB_DAT    00014840   freeRelocation section '.rel.plt' at offset 0x78c contains 6 entries: Offset     Info    Type            Sym.Value  Sym. Name0001c000  00000b07 R_386_JUMP_SLOT   000148a0   __libc_memalign0001c004  00001607 R_386_JUMP_SLOT   000149b0   malloc0001c008  00000d07 R_386_JUMP_SLOT   00014a80   calloc0001c00c  00000707 R_386_JUMP_SLOT   000149e0   realloc0001c010  00001207 R_386_JUMP_SLOT   00010b80   ___tls_get_addr0001c014  00000607 R_386_JUMP_SLOT   00014840   free

下面讨论函数的引用是如何重定位的,参考<<程序员的自我修养P209>>,".got.plt"的前三项是被系统占用的,即Address of .dynamic

、Module ID "Lib.so" 、_dl_runtime_resolve(),那么可知0x0001c008+4*3 = 0x0001c014为真正导入函数地址的地方,即为"free"

画个".got.plt"的结构图

      

个人觉得讨论得还不是很详细,若有不对的地方,希望读者能够指正!谢谢!


原创粉丝点击