vfork()的若干问题

时间:2022-07-19 06:52:34

转自:http://chhaj5236.blog.163.com/blog/static/1128810812013327102727881/

参考资料[1]对vfork进行了较为详细的描述:vfork()函数和fork()一样会创建一个新进程,所不同的是vfork()创建的子进程与父进程共享地址空间,且父进程会被阻塞,直到子进程调用exec()家族的某个函数,或调用_exit()。由于父子进程共享相同的地址空间,(准确地说是子进程)一定不要从调用vfork()的函数中返回,否则会破坏父进程的堆栈。同时,在执行exec()和_exit()之前,也最好不要调用任何对父进程状态有影响的操作,如改变父进程某个变量的值,或使用exit()退出子进程(exit()退出时会关闭I/O缓存,意味着父进程也会失去I/O缓存)等。

本文的主要目的就是理清参考文献[2]中提到的例子,即如果子进程从调用vfork()的函数中返回,会发生什么?为了更加清晰地说明这个问题,将文中提及的代码修改为如下所示:

#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int createproc();
int main(){
    pid_t pid;
    pid = createproc();
    printf("pid: %d, ret_pid: %d\n", getpid(), pid);
    exit(0);
}
int createproc(){
    pid_t pid;
    pid = vfork();  
    printf("after vfork - pid: %d, ret_pid: %d\n", getpid(), pid);
    if(!pid){
        printf("pid: %d, parent: %d\n", getpid(), getppid());
        //execl("/bin/ls", "ls", "-l", NULL); //正常情况下,应该调用exec()家族的一个函数,载入新的程序
        return pid;
    }   
    else return -1; 
}

上面程序的输出结果如下所示:
after vfork - pid: 29327, ret_pid: 0
pid: 29327, parent: 29326
pid: 29327, ret_pid: 0
after vfork - pid: 29326, ret_pid: 29327
after vfork - pid: 29328, ret_pid: 0
pid: 29328, parent: 29326
Segmentation fault

从上面的结果可以看出,前三行均是由pid为29327的子进程输出,第四行为父进程29326输出,之后两行则由子进程29328输出,最后则发生了段错误。所以这个程序虽然只有一个vfork,但是父进程实际上创建了两个子进程。下面具体来看下程序执行的过程中发生了什么,本文把main()和createproc()的反汇编代码列在最后,只在需要的时候去定位比较关键的内容。
首先,当主进程(父进程)执行到main()函数中的pid=createproc()一句时,实际上对应着两条汇编指令,一个是调用createproc过程(反汇编代码中的<_Z10createprocv>),一个是将函数结果赋值给变量pid,即反汇编代码中<main>下面的两个标红语句:
 

80484d5: e8 2d 00 00 00 call 8048507 <_Z10createprocv>
80484da: 89 44 24 1c mov %eax,0x1c(%esp)

使用call调用createproc函数前,进程会将当前指令的下一条指令地址压入堆栈,即0x80484da,然后转移到createproc过程。在createproc过程中,父进程将调用vfork()函数并阻塞。同上,调用vfork()函数前,除为了保存现场压入堆栈的数据外,还要将vfork的返回地址压入堆栈,即0x8048513,参见反汇编代码中<_Z10createprocv>下面的两个标红语句:
 
 

804850e: e8 ad fe ff ff call 80483c0 <vfork@plt>
8048513: 89 45 f4 mov %eax,-0xc(%ebp)

经过上面两步操作后,堆栈的状态大致如①所示(其实vfork()的返回地址保存在ecx寄存器中,后面再详细说明,暂时认为其存放在堆栈中易于理解)。由于此时父进程29326阻塞,操作系统将为父进程保存包括esp等寄存器在内的进程上下文。而子进程29327从vfork()返回,esp将函数返回地址0x8048513弹出到指令寄存器,并执行相应的指令,也就是将vfork()的返回值0赋给pid,之后输出第一句“after vfork - pid: 29327, ret_pid: 0”。由于是子进程(pid==0),所以还会输出第二句“pid: 29327, parent: 29326”。此时子进程执行return pid,这意味着之前被createproc压入堆栈的数据会在return pid执行之前弹出以恢复现场,此时堆栈的状态如②所示。从图中可以看出,子进程29327执行return后的返回地址为当前esp所指内容,意味着将弹出并执行0x80484da所对应的指令,即main()函数中将createproc()的返回值赋给pid。我们发现由于堆栈共享,子进程由createproc()创建,但最终返回到main()函数。指令接着往下执行,所以输出的第三句依然属于子进程29327:“pid: 29327, ret_pid: 0”,注意:在exit()之前的函数调用将改变堆栈中的数据,如压入参数等操作,如图③所示。最后子进程29327执行exit(0)退出,从反汇编代码<main>中最后两行可以看出,在前面将0x80484da弹出堆栈后,esp所指栈顶被赋值为0以传递exit参数,之后调用再调用exit()函数,正如之前所说,call exit的下一条指令地址将被压入堆栈,但这条语句已经是<main>的最后一条指令,它的下一条指令为<_Z10createprocv>的第一条指令,所以将返回地址0x8048507压入堆栈,如图④所示。
 
 

 80484fb: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
 8048502: e8 99 fe ff ff        call   80483a0 <exit@plt>
08048507   < _Z10createprocv >:
 8048507: 55                    push   %ebp
经过上面的一番折腾后,子进程29327调用完exit()后结束运行,而父进程29326则恢复进程上下文,并将esp指向了如图⑤的位置,即最初调用vfork()的位置。父进程从堆栈中取出vfork()返回地址,将vfork()的返回值——子进程id 29327——赋值给pid后,输出“after vfork - pid: 29326, ret_pid: 29327”。由于是父进程,最后将执行return -1。如前所述,createproc恢复现场后(很大程度上是脏现场),esp指向了如图⑥的位置。此时父进程并没有如期望一样返回到main()函数,而是从exit()的返回地址0x8048507继续执行,即返回到了createproc()函数的第一条指令。
显然,父进程之后又会执行一次vfork(),并再次被阻塞,第二个子进程29328则继续执行。和父进程第一次执行createproc()内指令不通的是,这一次并没有通过正常的call指令进入,也就意味着createproc()的返回地址没有被压入堆栈,而是直接压入用于保存现场的数据。这就意味着,子进程29328在执行完“after vfork - pid: 29328, ret_pid: 0”,“pid: 29328, parent: 29326”,准备return时,createproc()恢复现场的pop指令会使得esp指针指向图中0x0000000所在单元,并将该值作为返回地址。那么最后的“Segmentation fault”也就不足为奇了。
vfork()的若干问题
现在我们来进一步讨论一个问题,如上图我们可以看出,如果在子进程29327回到main函数后执行的某个函数调用,其在堆栈内压入了大量参数,以至于保存有vfork()返回地址0x8048513的单元被覆盖,则子进程结束时,父进程将无法正常从vfork()返回,很可能直接出现“Segmentation fault”,如图③所对应的情况。但是经过测试,不论函数所带的参数合起来有多少字节,父进程始终能从vfork()正确返回。正如文献[2]最后所讨论的,库函数vfork()本身没有使用栈上的内存空间,而是将返回地址弹出并保存在ecx中,在函数调用的最后再将ecx压回堆栈。同时,不同进程切换时会保护现场,也就意味着寄存器的值不会被其它进程所影响,所以调用vfork()的父进程总能从vfork()正确返回。vfork()实例代码来自文献[2],如下所示:
 
  

000983f0 <__vfork>:
983f0:       59                       pop    %ecx
983f1:       65 8b 15 6c 00 00 00     mov    %gs:0x6c,%edx
983f8:       89 d0                    mov    %edx,%eax
983fa:       f7 d8                   neg    %eax
......
9840e:       cd 80                    int    $0x80
98410:       51                       push   %ecx
......


==============================反汇编代码================================
080484cc <main>:
 80484cc: 55                    push   %ebp
 80484cd: 89 e5                 mov    %esp,%ebp
 80484cf: 83 e4 f0              and    $0xfffffff0,%esp
 80484d2: 83 ec 20              sub    $0x20,%esp
 80484d5:e8 2d 00 00 00       call   8048507 <_Z10createprocv>
 80484da:89 44 24 1c          mov    %eax,0x1c(%esp)
 80484de: e8 9d fe ff ff        call   8048380 <getpid@plt>
 80484e3: 8b 54 24 1c           mov    0x1c(%esp),%edx
 80484e7: 89 54 24 08           mov    %edx,0x8(%esp)
 80484eb: 89 44 24 04           mov    %eax,0x4(%esp)
 80484ef: c7 04 24 08 86 04 08  movl   $0x8048608,(%esp)
 80484f6: e8 75 fe ff ff        call   8048370 <printf@plt>
 80484fb:c7 04 24 00 00 00 00 movl   $0x0,(%esp)
 8048502:e8 99 fe ff ff       call   80483a0 <exit@plt>

08048507 <_Z10createprocv>:
 8048507:55                   push   %ebp
 8048508: 89 e5                 mov    %esp,%ebp
 804850a: 53                     push   %ebx
 804850b: 83 ec 24              sub    $0x24,%esp
 804850e:e8 ad fe ff ff       call   80483c0 <vfork@plt>
 8048513:89 45 f4             mov    %eax,-0xc(%ebp)
 8048516: e8 65 fe ff ff        call   8048380 <getpid@plt>
 804851b: 8b 55 f4              mov    -0xc(%ebp),%edx
 804851e: 89 54 24 08            mov    %edx,0x8(%esp)
 8048522: 89 44 24 04           mov    %eax,0x4(%esp)
 8048526: c7 04 24 20 86 04 08  movl   $0x8048620,(%esp)
 804852d: e8 3e fe ff ff        call   8048370 <printf@plt>
 8048532: 83 7d f4 00           cmpl   $0x0,-0xc(%ebp)
 8048536: 75 25                 jne    804855d <_Z10createprocv+0x56>
 8048538: e8 93 fe ff ff        call   80483d0 <getppid@plt>
 804853d: 89 c3                 mov    %eax,%ebx
 804853f: e8 3c fe ff ff        call   8048380 <getpid@plt>
 8048544: 89 5c 24 08           mov    %ebx,0x8(%esp)
 8048548: 89 44 24 04           mov    %eax,0x4(%esp)
 804854c: c7 04 24 44 86 04 08  movl   $0x8048644,(%esp)
 8048553: e8 18 fe ff ff        call   8048370 <printf@plt>
 8048558: 8b 45 f4              mov    -0xc(%ebp),%eax
 804855b: eb 05                 jmp    8048562 <_Z10createprocv+0x5b>
 804855d: b8 ff ff ff ff         mov    $0xffffffff,%eax
 8048562: 83 c4 24              add    $0x24,%esp
 8048565: 5b                     pop    %ebx
 8048566: 5d                    pop    %ebp
 8048567: c3                     ret    
======================================================================

参考资料:
[1] http://www.mkssoftware.com/docs/man3/vfork.3.asp
[2] 神奇的vfork