程序员的自我修养(5)可执行文件的装载


前言:

可执行文件(如PE格式的.exe或.dll)的装载过程是一个将磁盘上的程序加载到内存并准备执行的过程。

一、可执行文件的装载

1、程序与进程的区别

  • 程序:是一个静态的概念,它就是编译好的指令与数据的集合。
  • 进程:是一个动态的概念,它是程序运行的一个过程。

2、进程的建立

从操作系统的角度看,一个进程最关键的特殊是它拥有独立的虚拟地址空间,这使得它有别于其他进程。创建一个进程需要做下面三件事情:

  • 创建虚拟地址空间

一个虚拟空间是由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,创建虚拟空间实际上并不是创建空间而是创建映射函数需要的数据结构。这一步的映射函数是虚拟空间到物理内存的映射关系。

  • 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系

这一步所做的是虚拟空间与可执行文件的映射关系,当程序执行发生页错误时,操作系统从物理内存分配一个物理页,然后将缺页从磁盘读取到内存,再设置缺页的虚拟页和物理页的映射关系,这样程序才能正常运行。当操作系统捕捉到缺页错误后,应当知道程序当前需要的页在可执行文件中的哪个位置,这就是虚拟空间与可执行文件间的映射关系,这一步是整个装载过程最重要的步骤。

假设可执行文件只有一个代码段.text,它的虚拟地址是0x08048000,它在文件中的大小为0x000e1。由于虚拟存储是以页为单位,32位的操作系统页的大小一般为4096字节,由于该.text段大小不到一个页,考虑到内存对齐该段占用一个页。该可执行文件被装载后。可执行文件与进程虚拟地址空间的映射关系如下:

在这里插入图片描述

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

操作系统通过设置CPU的指令寄存器将控制权交给进程,由此进程开始执行。这一步看似简单,实际上在操作系统层面上比较复杂,它涉及内核堆栈与用户堆栈的切换、CPU运行权限的切换。不过从进程角度看这一步可以简单的认为操作系统执行了一系列跳转指令,直接跳转到可执行文件的入口地址。

3、页错误

上面的步骤执行完,其实可执行文件的真正指令与数据都没有装入内存,操作系统只是通过可执行文件的头部信息建立起可执行文件与进程虚存之间的映射关系。以上面的例子为例,页错误处理流程如下:

  • 程序入口地址是0x08048000,即刚好是.text段的起始位置,当CPU开始执行这个地址的指令时,发现0x08048000 - 0x08049000是一个空页,发出一个页错误
  • CPU将控制权交给操作系统,根据上面第二步建立的数据结构,计算出相应的页面在可执行文件中的偏移。
  • 在物理内存中分配一个物理页面,将该进程中的虚拟页与分配的物理页间建立映射关系,然后把控制权交给进程,进程从刚才页错误的位置重新开始执行。

在这里插入图片描述

4、进程虚拟空间分布

以下面的程序为例,分析下进程的虚拟地址空间,如下:

// util.cpp
#include "util.h"

int shared = 1;
int add(int a, int b) {
    return a + b;
}

// main.cpp
#include "util.h"

extern int shared;
int main()
{
    int a = 10, b = 20;
    shared = add(a, b);
    return 0;
}

PE文件有一个专门的数据结构叫PE头,用例记录各个段的信息。执行dumpbin /HEADERS main.exe查看可执行文件PE头,下面只列出了部分头信息,如下:

SECTION HEADER #1
   .text name                   // 段名称
   7DA83 virtual size           // 段在内存中的实际大小(可能比文件中的大小更大,例如 BSS 段)
    1000 virtual address (0000000140001000 to 000000014007EA82)  // 段在内存中的起始 RVA(Relative Virtual Address,相对虚拟地址)
   7DC00 size of raw data       // 段在文件中的大小(在磁盘上的占用空间)
     400 file pointer to raw data (00000400 to 0007DFFF)  // 段在 PE 文件中的偏移量(文件指针)
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         Execute Read

main.obj文件不需要被装载,它的.text段对应的RVA都是0,如下:

.text$mn name
       0 physical address
       0 virtual address
      2E size of raw data
     3D7 file pointer to raw data (000003D7 to 00000404)
     405 file pointer to relocation table
       0 file pointer to line numbers
       2 number of relocations
       0 number of line numbers
60500020 flags
         Code
         16 byte align
         Execute Read

5、Windows PE的装载

5.1、RVA(Relative Virtual Address)

PE里面有一个常见的术语叫RVA,它表示一个相对虚拟地址,是相对于PE文件的装载基地址的一个偏移地址。例如:一个PE文件被装到虚拟地址(VA)0x00400000,那么一个RVA为0x1000的地址就是0x00401000。每个PE文件在装载时都会有一个装载目标地址,这个地址就是所谓的基地址。由于PE文件被设计成可以装载到任意位置,所以这个基地址不是固定的,每次装载都有可能发生变化。

5.2、装载过程

装载一个PE可执行文件是个比ELF文件相对简单的过程:

  • 先读取文件的第一页,在这个页中,包含了DOS头PE文件头段表
  • 检查进程地址空间中,目标地址是否可用,如果不可用,则另外选一个装载地址。
  • 使用段表中提供的信息,将PE文件中所有的段一一映射到地址空间中的相应位置。
  • 如果装载地址不是目标地址,则进行Rebasing。
  • 装载所有PE文件所需要的DLL文件。
  • 对PE文件中的所有导入符号进行解析
  • 根据PE头中指定的参数,建立初始化栈和堆。
  • 建立主线程并且启动进程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值