张轩 学号:SA*****232
实验内容:
1.参考进程初探 编程实现fork(创建一个进程实体) -> exec(将ELF可执行文件内容加载到进程实体) -> running program
2.参照C代码中嵌入汇编代码示例及用汇编代码使用系统调用time示例分析fork和exec系统调用在内核中的执行过程
3.注意task_struct进程控制块,ELF文件格式与进程地址空间的联系,注意Exec系统调用返回到用户态时EIP指向的位置。
4.动态链接库在ELF文件格式中与进程地址空间中的表现形式
第一部分:fork() 和 exec()
1.使用fork()
- #include<sys/types.h>
- #include<unistd.h>
- pid_t fork(void);
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程相同。但是新进程有自己的数据空间(堆和栈),环境和文件描述符。在父进程中的fork调用返回的是新的子进程的PID,而新进程返回的是0.程序代码也靠这一点来区分父子进程。创建失败返回-1.这边在之前看到过有这么一个解释,相当与是一个链状的进程序列,子进程没有儿子了,所以0相当于指向为空
以下为示例
- #include<sys/types.h>
- #include<unistd.h>
- #include<stdlib.h>
- #include<stdio.h>
- int main()
- {
- pid_t pid;
- pid=fork();
- if(0==pid)
- {
- pid_t cpid=getpid();
- printf("this is the child thread,cpid=%d\n",cpid);
- }
- else if(pid>0)
- {
- pid_t ppid=getpid();
- printf("this is the parent thread,ppid=%d\n",ppid);
- }
- else
- printf("fork error\n");
- return 0;
- }
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
int main()
{
pid_t pid;
pid=fork();
if(0==pid)
{
pid_t cpid=getpid();
printf("this is the child thread,cpid=%d\n",cpid);
}
else if(pid>0)
{
pid_t ppid=getpid();
printf("this is the parent thread,ppid=%d\n",ppid);
}
else
printf("fork error\n");
return 0;
}
2.使用exec()
exec() 系列函数有一组相关的函数组成 ,exec函数可以把当前进程替换为另一个新进程,新进程由path 或者file 参数指定。我们可以使用exec函数将程序的执行从一个程序切换到另一个程序。在新的程序启动后,原来的程序就不再运行了。
- #include<unistd.h>
- char ** environ;
- int execl(constchar *path,constchar *arg0,...,(char *)0);
- int execlp(constchar *file,constchar *arg0,...,(char *)0);
- int execle(constchar *path,constchar *arg0,...,(char *)0,char *const envp[]);
- int execv(constchar *path,char *const argv[]);
- int execvp(constchar *file,char *const argv[]);
- int execve(constchar *path,char *const argv[],char *const envp[]);
#include<unistd.h>
char ** environ;
int execl(const char *path,const char *arg0,...,(char *)0);
int execlp(const char *file,const char *arg0,...,(char *)0);
int execle(const char *path,const char *arg0,...,(char *)0,char *const envp[]);
int execv(const char *path,char *const argv[]);
int execvp(const char *file,char *const argv[]);
int execve(const char *path,char *const argv[],char *const envp[]);
这些函数可以分为两大类。execl execlp execle 的参数个数可以变化,参数以一个空指针结束。execv execvp 的第二个参数是一个字符串数组。不管哪种情况,新程序在启动时会把argv数组中给定的参数传递给main函数。
这些函数通常都是用execve实现的。我们来看下面的一个例子,在这个例子当中,直接指定了各个变量,并没有从shell中读入。
- #include<stdlib.h>
- #include<unistd.h>
- #include<stdio.h>
- int main(int argc,char *argv[],char *envp[])
- {
- pid_t pre_pid=getpid();
- printf("Before Running ps with execlp,pre_pid=%d\n",pre_pid);
- execlp("ps","ps","-l",0);
- pid_t after_pid=getpid();
- printf("After Runing ps wiht execlp,after_pid=%d\n",after_pid);
- exit(0);
- }
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
int main(int argc,char *argv[],char *envp[])
{
pid_t pre_pid=getpid();
printf("Before Running ps with execlp,pre_pid=%d\n",pre_pid);
execlp("ps","ps","-l",0);
pid_t after_pid=getpid();
printf("After Runing ps wiht execlp,after_pid=%d\n",after_pid);
exit(0);
}
我们发现一个很有趣的现象,
printf("After Runing ps wiht execlp,after_pid=%d\n",after_pid);
并没有被执行。这是由于exec函数取代了原先的进程,一般情况下,exec函数是不会返回的,除非发生错误。出现错误时,exec函数返回-1,并设置错误变量errno。
特别要注意的一点,在原进程中已打开的文件描述符在新进程中仍将保持打开,除非它们的执行时关闭标志被置位。
第二部分:fork和exec系统调用在内核中的执行过程
对fork函数进行反汇编
汇编的时候要注意设置断点
如下:
- gcc -g forktest.c -o forktest
- gdb forktest
- b fork
- r
- disas fork
gcc -g forktest.c -o forktest
gdb forktest
b fork
r
disas fork
对exec函数进行反汇编
系统中存在一个formats链表,其链表结构分别对应一种可执行文件的执行方法,execlp()函数对应的系统调用sys_exece()函数会分配一个linux_binprm数据结构并将可执行文件的数据拷贝到其中,并依次扫描formats链表试图执行这个可执行文件,一旦找到了就执行链表结构中的load_binary方法,其主要步骤为:
动态链接执行程序的过程
图六
1)进程控制块task_struct:
task_struct,就是进程描述符(process descriptor),该数据结构中包含了与一个进程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。其中有指向mm_struct结构体的指针mm,这个结构体是对该进程用户空间的描述;也有指向fs_struct结构体的指针fs,这个结构体是对进程当前所在目录的描述;也有指向files_struct结构体的指针files,这个结构体是对该进程已打开的所有文件进行描述;另外还有一个小型的进程描述符thread_info,结构如下图所示。
2)ELF文件格式与进程地址空间的联系:
当子进程调用exec时,启动加载器,加载器删除子进程已有的虚拟存储器段,按照path路径所指向的可执行文件段头表的指导,将ELF可执行文件的相关内容加载到了当前子进程的上下文中(代码段和数据段等),它会覆盖当前子进程的地址空间,从而实现文件组块与进程空间地址的映射。
3)动态链接库在ELF文件格式中与进程地址空间中的表现形式:
应用程序通常都需要使用动态链接库,当在 shell 中敲入一个命令要执行时,内核会创建一个新的进程,它在往这个新进程的进程空间里面加载进可执行程序的代码段和数据段后,也会加载进动态连接器(在Linux里面通常就是 /lib/ld-linux.so 符号链接所指向的那个程序,它本省就是一个动态库)的代码段和数据。在这之后,内核将控制传递给动态链接库里面的代码。动态连接器接下来负责加载该命令应用程序所需要使用的各种动态库。加载完毕,动态连接器才将控制传递给应用程序的main函数。如此,应用程序才得以运行。
为了让动态连接器能成功的完成动态链接过程,在前面运行的连接编辑器需要在应用程序可执行文件中生成数个特殊的 sections,比方 .dynamic、.dynsym、.got和.plt等等。
ELF文件里面,每一个 sections 内都装载了性质属性都一样的内容,比方:
(1) .text section 里装载了可执行代码;
(2) .data section 里面装载了被初始化的数据;
(3) .bss section 里面装载了未被初始化的数据;
(4) 以 .rec 打头的 sections 里面装载了重定位条目;
(5) .symtab 或者 .dynsym section 里面装载了符号信息;
(6) .strtab 或者 .dynstr section 里面装载了字符串信息;
(7) 其他还有为满足不同目的所设置的section,比方满足调试的目的、满足动态链接与加载的目的等等。
动态链接库在ELF文件格式中与进程地址空间中的表现形式,如下如所示: