您的位置:首页 > 其它

进程的创建与可执行文件的加载

2013-05-30 22:12 330 查看
1、进程初探

1.1 fork函数

我们先来看下关于fork函数的一个程序,代码如下:



上述程序的运行结果为:



通过上述实验结果,程序先运行了子进程,之后运行了父进程。函数fork()用于创建一个新进程,而这个进程几乎是当前进程的一个拷贝。父子进程使用相同的代码段,子进程复制父进程的堆栈段和代码段。fork函数一次调用,两次返回。对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零。Linux系统在拷贝时,采用了一种称为“写时拷贝”的技术。读者如果有兴趣的话,可以把上述的代码改下,将父进程以及子进程中的printf多打印几次,运行时基本上每次的情况都不相同,这就是多进程。文章中就不在演示。

1.2 wait()函数

wait和waitpid()可以用于使父进程等待子进程结束,以下是wait函数使用的一个程序。



程序运行结果如下:



不管运行多少次程序,程序结果都不会改变,即父进程等待子进程终止,而在子进程中,count为31,父进程中count为13。虽然子进程几乎是父进程的拷贝,它继承了父进程的一切数据,但是子进程一旦开始运行,实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。

1.3 exec 函数族

exec指的是一组函数,一共有6个,分别是:
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 *filhe, car *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件,如果不是可以执行的文件,那么就解释成为一个shell文件,sh
**执行!
将fork函数的那段程序稍微修改下,在子进程用使用execl函数,当子进程运行时,会直接打开vi编辑器。源代码如下:



2、fork和exec在内核中执行过程
2.1 linux系统调用
在X86下,系统调用由0x80中断完成,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号,比如EAX=1表示退出进程(exit);EAX=2表示创建进程(fork)等,每个系统调用都对应于内核源代码中的一个函数,它们都是以"sys_"开头的,比如fork调用对应内核中的sys_exit函数。当系统调用返回时,EAX又作为调用结果的返回值。
2.2 特权级与中断
现代操作系统中,通常有两种特权级别,分别为用户模式和内核模式,我们也将之称为用户态和和内核态。而程序中对fork或者exec调用,正是基于这两种状态之间的切换,以及如何在这两种不同的模式下正确的调用相对的函数。而我们一般是通过中断来实现从用户态切换到内核态。中断的这个过程程序都需要做哪些工作呢?以下是中断过程计算机要要完成的工作:

1,确定与中断或者异常关联的向量i(0~255)

2,读idtr寄存器指向的IDT表中的第i项

3,从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符

4,确定中断是由授权的发生源发出的。

中断:中断处理程序的特权不能低于引起中断的程序的特权(对应GDT表项中的DPL vs CS寄存器中的CPL)
编程异常:还需比较CPL与对应IDT表项中的DPL

5,检查是否发生了特权级的变化,一般指是否由用户态陷入了内核态。

如果是由用户态陷入了内核态,控制单元必须开始使用与新的特权级相关的堆栈

a,读tr寄存器,访问运行进程的tss段

b,用与新特权级相关的栈段和栈指针装载ss和esp寄存器。这些值可以在进程的tss段中找到

c,在新的栈中保存ss和esp以前的值,这些值指明了与旧特权级相关的栈的逻辑地址

6,若发生的是故障,用引起异常的指令地址修改cs和eip寄存器的值,以使得这条指令在异常处理结束后能被再次执行

7,在栈中保存eflags、cs和eip的内容

8,如果异常产生一个硬件出错码,则将它保存在栈中

9,装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这对寄存器值给出中断或者异常处理程序的第一条指定的逻辑地址

我们用图来描绘下Linux系统调用fork函数的执行流程:



触发中断之后,如果系统调用带有参数,那么参数会通过EBX、ECX、EDX、ESI、EDI和EBP这6个寄存器来传递,X86下Linux支持的系统调用参数至多有六个。CPU执行到int $0x80时,会保存现在以便恢复,然后由将特权状态切换到内核态。然后CPU会查找中断向量表中的第0x80号中断。但在执行第0x80号中断所对应的函数之前,CPU首先还要进行栈的切换。在Linux中,用户态和内核态使用的是不同的栈,而上述两种状态切换时,程序的当前栈由用户栈切换到内核栈。从中断处理函数中返回时,程序当前栈从内核栈切换回用户栈。

而返回时,须调用iret指令来回到用户态,iret指令则会从内核栈中弹出寄存器SS、ESP、EFLAGS、CS、EIP的值,是的栈恢复到用户态的状态。以下是图示:



3、ELF文件格式与进程地址空间的联系

3.1 ELF文件

ELF文件,或者我们称之为目标文件中包含了3个段和1个“文件头”,程序编译后的机器指令经常被放在代码段(.text或者.code),全局变量和局部静态变量经常放在数据段(.data)。BBS段,则是为未初始化的全局变量和局部静态变量预留了位置。而数据段中则存放已初始化的全局变量和局部静态变量。Header(文件头)则描述了整个文件的文件属性等信息。其实ELF不仅仅只有上述的几个段,除了那些之外,还有只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note GNU-stack)等,大概图示如下:



3.2 进程空间

进程运行时,需要占用一定的内存空间。地址空间有两种:物理地址空间和虚拟地址空间。物理地址空间是实实在在的,而虚拟内存是指虚拟的,其实它并不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,这样就能够有效地隔离进程。虚拟存储有多种实现方式。我们常听到的分页式、分段式和段页式等,需要一个硬件来实现映射。对于32位的计算机来说,Linux中高地址的1GB属于操作系统,而低地址的3GB,则留给了用户进程。

3.3 两者的联系

一个进程最关键的特征是它拥有独立的虚拟地址空间,使得它有别于其他的地址空间,这在task_struct中通过process ID来标识不同的进程。创建一个进程,然后装载相应的可执行文件并且执行。在有虚拟存储的情况下,上述过程最开始只需要做三件事情。

1)创建一个独立的虚拟地址空间。

2)读取可执行文件头,并且建立虚拟空间与可执行文件的映射。

3)将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。

我们来看下可执行文件在装载时如何被映射成虚拟空间。如果系统捕捉到缺页异常时,它应当知道程序当前所需要的页在可执行文件中的哪一个位置。这就是虚拟空间与可执行文件之间的映射关系。我们考虑如下情况,假设我们的ELF可执行文件只有一个代码段“.text”,它的虚拟地址为0X08048000,它在文件中的大小为0x000e1,对齐为0x1000.由于虚拟存储的页映射都是以页为单位的,在32位计算机中一般为4K,所以32为的ELF的对齐粒度为0x100。那么ELF需占用一个段,可执行文件与执行该可执行文件进程的虚拟空间映射关系如下所示:



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

4.1 动态链接库与ELF文件格式

ELF文件格式中有个段表,它描述了ELF各个段的信息。它是以一个“Elf32_Shdr”结构体为元素的数组。数组的元素个数等于等于段的个数,每个“Elf32_Shdr”结构体对应一个段。“Elf32_Shdr”又被称为段描述符。Elf32_Shdr段描述符结构如下:



对于上述结构中的sh_addr,如果该段可以被加载,则sh_addr为该段被加载后在进程地址空间中的虚拟地址;否则sh_addr为0。而如果段的类型是与链接相关的(不论是动态链接还是静态的),比如重定位表、符号表等,那么sh_link、sh_info两个是有意义的,对于其他段则没有意义。如果有ELF文件中有“.rel.text”的段,那么它是一个重定位表。链接器在处理目标文件时,须要对目标文件中某些部位进行重定位。而动态链接时,对那些组成程序的目标文件链接时,需等到程序运行时。这时候动态链接器会将程序所需要的所有动态链接库装载到进程的地址空间,并且将程序中所有为决议的符号绑定到相应的动态链接库中,并进行重定位工作。

4.2 动态链接库与进程地址空间

动态链接库和程序,它们都是被操作系统用同样的方法映射至进程的虚拟空间,只是它们占据的虚拟地址和长度不同。而动态链接器会与普通共享对象一样被映射到进程的地址空间。而且,使用同一个动态链接库时,只会在内存中保留一份副本,然后进行重定位。这样可以解决了共享的目标文件多个副本浪费磁盘和空间的问题,在内存中共享一个目标动态库的话,还可以减少物理页面的换入换出,也可以增加CPU缓存的命中率。

总结:

linux中产生一个新的进程,使用fork函数,它几乎是当前进程的拷贝,但只有运行到子进程时,它才会复制,这是一种写时拷贝的技术。父子进程拥有自己各自的堆栈,它们之间的数据不再共享。它们也是多进程的一种体现。exec是一系列的函数簇,用fork创建了子进程之后,如果想在子进程中运行新的程序,则可以调用exec系列函数。当进程调用exec系列函数中的任意一个时,该进程代码段、数据段内容完全由新程序替代。因为调用exec并不创建新进程,所以前后的进程号等相关信息并不发生变化。exec只是用新程序替换了当前进程的正文、数据、堆和栈段。

系统调用fork和exec函数时,系统会产生中断,执行代码时,将会从用户态切换到核心态。int $0x80号系统调用,将参数存放至寄存器中,并且保存现场。在内核态栈中,硬件会自动保存SS、ESP、EFLAGS等的值,然后执行中断服务程序,查找系统调用表,系统调用完成后,返回,并且恢复ESP、SS的值,恢复现场等一系列操作。

至于ELF文件格式与进程之间的联系,ELF文件中各个段的作用与进程虚拟存储以及到内存空间时是如何映射是至关重要的,以及task_struct如何标识各个进程,以及进程的状态等。动态链接库与ELF文件中段结构体,要使用到动态链接时,结构体中哪个成员至关重要。还有,动态链接库会像普通的程序那样被OS映射到进程空间中。

参考资料
[1]《程序员的自我修养-——链接、装载雨库》 俞甲子,石凡,潘爱民.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: