SA*****160 *丰
实验环境:
1.操作系统:VMware+Ubuntu 12.04 LTS(32-bit)
2.硬件平台:32位X86
实验要求:
1.编程实现fork(创建一个进程实体) -> exec(将ELF可执行文件内容加载到进程实体) -> running program
代码如下:
#include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void main() { char s[5]; int i; for(i=0; i<5; i++) { s[i]='\0'; } scanf("%s",s); char arr[12]; strcpy(arr,"/bin/");//shell命令一般都在bin目录下 strcat(arr,s); execl(arr,s,NULL); // execl("/bin/ls","ls",NULL); perror("execl"); wait(NULL); exit(1); }
执行结果是:
2.参照C代码中嵌入汇编代码示例及用汇编代码使用系统调用time示例分析fork和exec系统调用在内核中的执行过程
2.1 Linux的进程创建可以分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的
区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源的统计量。exec()函数负责读取可
执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果类似。
fork在内核中的执行过程
Linux通过clone()系统调用实现fork()。fork()、vfork()和_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用
do_fork()函数。do_fork()函数调用了copy_process()函数。
exec在内核中的执行过程
当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID
并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。
2.2 嵌入式汇编方式调用系统调用
3.注意task_struct进程控制块,ELF文件格式与进程地址空间的联系,注意Exec系统调用返回到用户态时EIP指向的位置
3.1 task_struct进程控制块
进程在操作系统中都有一个户口,用于表示这个进程。这个户口操作系统被称为PCB(进程控制块),在linux中具体实现是
task_struct数据结构(在linux/sched.h文件里定义),它记录了一下几个类型的信息:
a.状态信息,例如这个进程处于可执行状态,休眠,挂起等。
b.性质,由于unix有很多变种,进程有自己独特的性质。
c.资源,资源的链接比如内存,还有资源的限制和权限等。
d.组织,例如按照家族关系建立起来的树(父进程,子进程等)。
3.2 ELF文件格式与进程地址空间的联系
ELF为现在非常流行的可执行文件的格式,它为程序运行划分了两个段,一个段是可以执行的代码段,它是只读,可执行;
另一个段是数据段,它是可读写,不能执行。
将代码段加载到0x08048000的位置,其他的数据也根据规则加载即可。
3.3 Exec系统调用返回到用户态时EIP指向的位置
do_exec的工作较为复杂,它的主要目标是将一个可执行程序加载到当前进程中来,返回到用户态时EIP 指向可执行程序的入口位置
(即 0x08048000)。
4.动态链接库在ELF文件格式中与进程地址空间中的表现形式
连接器在可执行文件中标记出程序调用外部函数的位置,并不把代码复制进去,只是标出函数在动态连接库中的位置。用这样的方式生成的
特殊可执行文件就是动态连接的。在运行这种动态程序时,系统在运行时把该程序调用的外部函数地址映射到程序地址,这就是所谓的动态
连接,系统就有一个程序叫做动态连接器,在动态连接的程序执行前都要先把地址映射好。很显然的,必须有一种机制保证动态连接的程序
中的函数地址正确地指向了动态连接库的某个函数地址。这就需要讨论一下elf可执行文件格式处理动态连接的机制了。elf的动态连接库是内
存位置无关的 ,就是说你可以把这个库加载到内存的任何位置都没有影响。这就叫做 position independent 。而a.out的动态连接库是内存
位置有关的,它一定要被加载到规定的内存地址才能工作。在编译内存位置无关的动态连接库时,要给编译器加上-fpic选项,让编译器产生的
目标文件是内存位置无关的还会 尽量减少对变量引用时使用绝对地址。把库编译成内存位置无关会带来一些花费,编译器会保留一个寄存器
来指向全局偏移量表(global offset table (or GOT for short)),这就会导致编译器在优化代码时少了一个寄存器可以使用,但是在最坏
的情况下这种性能的减少只有3%,在其他情况下是大大小于3%的。
5.通过300-500字总结以上实验和分析所得,实验情况和分析的关键代码可以作为总结后面的附录以提供详细信息
通过本次实验我学到了a.进程的创建fork、将ELF可执行文件内容加载到进程实体exec;b.fork和exec在内核中的执行过程;c.进程控制块
task_struct的结构信息;d.ELF文件格式与进程地址空间的联系;e.动态链接库在ELF文件格式中与进程地址空间中的表现形式等。
附录:
1.fork是怎么找到do_fork的
2.进程的创建过程
http://blog.chinaunix.net/uid-25436678-id-3076217.html
3.老师给的参考:
fork和exec系统调用最终都是通过int 0x80软中断 + EAX寄存器(存储对应的系统调用号)进入内核,在内核中fork和exec对应找到
sys_fork/do_fork和sys_exec/do_exec。do_fork主要的工作就是创建一个新进程,创建的方法是拷贝当前进程、分配新的进程pid、
插入进程相关链表队列中等。 do_exec的工作较为复杂,它的主要目标是将一个可执行程序加载到当前进程中来,返回到用户态时EIP
指向可执行程序的入口位置(即 0x08048000)。 可执行程序的加载过程可以分为两种情况:一种是加载静态编译的ELF文件,只需要
将代码段加载到0x08048000的位置,其他的数据也根据规则加载即可;另一种情况更常见需要动态链接,ELF文件中说明了它所依赖
的其他动态链接库so文件(也是ELF文件格式),so文件还可以依赖其他的so文件,这就形成了一个以ELF可执行程序文件为根的依赖树,
这时加载过程比较复杂,大致是根据深度优先遍历整个依赖树加载所有so文件,还需要重定位动态链接等工作,最终返回到用户态。
ELF文件中的数据会加载到进程的地址空间中来,可以说ELF文件中的数据与进程的地址空间有种映射关系,不同的ELF文件格式是为了
便于存储,而进程地址空间中的程序数据是为了便于执行。