【实验二】进程的创建与可执行程序的加载

实验环境:Ubuntu9.04 GCC4.3.3

实验要求:

  • 参考进程初探 编程实现fork(创建一个进程实体) -> exec(将ELF可执行文件内容加载到进程实体) -> running program
  • 参照C代码中嵌入汇编代码示例及用汇编代码使用系统调用time示例分析fork和exec系统调用在内核中的执行过程
  • 注意task_struct进程控制块,ELF文件格式与进程地址空间的联系,注意Exec系统调用返回到用户态时EIP指向的位置。
  • 动态链接库在ELF文件格式中与进程地址空间中的表现形式
  • 通过300-500字总结以上实验和分析所得,实验情况和分析的关键代码可以作为总结后面的附录以提供详细信息。

 一、Linux进程

1、查看进程命令ps和pstree

ps && pstree
PID TTY          TIME CMD
 7131 pts/1    00:00:00 bash
13995 pts/1    00:00:00 ps
init-+-NetworkManager-+-dhclient
     |                `-{NetworkManager}
     |-acpid
     |-atd
     |-avahi-daemon---avahi-daemon
     |-bluetoothd
     |-bonobo-activati---{bonobo-activati}
     |-console-kit-dae---63*[{console-kit-dae}]
     |-cron
     |-cupsd
     |-2*[dbus-daemon]
     |-dbus-launch
     |-dd
     |-fast-user-switc
     |-gconfd-2
     |-gdm---gdm-+-Xorg
     |           `-x-session-manag-+-bluetooth-apple
     |                             |-evolution-alarm---{evolution-alarm}
     |                             |-gnome-panel
     |                             |-metacity
     |                             |-nautilus
     |                             |-nm-applet
     |                             |-python
     |                             |-seahorse-agent
     |                             |-ssh-agent
     |                             |-update-notifier
     |                             `-{x-session-manag}
     |-gnome-keyring-d
     |-gnome-power-man
     |-gnome-terminal-+-bash---pstree
     |                |-gnome-pty-helpe
     |                `-{gnome-terminal}
     |-gvfs-fuse-daemo---3*[{gvfs-fuse-daemo}]
     |-gvfs-gphoto2-vo
     |-gvfs-hal-volume---{gvfs-hal-volume}
     |-gvfsd
     |-gvfsd-burn
     |-gvfsd-trash
     |-hald---hald-runner-+-hald-addon-acpi
     |                    |-hald-addon-inpu
     |                    `-2*[hald-addon-stor]
     |-indicator-apple
     |-klogd
     |-6*[login---bash]
     |-mixer_applet2---{mixer_applet2}
     |-nm-system-setti
     |-notify-osd
     |-pulseaudio-+-gconf-helper
     |            `-2*[{pulseaudio}]
     |-scim-bridge
     |-scim-helper-man
     |-2*[scim-launcher]
     |-scim-panel-gtk---{scim-panel-gtk}
     |-syslogd
     |-system-tools-ba
     |-test1
     |-trashapplet
     |-udevd
     `-wpa_supplicant
查看执行结果

2、终止进程命令Ctrl+C、killall、kill

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 #include <unistd.h>
 4 #include <sys/types.h>
 5 
 6 int main()
 7 {
 8     while(1)
 9     {
10         sleep(5);
11     }
12     return 0;
13 }
gcc sleep.c -o sleep
./sleep

 

3、fork函数

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 #include <unistd.h>
 4 #include <sys/types.h>
 5 
 6 int main()
 7 {
 8     pid_t pid;
 9     pid = fork();
10     if(pid == 0)
11         printf("Child process!\n");
12     else if(pid > 0)
13         printf("Parent process!\n");
14     else
15         printf("Fork failure!\n");
16     return 0;
17 }

Q:What the output of the following program?

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 #include <unistd.h>
 4 #include <sys/types.h>
 5 
 6 int main()
 7 {
 8     pid_t pid;
 9     int count = 13;
10     pid = fork();
11     if(pid == 0)
12     {
13         sleep(5);
14         count = 31;
15     }
16     else if(pid > 0)
17     {
18         wait(NULL);
19     }
20     else
21         printf("Fork failure!\n");
22     printf("There are %d apples!\n",count);
23     return 0;
24 }

Q:How many processes are there altogether?

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 #include <unistd.h>
 4 #include <sys/types.h>
 5 
 6 int main(int argc, char *argv[])
 7 {
 8     fork();
 9     fork() && fork() || fork();
10     fork();
11     return 0;
12 }

答:20个进程

4、execl 函数

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 #include <unistd.h>
 4 #include <sys/types.h>
 5 
 6 int main(int argc, char *argv[])
 7 {
 8     execl("/usr/bin/vi","vi",NULL);
 9     /* We can only reach this code when is an error in execl */
10     perror("execl");
11     return 0;
12 }
gcc execl.c -o execl
./execl

执行之后打开VI编辑器

5、实现一个简单的shell程序

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <string.h>
 5 #include <errno.h>
 6 #include <sys/types.h>
 7 #define BUFFER_SIZE 1<<16 // 用户输入的最大长度
 8 #define ARR_SIZE 1<<16 // 单个参数的最大的长度
 9 
10 
11 void parse_args(char *buffer, char** args,
12                 size_t args_size, size_t *nargs)
13 {
14     char *buf_args[args_size]; /* 需要C99支持 */
15     char **cp;
16     char *wbuf;
17     size_t i, j;
18 
19     wbuf=buffer;
20     buf_args[0]=buffer;
21     args[0] =buffer;
22 
23     // 分解字符串为一组字符串
24     for(cp=buf_args; (*cp=strsep(&wbuf, " \n\t")) != NULL ;){
25         if ((*cp != '\0') && (++cp >= &buf_args[args_size]))
26             break;
27     }
28 
29     // 分解buffer字符串为一组args字符串
30     for (j=i=0; buf_args[i]!=NULL; i++){
31         if(strlen(buf_args[i])>0)
32             args[j++]=buf_args[i];
33     }
34 
35     *nargs=j;
36     args[j]=NULL;
37 }
38 
39 
40 int main(int argc, char *argv[])
41 {
42     char buffer[BUFFER_SIZE];
43     char *args[ARR_SIZE];
44 
45     int *ret_status; // 子进程的状态
46     size_t nargs; // 参数的个数
47     pid_t pid;
48 
49     while(1){
50         printf(">");
51         fgets(buffer, BUFFER_SIZE, stdin);
52         parse_args(buffer, args, ARR_SIZE, &nargs);
53 
54         if (nargs==0) continue;
55         if (!strcmp(args[0], "exit" )) exit(0);
56         pid = fork();
57         if (pid){
58             printf("Waiting for Command (%d)\n", pid);
59             pid = wait(ret_status);
60             printf("Command (%d) finished\n", pid);
61         } else {
62             if( execvp(args[0], args)) {
63                 puts(strerror(errno));
64                 exit(127);
65             }
66         }
67     }
68     return 0;
69 }

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

1、C代码中嵌入一般汇编代码的方式

 1 #include <stdio.h>
 2 
 3 int  main()
 4 {
 5     /* val1+val2=val3 */
 6     unsigned int val1 = 1;
 7     unsigned int val2 = 2;
 8     unsigned int val3 = 0;
 9     printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);
10     asm volatile(
11     "movl $0,%%eax\n\t" /* clear %eax to 0*/
12     "addl %1,%%eax\n\t" /* %eax += val1 */
13     "addl %2,%%eax\n\t" /* %eax += val2 */
14     "movl %%eax,%0\n\t" /* val2 = %eax*/
15     : "=m" (val3) /* output =m mean only write output memory variable*/
16     : "c" (val1),"d" (val2) /* input c or d mean %ecx/%edx*/
17     );
18     printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
19 
20     return 0;
21 }

 1 080483c4 <main>:
 2  #include <stdio.h>
 3  
 4  int  main()
 5  {
 6   80483c4:    8d 4c 24 04              lea    0x4(%esp),%ecx
 7   80483c8:    83 e4 f0                 and    $0xfffffff0,%esp
 8   80483cb:    ff 71 fc                 pushl  -0x4(%ecx)
 9   80483ce:    55                       push   %ebp
10   80483cf:    89 e5                    mov    %esp,%ebp
11   80483d1:    51                       push   %ecx
12   80483d2:    83 ec 24                 sub    $0x24,%esp
13  /* val1+val2=val3 */
14  unsigned int val1 = 1;
15   80483d5:    c7 45 f8 01 00 00 00     movl   $0x1,-0x8(%ebp)
16  unsigned int val2 = 2;
17   80483dc:    c7 45 f4 02 00 00 00     movl   $0x2,-0xc(%ebp)
18  unsigned int val3 = 0;
19   80483e3:    c7 45 f0 00 00 00 00     movl   $0x0,-0x10(%ebp)
20  printf("val1:%d,val2:%d,val3:%d\n",val1,val2,val3);
21   80483ea:    8b 45 f0                 mov    -0x10(%ebp),%eax
22   80483ed:    89 44 24 0c              mov    %eax,0xc(%esp)
23   80483f1:    8b 45 f4                 mov    -0xc(%ebp),%eax
24   80483f4:    89 44 24 08              mov    %eax,0x8(%esp)
25   80483f8:    8b 45 f8                 mov    -0x8(%ebp),%eax
26   80483fb:    89 44 24 04              mov    %eax,0x4(%esp)
27   80483ff:    c7 04 24 10 85 04 08     movl   $0x8048510,(%esp)
28   8048406:    e8 ed fe ff ff           call   80482f8 <printf@plt>
29  asm volatile(
30   804840b:    8b 4d f8                 mov    -0x8(%ebp),%ecx
31   804840e:    8b 55 f4                 mov    -0xc(%ebp),%edx
32   8048411:    b8 00 00 00 00           mov    $0x0,%eax
33   8048416:    01 c8                    add    %ecx,%eax
34   8048418:    01 d0                    add    %edx,%eax
35   804841a:    89 45 f0                 mov    %eax,-0x10(%ebp)
36  "addl %2,%%eax\n\t" /* %eax += val2 */
37  "movl %%eax,%0\n\t" /* val2 = %eax*/
38  : "=m" (val3) /* output =m mean only write output memory variable*/
39  : "c" (val1),"d" (val2) /* input c or d mean %ecx/%edx*/
40  );
41  printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
42   804841d:    8b 45 f0                 mov    -0x10(%ebp),%eax
43   8048420:    89 44 24 0c              mov    %eax,0xc(%esp)
44   8048424:    8b 45 f4                 mov    -0xc(%ebp),%eax
45   8048427:    89 44 24 08              mov    %eax,0x8(%esp)
46   804842b:    8b 45 f8                 mov    -0x8(%ebp),%eax
47   804842e:    89 44 24 04              mov    %eax,0x4(%esp)
48   8048432:    c7 04 24 29 85 04 08     movl   $0x8048529,(%esp)
49   8048439:    e8 ba fe ff ff           call   80482f8 <printf@plt>
50  
51  return 0;
52   804843e:    b8 00 00 00 00           mov    $0x0,%eax
53  }
查看反汇编代码

2、C代码中嵌入系统调用汇编代码

 1 #include <stdio.h>
 2 #include <time.h>
 3 
 4 int  main()
 5 {
 6     time_t tt;
 7     struct tm *t;
 8         int ret;
 9 /*
10 (gdb) disassemble time
11 Dump of assembler code for function time:
12    0x0804f800 <+0>:    push   %ebp
13    0x0804f801 <+1>:    mov    %esp,%ebp
14    0x0804f803 <+3>:    mov    0x8(%ebp),%edx
15    0x0804f806 <+6>:    push   %ebx
16    0x0804f807 <+7>:    xor    %ebx,%ebx
17    0x0804f809 <+9>:    mov    $0xd,%eax
18    0x0804f80e <+14>:    int    $0x80
19    0x0804f810 <+16>:    test   %edx,%edx
20    0x0804f812 <+18>:    je     0x804f816 <time+22>
21    0x0804f814 <+20>:    mov    %eax,(%edx)
22    0x0804f816 <+22>:    pop    %ebx
23    0x0804f817 <+23>:    pop    %ebp
24    0x0804f818 <+24>:    ret    
25 End of assembler dump.
26 
27 */
28 #if 0
29     time(&tt);
30     printf("tt:%ld\n",tt);
31 #else
32         /* 没有使用常规寄存器传参的方法 */
33     asm volatile(
34         "mov $0,%%ebx\n\t" /* 不使用参数tt */
35     "mov $0xd,%%eax\n\t" 
36     "int $0x80\n\t" 
37     "mov %%eax,%0\n\t"  
38     : "=m" (tt) 
39     );
40     printf("tt:%ld\n",tt);
41     t = localtime(&tt);
42     printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
43         /* 使用常规寄存器传参的方法 */
44     asm volatile(
45         "mov %1,%%ebx\n\t" /* 使用参数tt */
46     "mov $0xd,%%eax\n\t" 
47     "int $0x80\n\t" 
48     "mov %%eax,%0\n\t"  
49     : "=m" (ret) 
50         : "b" (&tt)
51     );
52     printf("tt:%ld\n",tt);
53     t = localtime(&tt);
54     printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
55 #endif
56 
57 
58     return 0;
59 }

  1 080483f4 <main>:
  2  #include <stdio.h>
  3  #include <time.h>
  4  
  5  int  main()
  6  {
  7   80483f4:    8d 4c 24 04              lea    0x4(%esp),%ecx
  8   80483f8:    83 e4 f0                 and    $0xfffffff0,%esp
  9   80483fb:    ff 71 fc                 pushl  -0x4(%ecx)
 10   80483fe:    55                       push   %ebp
 11   80483ff:    89 e5                    mov    %esp,%ebp
 12   8048401:    57                       push   %edi
 13   8048402:    56                       push   %esi
 14   8048403:    53                       push   %ebx
 15   8048404:    51                       push   %ecx
 16   8048405:    83 ec 38                 sub    $0x38,%esp
 17  #if 0
 18      time(&tt);
 19      printf("tt:%ld\n",tt);
 20  #else
 21          /* 没有使用常规寄存器传参的方法 */
 22      asm volatile(
 23   8048408:    bb 00 00 00 00           mov    $0x0,%ebx
 24   804840d:    b8 0d 00 00 00           mov    $0xd,%eax
 25   8048412:    cd 80                    int    $0x80
 26   8048414:    89 45 ec                 mov    %eax,-0x14(%ebp)
 27      "mov $0xd,%%eax\n\t" 
 28      "int $0x80\n\t" 
 29      "mov %%eax,%0\n\t"  
 30      : "=m" (tt) 
 31      );
 32      printf("tt:%ld\n",tt);
 33   8048417:    8b 45 ec                 mov    -0x14(%ebp),%eax
 34   804841a:    89 44 24 04              mov    %eax,0x4(%esp)
 35   804841e:    c7 04 24 e0 85 04 08     movl   $0x80485e0,(%esp)
 36   8048425:    e8 06 ff ff ff           call   8048330 <printf@plt>
 37      t = localtime(&tt);
 38   804842a:    8d 45 ec                 lea    -0x14(%ebp),%eax
 39   804842d:    89 04 24                 mov    %eax,(%esp)
 40   8048430:    e8 db fe ff ff           call   8048310 <localtime@plt>
 41   8048435:    89 45 e8                 mov    %eax,-0x18(%ebp)
 42      printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
 43   8048438:    8b 45 e8                 mov    -0x18(%ebp),%eax
 44   804843b:    8b 30                    mov    (%eax),%esi
 45   804843d:    8b 45 e8                 mov    -0x18(%ebp),%eax
 46   8048440:    8b 78 04                 mov    0x4(%eax),%edi
 47   8048443:    8b 45 e8                 mov    -0x18(%ebp),%eax
 48   8048446:    8b 50 08                 mov    0x8(%eax),%edx
 49   8048449:    8b 45 e8                 mov    -0x18(%ebp),%eax
 50   804844c:    8b 48 0c                 mov    0xc(%eax),%ecx
 51   804844f:    8b 45 e8                 mov    -0x18(%ebp),%eax
 52   8048452:    8b 58 10                 mov    0x10(%eax),%ebx
 53   8048455:    8b 45 e8                 mov    -0x18(%ebp),%eax
 54   8048458:    8b 40 14                 mov    0x14(%eax),%eax
 55   804845b:    05 6c 07 00 00           add    $0x76c,%eax
 56   8048460:    89 74 24 18              mov    %esi,0x18(%esp)
 57   8048464:    89 7c 24 14              mov    %edi,0x14(%esp)
 58   8048468:    89 54 24 10              mov    %edx,0x10(%esp)
 59   804846c:    89 4c 24 0c              mov    %ecx,0xc(%esp)
 60   8048470:    89 5c 24 08              mov    %ebx,0x8(%esp)
 61   8048474:    89 44 24 04              mov    %eax,0x4(%esp)
 62   8048478:    c7 04 24 e8 85 04 08     movl   $0x80485e8,(%esp)
 63   804847f:    e8 ac fe ff ff           call   8048330 <printf@plt>
 64          /* 使用常规寄存器传参的方法 */
 65      asm volatile(
 66   8048484:    8d 5d ec                 lea    -0x14(%ebp),%ebx
 67   8048487:    89 db                    mov    %ebx,%ebx
 68   8048489:    b8 0d 00 00 00           mov    $0xd,%eax
 69   804848e:    cd 80                    int    $0x80
 70   8048490:    89 45 e4                 mov    %eax,-0x1c(%ebp)
 71      "int $0x80\n\t" 
 72      "mov %%eax,%0\n\t"  
 73      : "=m" (ret) 
 74          : "b" (&tt)
 75      );
 76      printf("tt:%ld\n",tt);
 77   8048493:    8b 45 ec                 mov    -0x14(%ebp),%eax
 78   8048496:    89 44 24 04              mov    %eax,0x4(%esp)
 79   804849a:    c7 04 24 e0 85 04 08     movl   $0x80485e0,(%esp)
 80   80484a1:    e8 8a fe ff ff           call   8048330 <printf@plt>
 81      t = localtime(&tt);
 82   80484a6:    8d 45 ec                 lea    -0x14(%ebp),%eax
 83   80484a9:    89 04 24                 mov    %eax,(%esp)
 84   80484ac:    e8 5f fe ff ff           call   8048310 <localtime@plt>
 85   80484b1:    89 45 e8                 mov    %eax,-0x18(%ebp)
 86      printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
 87   80484b4:    8b 45 e8                 mov    -0x18(%ebp),%eax
 88   80484b7:    8b 30                    mov    (%eax),%esi
 89   80484b9:    8b 45 e8                 mov    -0x18(%ebp),%eax
 90   80484bc:    8b 78 04                 mov    0x4(%eax),%edi
 91   80484bf:    8b 45 e8                 mov    -0x18(%ebp),%eax
 92   80484c2:    8b 50 08                 mov    0x8(%eax),%edx
 93   80484c5:    8b 45 e8                 mov    -0x18(%ebp),%eax
 94   80484c8:    8b 48 0c                 mov    0xc(%eax),%ecx
 95   80484cb:    8b 45 e8                 mov    -0x18(%ebp),%eax
 96   80484ce:    8b 58 10                 mov    0x10(%eax),%ebx
 97   80484d1:    8b 45 e8                 mov    -0x18(%ebp),%eax
 98   80484d4:    8b 40 14                 mov    0x14(%eax),%eax
 99   80484d7:    05 6c 07 00 00           add    $0x76c,%eax
100   80484dc:    89 74 24 18              mov    %esi,0x18(%esp)
101   80484e0:    89 7c 24 14              mov    %edi,0x14(%esp)
102   80484e4:    89 54 24 10              mov    %edx,0x10(%esp)
103   80484e8:    89 4c 24 0c              mov    %ecx,0xc(%esp)
104   80484ec:    89 5c 24 08              mov    %ebx,0x8(%esp)
105   80484f0:    89 44 24 04              mov    %eax,0x4(%esp)
106   80484f4:    c7 04 24 e8 85 04 08     movl   $0x80485e8,(%esp)
107   80484fb:    e8 30 fe ff ff           call   8048330 <printf@plt>
108  #endif
109  
110  
111      return 0;
112   8048500:    b8 00 00 00 00           mov    $0x0,%eax
113  }
查看反汇编代码

3、fork系统调用在内核中的执行过程

fork函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。从上图可以看出,一开始是一个控制流程,调用fork之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id(从根本上说fork是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。

fork的返回值这样规定是有道理的。fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。

fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。

在用户态下,使用fork()创建一个进程对我们来说已经不再陌生。除了这个函数,一个新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个API函数均由C库提供,它们分别在C库中封装了与其同名的系统调用fork(),vfork()和clone()。API所封装的系统调用对编程者是隐藏的,编程者只需知道如何使用这些API即可。

上述三个系统调用所对应的系统调用号在linux/include/asm-i386/unistd.h中定义如下:

1 #define __NR_restart_syscall      0
2 #define __NR_exit                 1
3 #define __NR_fork                 2
4 #define __NR_clone              120
5 #define __NR_vfork              190

传统的创建一个新进程的方式是子进程拷贝父进程所有资源,这无疑使得进程的创建效率低,因为子进程需要拷贝父进程的整个地址空间。更糟糕的是,如果子进程创建后又立马去执行exec族函数,那么刚刚才从父进程那里拷贝的地址空间又要被清除以便装入新的进程映像。

就像一开始所分析的那样,用户程序并不直接使用系统调用,而是通过C库中的API。而系统调用在内核中也并不是直接实现的,而是通过调用各自对应的服务例程。系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。因此,想要了解fork等系统调用的详细执行过程,就必须查看它们所对应的内核函数(也就是服务例程)是如何实现的。上述三个系统调用对应的服务例程分别定义在linux/arch/i386/kernel/process.c 中,具体如下:

 1 asmlinkage int sys_fork(struct pt_regs regs)
 2 {
 3     return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
 4 }
 5 
 6 asmlinkage int sys_clone(struct pt_regs regs)
 7 {
 8     unsigned long clone_flags;
 9     unsigned long newsp;
10     int __user *parent_tidptr, *child_tidptr;
11 
12     clone_flags = regs.ebx;
13     newsp = regs.ecx;
14     parent_tidptr = (int __user *)regs.edx;
15     child_tidptr = (int __user *)regs.edi;
16     if (!newsp)
17         newsp = regs.esp;
18     return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);
19 }
20 
21 asmlinkage int sys_vfork(struct pt_regs regs)
22 {
23     return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
24 }

可以看到do_fork()均被上述三个服务例程调用。而在do_fork()内部又调用了copy_process(),因此我们可以通过下图来理解上述的调用关系。

 

当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。在X86体系中,可以通过两种不同的方式进入系统调用:执行int $0×80汇编命令和执行sysenter汇编命令。后者是Intel在PentiumII中引入的指令,内核从2.6版本开始支持这条命令。本文将集中讨论以int $0×80方式进入系统调用的过程。

通过int $0×80方式调用系统调用实际上是用户进程产生一个中断向量号为0×80的软中断。当用户态进程发出int $0×80指令时,CPU将从用户态切换到内核态并开始执行system_call()。这个函数是通过汇编命令来实现的,它是0×80号软中断对应的中断处理程序。对于所有系统调用来说,它们都必须先进入system_call(),也就是所谓的系统调用处理程序。再通过系统调用号跳转到具体的系统调用服务例程处。

在该函数执行之前,CPU控制单元已经将eflags、cs、eip、ss和esp寄存器的值自动保存到该进程对应的内核栈中。随之,在system_call内部首先将存储在eax寄存器中的系统调用号压入栈中。接着执行SAVE_ALL宏。该宏在栈中保存接下来的系统调用可能要用到的所有CPU寄存器。

4、exec系统调用在内核中的执行过程

fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

其实有六种以exec开头的函数,统称exec函数:

1 #include <unistd.h>
2 
3 int execl(const char *path, const char *arg, ...);
4 int execlp(const char *file, const char *arg, ...);
5 int execle(const char *path, const char *arg, ..., char *const envp[]);
6 int execv(const char *path, char *const argv[]);
7 int execvp(const char *file, char *const argv[]);
8 int execve(const char *path, char *const argv[], char *const envp[]);

在内核中,exec函数族拥有统一的函数入口sys_execve,在用户态下调用execve(),引发系统中断后,在内核态执行的相应函数是do_sys_execve(),而do_sys_execve()会调用do_execve()函数。do_execve()首先会读入可执行文件,如果可执行文件不存在,则报错。否则检查可执行文件的权限,如果文件不是当前用户可执行的,则execve()会返回-1,报permission denied的错误,否则继续读入运行可执行文件所需的信息,接着系统调用search_binary_handler(),根据可执行文件的类型查找到相应的处理函数,然后执行相应的load_binary()函数开始加载可执行文件。

加载ELF类型文件的handler是load_elf_binary(),它先读入ELF文件的头部,根据ELF文件的头部信息读入各种数据(header information),再次扫描程序段描述表,找到类型为PT_LOAD的段,将其映射(elf_map())到内存的固定地址上。如果没有动态链接器的描述段,把返回的入口地址设置成应用程序的入口,完成这个功能的是start_thread(),start_thread()并不启用一个线程,而只是用来修改了pt_regs中保存的PC等寄存器的值,使其指向加载的应用程序的入口。这样当内核操作结束,返回用户态的时候,接下来执行的就是应用程序了。

如果应用程序中使用了动态链接库,内核出了加载指定的可执行文件,还要把控制权交给动态链接器以处理动态链接的程序。内核搜寻段表,找到标记为PT_INTERP的段中所对应的动态链接器的名称,并使用load_elf_interp()加载其映像,并把返回的入口地址设置成load_elf_interp()的返回值,即动态链接器入口。当execve退出的时候动态链接器接着运行。动态链接器检查应用程序对共享连接库的依赖性,并在需要时对其进行加载,对程序的外部引用进行重定位。然后动态链接器把控制权交给应用程序,从EFL文件头部中定义的程序进入点开始执行。

5、task_struct进程控制块

在Linux内核中,PCB对应着一个具体的结构体——task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了与一个进程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。进程描述符内部是比较复杂的如下图所示。

无论是内核线程还是用户进程,对于内核来说,无非都是task_struct这个数据结构的一个实例而已,task_struct被称为进程描述符(process descriptor),因为它记录了这个进程所有的context。其中有一个被称为'内存描述符‘(memory descriptor)的数据结构mm_struct,抽象并描述了Linux视角下管理进程地址空间的所有信息。mm_struct定义在include/linux/mm_types.h中,其中的域抽象了进程的地址空间,如下图所示:

 

6、ELF文件格式

ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型:

  • 可重定位的目标文件(Relocatable)

  • 可执行文件(Executable)

  • 共享库(Shared Object)

ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是由Program Header Table描述的一系列Segment的集合。如下图所示。

 

7、进程地址空间

exec系统调用执行新程序时会把命令行参数和环境变量表传递给main函数,它们在整个进程地址空间中的位置如下图所示。

 

8、ELF文件格式中与进程地址空间中的表现形式

可执行文件和进程的内存映像虽然都有代码段和数据段,但两者其实是不同的。首先可执行文件(也就是程序)是静态的,存放在磁盘上,而进程的内存映像只有在程序运行时才产生;其次,可执行文件没有堆栈段,而进程的内存映像是包含堆栈段的,因为堆(heap)用于动态分配内存,而栈(stack)需要保存局部变量、临时数据和传递到函数的参数等。从这些不同点也可以说明程序是动态的。可执行文件的段在进程地址空间中的分布图可以参考如下:

三、总结

我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。现在我们全面了解一下其中都有哪些信息。

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。

  • 进程的状态,有运行、挂起、停止、僵尸等状态。

  • 进程切换时需要保存和恢复的一些CPU寄存器。

  • 描述虚拟地址空间的信息。

  • 描述控制终端的信息。

  • 当前工作目录(Current Working Directory)。

  • umask掩码。

  • 文件描述符表,包含很多指向file结构体的指针。

  • 和信号相关的信息。

  • 用户id和组id。

  • 控制终端、Session和进程组。

  • 进程可以使用的资源上限(Resource Limit)

forkexec是本章要介绍的两个重要的系统调用。fork的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程(Parent Process),新进程称为子进程(Child Process)。系统中同时运行着很多进程,这些进程都是从最初只有一个进程开始一个一个复制出来的。在Shell下输入命令可以运行一个程序,是因为Shell进程在读取用户输入的命令之后会调用fork复制出一个新的Shell进程,然后新的Shell进程调用exec执行新的程序。

一个程序可以多次加载到内存,成为同时运行的多个进程,例如可以同时开多个终端窗口运行/bin/bash,另一方面,一个进程在调用exec前后也可以分别执行两个不同的程序,例如在Shell提示符下输入命令ls,首先fork创建子进程,这时子进程仍在执行/bin/bash程序,然后子进程调用exec执行新的程序/bin/ls,如下图所示。

 


 

(完:学号:SA6352)

本文链接:http://www.cnblogs.com/kaimin/archive/2013/05/30/the-process-of-creating-and-the-executable-program-of-loading.html

转载于:https://www.cnblogs.com/kaimin/archive/2013/05/30/the-process-of-creating-and-the-executable-program-of-loading.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值