实验二 进程的创建与可执行程序的加载
来源:互联网 发布:学科评估网络空间安全 编辑:程序博客网 时间: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){ }}
使程序在后台运行,查看生成的新进程的数量,如下:
前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)、动态链接符号表位置、动态链接重定位表的位置、共享对象初始化代码的地址等信息。
动态链接库最后被映射到进程地址空间的共享库区域段。
- 【实验二】进程的创建与可执行程序的加载
- 实验二 进程的创建与可执行程序的加载
- 【实验二】进程的创建与可执行程序的加载
- 【实验二】进程的创建与可执行程序的加载
- 实验二:进程的创建与可执行程序的加载
- 实验二 进程的创建与可执行程序的加载
- Linux操作系统实验二:进程的创建与可执行程序的加载
- 实验二(进程的创建与可执行程序的加载)
- Linux操作系统分析(二)进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 进程的创建与可执行程序的加载
- 定时器2+串口发送
- Linux下的SPI总线驱动(一)
- std::vector : 用法与技巧 转
- 5、生成树协议原理及配置
- metasploit的SET的Credential Harvester Attack Method
- 实验二 进程的创建与可执行程序的加载
- struts2学习笔记
- stm32串口点灯
- Linux下的SPI总线驱动(二)
- [cocos2d-x学习之路]Cocos2d-x下载与安装
- stm32之定时器彻底研究
- MYSQL、SQL Server、Oracle数据库排序空值null问题及其解决办法
- 背景图片加链接
- stm32之DMA彻底研究