可执行文件的装载

    这里,我们从OS的角度来阐述一个可执行文件如何被装载,并且同时在进程中执行。一个可执行文件从装载到执行,最开始只需要做三件事情:
  • 创建一个独立的虚拟地址空间
  • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
  • 将CPU指令寄存器设置成可执行文件的入口地址,启动运行
Linux下,创建虚拟地址空间只是分配一个页目录就可以了,甚至不设置页映射关系。当程序发生页错误时,OS将从物理内存中分配一个物理页,然后将“缺页”读取到内存中,在设置缺页的虚拟页和物理页的映射关系。显然,当OS捕获到缺页错误时,它应知道程序目前所需要的页在可执行文件中的哪个位置。这就是虚拟空间与可执行文件之间的映射关系,也是传统意义上的“装载”过程。这种映射关系只是保存在操作系统内部的一个数据结构。Linux将进程虚拟空间中的一个段叫做虚拟内存区域。将CPU指令寄存器设置为可执行文件入口,从进程角度来看,这一步可以简单地认为OS执行了一条跳转指令。
    上述步骤执行完以后,其实可执行文件的真正指令和数据都没有被转载到内存中,操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟之间的数据结构而已。当CPU开始执行这个地址的指令时,发现该页面是个空页面,于是它就认为这是一个页错误。CPU将控制权交给OS,OS根据映射关系找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权交给进程继续执行。
    OS在装载时,只关心一些跟装载相关的问题,最主要的是段的权限。在ELF文件中,段的权限往往只有为数不多的几种组合:
  • 以代码段为代表的权限为可读可执行的段
  • 以数据段和BSS段为代表的权限为可读可写的段
  • 以只读数据段为代表的权限为只读的段
OS给出一个简单的方案,对于相同权限的段,把它们合并到一起当作一个段进行映射。ELF可执行文件引入了一个概念“segment”,一个“segment”包含一个或多个属性类似的“section”。这样做的好处是可以很明显的减少页面内部碎片,从而节省了内存空间。下面是一个很小的程序:
//SectionMapping

#include <stdlib.h>
int main()
{
    while(1)
        sleep(1000);
    return 0;
}
    
使用静态链接的方式将其编译成可执行文件,得到一个elf文件
$ gcc -static SectionMapping.c -o SectionMapping.elf
    使用readelf查看该elf文件的section: $ readelf -S SectionMapping.elf
    也可以使用readelf命令查看ELF的“Segment”,描述“segment”的结构叫程序头( Program Header),它描述了ELF文件该如何被OS映射到进程的虚拟空间: $ readelf -l SectionMapping.elf

    从装载的角度,我们只关心“LOAD”类型的Segment。Segment[00]是可读可执行的,统一被映射到VMA0;Segment[01]是可读写的,统一被映射到VMA1。
    
    操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间,基本原则是将相同权限属性的、有相同映像文件的映射成一个VMA;一个进程基本上可以分为如下几种VMA区域:
  • 代码VMA,权限可读、可执行;有映像文件
  • 数据VMA,权限可读写、不可执行;有映像文件
  • 堆VMA,权限可读写、不可执行;无映像文件,匿名,可向上扩展
  • 栈VMA,权限可读写、不可执行;无映像文件,匿名,可向下扩展
Linux下,我们可以通过查看/proc来查看进程的虚拟空间分布:
$ ./SectionMapping.elf &
[1] 3061
$ cat /proc/3061/maps

    我们从权限属性可以看出VMA1可执行,映射到代码段等;VMA2同VMA1有映像文件,可读写不可执行,VMA2映射到数据段等;VMA3为堆VMA;VMA4为栈VMA。

下面简介下Linux内核装载ELF过程:
    首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚启动的新进程结束,然后继续等待用户输入命令。
    在进入execve()系统调用之后,Linux内核就开始进行真正的装在工作。在内核中,execve()系统调用相应的入口时sys_execve(),内部调用do_execve()。do_execve()会首先查找被执行的文件,如果找到文件,则读取文件的前128字节,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装在处理过程。Linux中所有被支持的可执行文件格式都有相应的装在处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并调用相应的装载处理过程;这里我们只关心ELF可执行文件的装载,load_elf_binary(),它的主要套路是:
  1. 检查ELF可执行文件的有效性;
  2. 寻找动态链接的“.interp”段,设置动态链接路径
  3. 根据ELF的程序头表的描述,对ELF文件进行映射
  4. 初始化ELF进程环境
  5. 将系统调用的返回地址修改为ELF文件的入口点;入口点取决于程序的链接方式,静态链接ELF文件的头文件中e_entry所指的地址,动态链接ELF文件则入口点为动态链接器
当sys_execve()从内核态返回到用户态时,EIP寄存器直接跳转到ELF程序入口地址,程序开始执行,装载完成。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
.版本 2 .局部变量 段, 段, , "0" .局部变量 表目录, 表目录, , "0" .局部变量 现行位置 .局部变量 段数 .局部变量 段内存地址, , , "0" .局部变量 段占用文件大小 .局部变量 代码入口地址 .局部变量 表的数量, 整数型 .局部变量 计次, 整数型 .局部变量 计次2, 整数型 .局部变量 代码段地址, 整数型 .局部变量 文件对齐度 .局部变量 DOS数据, 字节集 .局部变量 首选装载地址, 整数型 .局部变量 控制台程序, 逻辑型 现行位置 = 取字节集数据 (文件, #整数型, #MZ头长度 - 3) + 1 ' DOS数据 = 取字节集间 (文件, #MZ头长度 + 1, 现行位置 - #MZ头长度 - 1) .如果真 (取字节集数据 (文件, #整数型, 现行位置) ≠ #PE署名) 返回 () .如果真结束 现行位置 = 现行位置 + 2 段数 = 取字节集数据 (文件, #短整数型, 现行位置) 重定义数组 (段, 假, 段数) 重定义数组 (段内存地址, 假, 段数) 现行位置 = 现行位置 + 32 代码入口地址 = 取字节集数据 (文件, #整数型, 现行位置) 代码段地址 = 取字节集数据 (文件, #整数型, 现行位置) 现行位置 = 现行位置 + 4 首选装载地址 = 取字节集数据 (文件, #整数型, 现行位置) 现行位置 = 现行位置 + 4 文件对齐度 = 取字节集数据 (文件, #整数型, 现行位置) 现行位置 = 现行位置 + 28 控制台程序 = 取字节集间 (文件, 现行位置, 2) = { 3, 0 } 现行位置 = 现行位置 + 24 表的数量 = 取字节集数据 (文件, #整数型, 现行位置) 重定义数组 (表目录, 假, 表的数量) .计次循环首 (表的数量, 计次) 表目录 [计次].地址 = 取字节集数据 (文件, #整数型, 现行位置) 表目录 [计次].大小 = 取字节集数据 (文件, #整数型, 现行位置) .计次循环尾 () .计次循环首 (段数, 计次) ' 段 [计次].名称 = 取字节集间 (文件, 现行位置, 8) 现行位置 = 现行位置 + 8 段 [计次].内存大小 = 取字节集数据 (文件, #整数型, 现行位置) 段内存地址 [计次] = 取字节集数据 (文件, #整数型, 现行位置) 段占用文件大小 = 取字节集数据 (文件, #整数型, 现行位置) 段 [计次].数据 = 字节集删尾空 (取字节集间 (文件, 取字节集数据 (文件, #整数型, 现行位置) + 1, 段占用文件大小)) 现行位置 = 现行位置 + 12 段 [计次].属性 = 取字节集数据 (文件, #整数型, 现行位置) .计次循环尾 () .计次循环首 (表的数量, 计次) .变量循环首 (段数, 1, -1, 计次2) .如果真 (表目录 [计次].地址 ≥ 段内存地址 [计次2]) 表目录 [计次].段号 = 计次2 表目录 [计次].地址 = 表目录 [计次].地址 - 段内存地址 [计次2] 跳出循环 () .如果真结束 .变量循环尾 () .计次循环尾 () .计次循环首 (段数, 计次) .如果真 (段内存地址 [计次] = 代码段地址) 代码入口地址 = 代码入口地址 - 代码段地址 生成PE文件 (首选装载地址, 段, 计次, 代码入口地址, 文件对齐度, 表目录, DOS数据, 控制台程序) 跳出循环 () .如果真结束 .计次循环尾 ()

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值