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

来源:互联网 发布:学科评估网络空间安全 编辑:程序博客网 时间:2024/05/19 11:35

学号:SA12226383   姓名:张泽尧

一、关于fork()与exec

在linux中主要通过fork()函数实现进程的创建,exec函数族实现可执行程序的加载。

fork():

fork( )函数通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在在于PID、PPID和某些资源和统计量(例如,挂起的信号,它没必要被继承)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值,而父进程中返回子进程ID。子进程是父进程的副本,它获得父进程数据空间、堆、栈等资源的副本。Linux将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。

fork()是通过clone( )实现的,clone( )再调用do_fork( ),具体过程如下:

1、为新进程创建一个内核栈、thread_info结构和task_struct 结构,这些结构的值与当前进程的值相同。此时子进程和父进程的描述符是完全相同。

2、子进程开始使自己与父进程区别开来。进程描述符内的许多成员要被清零或设为初始值。

3、子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证不会透入运行。

4、设置task_struct中的相应flags标志,例如权限

5、为新进程分配一个有效PID

6、根据传递给clone( )的参数标志进行拷贝或者共享打开的文件、文件系统信息、进程地址空间和命名空间等。

7、最后返回子进程的PID。

exec:

exec函数族描述一组函数,它们都以exec开头,分别是:

int execl(const char *path, const char *arg, ...);  int execlp(const char *file, const char *arg, ...);  int execle(const char *path, const char *arg, ..., 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[]); 
他们都是libc中经过包装的的库函数,最后通过系统调用execve()来实现。

exec函数族的作用是根据指定的文件名找到可执行文件,用它来取代当前进程的内容,并且这个取代是不可逆的,即被替换掉的内容不再保存,当可执行文件结束,整个进程也随之僵死。因为当前进程的代码段,数据段和堆栈等都已经被新的内容取代,所以exec函数族的函数执行成功后不会返回,失败是返回-1。(这里的可执行文件既可以是二进制文件,也可以是脚本文件。)

大致过程概述如下:

1、删除当前进程虚拟地址空间的用户部门已经存在的区域结构。
2、加载可执行文件,用可执行文件中的内容覆盖当前进程地址空间相应区域
3、设置程序计数器即eip中的值,使它指向新的代码区的入口点,调用启动代码,启动代码设置栈,控制传给新程序的主函数。

函数调用流程如下:

execve() -> sys_execve() -> do_execve() -> search_binary_handler() -> 与可执行文件类型相应的处理函数 -> 相应的load_binary()

其中sys_execve()是系统调用的入口,,系统调用的时候,把参数依次在:ebx,ecx,edx,esi,edi,ebp,eax寄存器,第一个参数为可执行文件路径,第二个参数为参数的个数,第三个参数为可执行文件对应的参数。

do_execve()首先会读入可执行文件,如果可执行文件不存在,会报错。然后对可执行文件的权限进行检查。如果文件不是当前用户是可执行的,则execve()会返回-1,报permission denied的错误。否则继续读入运行可执行文件时所需的信息。


下面是fork()与exec的使用范例:

1、fork()

/*********fork1.c*********/#include <stdlib.h>#include <stdio.h>#include <sys/types.h>#include <unistd.h>int main() {    pid_t pid;    int count = 13;    pid = fork();    if(pid == 0) {        sleep(5);        count = 31;    }    else if(pid > 0) {        wait(NULL);    }    else printf("Fork Failure\n");    printf("There are %d apples!\n", count);    exit(0);}
fork1.c输出如下:


父进程调用fork()生成子进程后执行wait(NULL)等待子进程执行完毕,子进程继承了父进程地址空间中的内容,如count,当子进程对count进行修改时,内核在物理内存上新开辟一块控件保存新count,子进程虚拟内存中count就映射到新开辟的物理内存,不影响父进程中的count,即父进程count依然为13.


/*********fork2.c*********/#include <sys/types.h>#include <unistd.h>int main() {    fork();    fork() && fork() || fork();    fork();    while(1){    }}

使程序在后台运行,查看生成的新进程的数量,如下:


共计生成了20个新进程。
前4个fork执行情况如下图(子进程中fork()返回0,父进程中fork()返回子进程id,非0),加粗圆代表子进程,非加粗代表父进程:


此时有10个进程,再执行完最后一个fork后即有了20个进程。

shell中可以用killall来“杀死”进程,如下



2、execl()

以下程序使用execl()来在进程中加载可执行程序。

/*********exe1.c********/#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <unistd.h>int main() {    pid_t p;    p = fork();    if(p == 0) {        execl("/bin/ls", "ls", "-l", NULL);        perror("execl");        exit(1);    }    else {        wait(NULL);        printf("father\n");    }    exit(0);}
程序加载了可执行程序ls,查看当前目录下文件



3、利用fork()与execl()实现一个简单的shell程序,识别简单的命令

/*******shelldemo.c********/#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <unistd.h>int main() {    char command[50];    char addr[100];    pid_t p;    while(1) {        printf("commond : ");        scanf("%s", command);        p = fork();        if(p == 0) {            strcpy(addr, "/bin/");            strcat(addr, command);            execl(addr, command, NULL);            perror("execl");            exit(1);        }        else if(p > 0) {            wait(NULL);        }        else printf("Fork Failure!\n");    }}
程序只简单地默认可执行程序位于/bin/目录下,未加入参数。下面以ps,ls为例输出结果:



二、task_struct进程控制块,ELF文件格式与进程地址空间的联系

在linux 中每一个进程都由task_struct 数据结构来定义。task_struct 就是我们通常所说的PCB.她是对进程控制的唯一手段也是最有效的手段. 当我们调用fork() 时, 系统会为我们产生一个task_struct 结构体。然后从父进程,那里继承一些数据, 并把新的进程插入到进程树中, 以待进行进程管理。task_struct的结构如下:



ELF文件格式如下:


ELF文件里面,每一个 sections 内都装载了性质属性都一样的内容,.text section 里装载了可执行代码; .data section 里面装载了被初始化的数据; .bss section 里面装载了未被初始化的数据;以 .rec 打头的 sections 里面装载了重定位条目; .symtab 或者 .dynsym section 里面装载了符号信息;.strtab 或者 .dynstr section 里面装载了字符串信息; 其他还有为满足不同目的所设置的section,比方满足调试的目的、满足动态链接与加载的目的等等。

进程地址空间中典型的存储区域分配情况如下:


从低地址到高地址分别为:代码段、(初始化)数据段、(未初始化)数据段(BSS)、堆、栈、命令行参数和环境变量。其中堆向高内存地址生长,栈向低内存地址生长。

task_struct进程控制块中的mm字段所指向的mm_struct结构描述了进程地址空间的信息,包括代码段、数据段、堆段、栈段所在地址空间里的起始和结束地址等信息。
ELF文件格式中的 ELF头部、段头部表、.init、.text、.rodata段对应进程地址空间中的代码段,在加载可执行文件时,会把它们映射到进程地址空间中的代码段区域。
ELF文件格式中的 .data、.bss段 对应 进程地址空间中的 数据段,在加载可执行文件时,会把它们映射到进程地址空间的数据段区域。


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

库有动态与静态两种,动态通常用.so为后缀,静态用.a为后缀。例如:libhello.so libhello.a 
为了在同一系统中使用不同版本的库,可以在库文件名后加上版本号为后缀,例如: libhello.so.1.0,由于程序连接默认以.so为文件后缀名。所以为了使用这些库,通常使用建立符号连接的方式。
ln -s libhello.so.1.0 libhello.so.1 
ln -s libhello.so.1 libhello.so 
当 要使用静态的程序库时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。然 而,对动态库而言,就不是这样。动态库会在执行程序内留下一个标记‘指明当程序执行时,首先必须载入这个库。由于动态库节省空间,linux下进行连接的 缺省操作是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接。

动态连接的本质,就是对ELF文件进行重定位和符号解析。
重定位可以使得ELF文件可以在任意的执行(普通程序在链接时会给定一个固定执行地址);符号解析,使得ELF文件可以引用动态数据(链接时不存在的数据)。
从流程上来说,我们只需要进行重定位。而符号解析,则是重定位流程的一个分支。

动态链接库在ELF文件中对应着.dynamic段所包含的信息:包括动态链接器所需要的相关信息,比如依赖于哪些共享对象(例如libc.so)、动态链接符号表位置、动态链接重定位表的位置、共享对象初始化代码的地址等信息。
动态链接库最后被映射到进程地址空间的共享库区域段。





原创粉丝点击