实验环境: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
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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 }
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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 }
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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格式,它有三种不同的类型:
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和进程组。
fork
和exec
是本章要介绍的两个重要的系统调用。fork
的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程(Parent Process),新进程称为子进程(Child Process)。系统中同时运行着很多进程,这些进程都是从最初只有一个进程开始一个一个复制出来的。在Shell下输入命令可以运行一个程序,是因为Shell进程在读取用户输入的命令之后会调用fork
复制出一个新的Shell进程,然后新的Shell进程调用exec
执行新的程序。
一个程序可以多次加载到内存,成为同时运行的多个进程,例如可以同时开多个终端窗口运行/bin/bash
,另一方面,一个进程在调用exec
前后也可以分别执行两个不同的程序,例如在Shell提示符下输入命令ls
,首先fork
创建子进程,这时子进程仍在执行/bin/bash
程序,然后子进程调用exec
执行新的程序/bin/ls
,如下图所示。
(完:学号:SA6352)