进程的创建与可执行程序的加载 Sk8er

学号:SA*****201

姓名:方超

一、进程控制块与地址空间的联系

  进程的地址空间也叫线性空间,由进程可寻址的虚拟内存组成。系统允许进程使用这种虚拟内存中的地址。在32位操作系统中,进程的地址空间有32位,即4GB的寻址范围,在64位系统中会更大。

  可执行程序分为若干的功能段,如代码段,数据段,堆栈段等等。相应的,系统会将进程的这些段放置在进程线性空间的一段内存区域上。进程只能访问有效内存区域内的内存地址。每个内存区域也具有相关权限,如对相关进程有可读,可写,可执行属性。若一个进程访问了不在有效范围内的内存区域,系统就会产生错误,如Linux中常见的段错误。

  Linux内核中用内存描述符来描述进程地址空间有关的全部信息,该结构的类型为mm_struct。每一个进程拥有一个该类型的变量。

  进程地址空间的每一内存区域都由一个类型为vm_area_struct的对象来表示,该类型的vm_start指向该内存区域中线性地址的起点。vm_end表示该内存区域在线性地址的终点。vm_end - vm_start表示该内存区域的长度。一个进程根据内存区域地址的递增顺序将vm_area_struct对象连接成一个单向链表,由vm_next成员来指向下一个内存区域。但是为了方便内核快速查找进程的内存区域,一个进程的vm_area_struct对象还被组织成一颗红黑树。也就是说,一个进程的所有内存区域在内核中即被组织成一个链表又被组织成了一颗红黑树。

  在进程描述符中的mm字段指向该进程的内存描述符。内存描述符中的mmap字段指向内存区域对象的链表头。

  组织如下图所示:   

  

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

  一个ELF格式的可执行文件有许多的功能段,那么将一个ELF格式程序映射到进程的地址空间就是依据这些段来划分内存区域的,但实际情况会稍微复杂一点。当一个ELF文件被映射到地址空间时,是以系统的页长度作为单位的,每个段在映射时的长度都应该是系统页长度的整数倍,有些段的长度会小于一个系统页的长度,但也会占用一个系统页,这就造成了内存的浪费。那么,系统是如何解决的呢?

  在ELF文件中,段的权限只有为数不多的几种组合:可读可执行的段,可读可写的段,只读的段。因此,系统可以将权限相同的段合并到一起当作一个内存区域来映射。这样,就会大大减小内存的浪费。

  ELF文件格式与进程地址空间的联系大致如下图所示:

三、fork系统调用

  在linux应用编程中,创建新进程运行ELF可执行文件是通过fork和exec函数相互配合来实现的,首先调用fork函数通过拷贝当前进程来创建一个子进程。之后在子进程中读取elf可执行文件并将其载入地址空间开始运行。

本次实验编写了一个简单的例子来使用fork和exec函数,代码和演示效果在附录中给出。

  fork系统调用:  

  fork()调用过程如下图所示

  

  在do_fork函数中又会copy_process函数。copy_process中完成了创建进程的大部分工作:

  1.   调用dup_task_struct函数来为新进程创建一个内核栈,thread_info和进程描述符结构。这些值与其父进程完全相同。
  2.   检查并确保用户拥有的进程数目没有超过限制。
  3.   初始化新进程的进程描述符中相关的成员。主要是统计信息等,大部分成员的值还是与父进程相同。
  4.   将子进程的状态设置为TASK_UNINTERRUPTIBLE。
  5.   调用copy_flags函数更新新进程的flags成员。
  6.   调用alloc_pid为新进程分配一个有效的PID。
  7.   根据传递给clone()参数标志,copy_process拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。
  8.   扫尾工作并返回一个指向子进程的指针。

  do_fork函数在调用完copy_process函数之后,会将新创建的进程插入其父进程的运行队列,并有意将其插入在父进程的前面。之后会唤醒并运行该进程。之所以要让子进程先于父进程执行,是因为内核中采用了写时拷贝技术,即父子进程会共享相同的页空间直到有一方需要写入时才拷贝新的页。一般子进程创建之后会立即调用exec函数来载入可执行程序,这样让子进程先于父进程运行可以避免多余的数据拷贝。

  exec系统调用:

  exec函数的功能是将ELF可执行文件所描述的进程上下文来代替当前进程的上下文,从而实现装载ELF可执行文件的工作。exec是一个函数族,包括execl,execlp,execle,execv,execvp,execve这六个函数,他们实现的功能想同,只是参数有稍许差异。exec族函数都是通过sys_execve系统调用来实现的。

  sys_execve会首先进行一些参数的检查和复制工作,之后会调用do_execve函数来完成主要的装载工作。

  do_execve函数首先会确认文件存在,然后检查可执行文件的类型,如果该可执行文件是ELF格式的,则会调用load_elf_binary函数来完成ELF可执行文件的装载处理工作。

  load_elf_binary的主要步骤是:

  1.   通过检查elf魔数以及文件头表等来检查ELF可执行文件的有效性
  2.   设置动态链接器路径
  3.   释放该进程占用的几乎所有资源,包括内存描述符,所有内存区域对象以及所有的页框,并清除页表,进程描述符的comm字段,浮点寄存器和TSS段,恢复所有默认的信号处理程序,关闭所有打开的文件。
  4.   根据ELF可执行文件的描述创建新的资源以及进程环境的初始化。
  5.   将系统调用的返回地址修改成ELF可执行文件的入口点。

  load_elf_binary返回至do_execve再返回至sys_execve时,由于返回地址已经被修改,因此返回至用户态后,EIP寄存器直接跳转至ELF程序的入口地址。至此,ELF可执行程序装载完成。

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

  在装载使用动态链接的ELF程序时,操作系统首先会启动动态连接器来完成程序的运行时链接任务。动态连接器是由ELF可执行文件中的.interp段来指定的,该段的内容就是一个简单的字符串。

  ELF可执行程序依赖的动态链接库保存在ELF的.dynamic段中。

  ELF可执行程序的动态链接库依然通过映射到进程地址空间的方式来完成程序的访问执行。

  在进程运行中通过察看该进程在proc文件系统中的内存布局文件就可以确定动态链接库的位置。

  

  使用readelf -d选项可以察看ELF可执行程序的dynamic段中动态链接库的信息。

  

附录:

1)fork和exec使用实验的代码及运行结果

fork.c

 1 #include<unistd.h>
 2 #include<stdlib.h>
 3 #include<stdio.h>
 4 
 5 int main()
 6 {
 7     int pid = fork();
 8     
 9     if(pid == 0)
10     {
11         printf("In child process\n");
12         execl("/bin/ls", "ls", "-l", NULL);
13         printf("Here should never be executed!\n");
14     }
15     else if(pid > 0)
16     {
17         printf("In parent process\n");
18         wait();
19         exit(0);
20     }
21     else
22     {
23         perror("fork error");
24     }
25 }

2)察看动态链接库在ELF可执行文件以及进程空间的表现形式的演示代码

helloworld.c

1 #include<unistd.h>
2 #include<stdio.h>
3 
4 int main()
5 {
6     printf("helloworld!\n");
7     sleep(-1);
8     return 0;
9 }

编译后的elf可执行程序的各个段信息

原文地址:https://www.cnblogs.com/f8915345/p/3107931.html