6exe的装载与进程

  • exe只有装载到内存后才能被CPU执行
  • 早期程序装载简陋
    • 装载的基本过程是把程序从外存读取内存某位置
  • 随着硬件MMU诞生
    • 多进程、多用户、虚拟存储的OS出现后,
    • exe的装载变复杂

  • ELF文件在Linux下的装载过程
    • 看exe装载的本质到底
  • 先介绍进程的虚拟地址空间?
    • 为什么进程要有自己独立的虚地址空间?
  • 历史角度
    • 装载的几种方式
    • 覆盖装载、页映射
    • 还绍进程虚拟地址空间的分布情况
      • 代码段、数据段、BSS段、堆、栈分別在进程地址空间中怎么分布,位置和长度如何决定

6.1进程虚拟地址空间

  • 第1章回顾虚拟地址空间和地址映射
  • 基于这些现代的计算机硬件体系结构和操作系统的概念,
  • 逐步结合现实的系统,
  • 来分析这些概念是如何在实际中被应用的,
  • 并且影响到我们构建程序的方方面面

  • 程序(狭义上讲可执行文件)是静态概念,
    • 一些预先编译好的指令和数据集合的文件
  • 进程是动态
    • 是程序运行时的一个过程
    • 动态库叫做运行时( Runtime)也有一定含义。
  • 程序就是菜谱,CPU就是人,
    • 厨具则是计算机的其他硬件,
    • 炒菜的过程就是一个进程。
  • 计算机按程序的指示把输入数据加工成输出数据,
    • 菜谱指导着人把原料做成美味可口的菜肴。
  • 从这个比喻中还可扩大到更大范围
    • 一个程序能在两个CPU上执行等。

  • 每个程序被运行起来后,它将有自己独立的虚拟地址空间
    • 大小由计算机的硬件平台决定,CPU位数决定
  • 硬件决定地址空间的最大理论上限,即硬件的寻址空间大小,
    • 32位的硬件决定了虚拟地址空间的地址为4G;
    • 64位总共17179869184GB,
    • ,几乎是无限的,
  • 或许有一天我们会觉得64位的地址空间很小,
    • 就像我们现在觉得32位地址不够用一样。
    • 当人们第一次推出32位处理器的时候,很多人都在疑惑4GB这么大的地址空间有什么用

  • 判断C的指针所占的空间来计算虚拟地址空间的大小。
  • ,C指针大小的位数与虚拟空间的位数相同,
  • 32位平台下的指针为32位,
  • 64位平台下的指针为64位,即8字节。
  • 特殊情況下,这种规则不成立,
    • 早期的MSC的C语言分长指针、短指针和近指针,
    • 这是为了适应当时畸形处理器而设立的,
    • 现在基本可以不予考虑

  • 4GB虚拟空间,程序可任意用?
  • 程序运行时处OS监管,
    • OS为达到监控程序运行等一系列目的,
    • 进程的虚拟空间都在OS掌握
  • 进程只能用那些OS分配给进程的地址
    • 访问未经允许的空间,OS就捕获到这些访问,
    • 这种访问当作非法操作,强制结束进程
  • Windows
    • “进程因非法操作需要关闭”
  • Linux
    • “Segmentation fault”很多是
    • 因为进程访间了未经允许的地址

  • 默认Linux将虚拟地址空间
  • 如图6-1
6exe的装载与进程

  • 操作系统用:C000 0000列FFFF FFFF,共1GB。
  • 0000 0000开始到BFFF FFFF都给进程用。
  • 原则上进程最多可用3GB虚
  • 整个进程在执行的时候,
    • 所有代码、
    • 数据包括通过C语言 mallock申请的虚拟空间之和
    • 不可超3GB。
  • 3GB的虚拟空间有时候是不够用的,
  • 数据库系统、数值计算、图形图像处理、虚拟现实、游戏等程序需要占用的内存空间较大
  • 一本万利就用64位处理器,
    • 把虚拟地址空间扩展到17179869184G
  • 不是人人都能顺利地更换64位
  • 有很多现有的程序只能运行在32位处理器下。
  • 32位CPU能不能用超过4GB的空间?
  • 将在后面“PAE”一节中介绍。

  • 遗憾,进程并不能完全使用这3GB的虚拟空间,
  • 其中有一部分是预留给其他用途的,
  • 我们在后面还会提到。

  • Windows的进程虚拟地址空间划分是操作系统占用2GB,
  • 进程只剩下2GB。
  • Windows有个启动参数可将操作系统占用的虚拟地址空间减少到1GB
  • 即Linux一样。
  • 系统盘根目录下的 Boot.ini.,加上“/3G”

6exe的装载与进程

PAE

  • 32位的CPU下,程序使用的空间能不能超过4GB呢?
  • 如果是指處拟地址空间,那么答案是“否”。
  • 32位的CPU只能使用32位的指针,它最大的寻址范围是0到4GB;
  • 计算机的内存空间,那么答案为“是”。
  • Intel从95年的 Pentium Pro CPU开始采用36位物理地址,
    • 可访问高达64GB的物理内存。

  • 硬件层面上讲,
    • 32位地址线只能访问最多4GB物内
    • 扩展至36位地址线后,
    • Intel修改页映射的方式,
    • 使新的映射方式可以访问到更多的物理内存。
  • Physical Address Extension)

  • 当然扩展的物理地址空间,对于普通应用程序来说正常情况下感觉不到它的存在,这是操作系统的事,应用程序里,只有32位的虚拟地址空间。

  • 应用程序该如何使用这些大于常规的内存空间?

  • 操作系统提供一个窗口映射的方法,把这些额外的内存映射到进程地址空间中

  • 应用程序可根据需要来选掙申请和映射,

  • 一个应用程序中1000 0000 2000 0000这段256MB的虛拟地址空间做窗口

  • 程序可从高于4GB的物理空间中申请多个大小为256MB的物理空间,

  • 编成A、B、C等,

  • 根据需要将这个窗口映射到不同的物理空间块,

  • 用到A时将0x1000000 0x200000映射到A,

  • 用到B、C时再映射过去,如此重复

  • Windows,这种访问内存的操作方式叫做AWE( Address Windowing Extensions):

  • 像Linux等UNX类操作系统则用mmap系统调用来实现

  • 补救32位地址空间不够大时的非常规手段,
    • 真正的解决方法还应用64位的处理器和操作系统
  • DOS时代16位地址不够用时,
    • 也用类似的16位CPU字长,20位地址线长度,
    • 系统有着640KB、1MB等诸多访问限制
  • 很多应用程序须访问超过IMB的内存,
    • 当时也有很多类似PAE和AWE的方法,
    • 当时著名的XMS( extended Memory Specification)

  • Windows下的PAE和AWE可用与/3G相似的启动选项/PAE和AWE打开

6.2装载的方式

  • 程序执行时所需的指令和数据必须在内存中才能正常运行,
  • 最简单的办法就是将程序运行所需要的指令和数据全都装入内存,
  • 这样程序就可以顺利运行,这就是最简单的静态装入。
  • 但是很多情況下程序所需要的内存数量大于物理内存的数量,当内存的数量不够时,根本的解决办法就是添加内存。
  • 相对于磁盘,内存昂贵,自计算机磁盘诞生以来一直如此。
  • 希望能够在不添加内存的情况下让更多的程序运行,尽可能有效地利用内存。
  • 后来发现,程序运行时是有局部性原理的,
    • 可将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盐,这就是动态装入的基本原理。

  • 覆盖装入和页映射典型的动态装载方法,
    • 思想都差不多,原则上都利用程序的局部性原理。
  • 动态装入的思想是程序用到哪个模块,就将哪个模块裝入内存,如果不用就暂时不装入,存放在磁盘中

6exe的装载与进程

6.2.1覆盖装入

  • 没发明虚拟存储前广泛,现在淘汰。
  • 被虚拟存储惯坏了的现代PC机程序员不屑一顾,
    • 它在计算机发展的初期的确为程序能够在内存受限的机器下正常运行提供了一种解决方案
  • 思想还很有意义的
  • 在一些现代嵌入式的内存受限环境下,特别是诸如DSP,
    • 这种方法或许还有用武之地。

  • 覆盖装入的方法把挖掘内存潜力的任务交给了程序员,程序员在编写程序的时候必须手工将程序分割成若干块,
  • 写一个小的辅助代码来管理这些模块何时应该驻留内存而何时应该被替换掉。
  • 小的辅助代码就是所谓的覆盖管理器。
  • 一个程序有主模块“main”,
  • main分别调模块A和模块B,A和B间不相互调
  • 三模块1024、512和256
  • 不考虑内存对齐、装载地址限制
    • 理论上运行这程序要1792字节内存
    • 用覆盖装入,内存中这样安排
6exe的装载与进程
  • 把模块A和模块B在内存中“相互覆盖”,
  • 两个模块共享块内存区域。
  • main调A时,覆盖管理器保证将A从文件中读入内存;
  • main调用B时,则覆盖管理器将模块B从文件中读入内存,
    • 由于这时模块A不会被使用,那么模块B可以装入到原来模块A所占用的内存空间
  • 除了覆盖管理器,整个程序运行只需1536,节省256字节。
  • 覆盖管理器本身往往很小,从数十字节到数百字节不等,一般都常驻内存。

  • 程序往往不止两个模块,模块间的调用关系也比上面的例子复杂。
  • 多模块情况下,程序员需要手工将模块按照它们之间的调用依赖关系组织成树状结构

  • 按照图6-3的组织关系,
  • main依赖于A和B,模块A依赖于C和D;
  • B依赖E和F,则它们在内存中的覆盖方式如图。
  • 这个程序的运行方式与前面的例子大同小异,值得注意的是,覆盖管理器需要保证两点。

有点没写啊

6.3 OS角度看可执行文件的装载

  • 页映射的动态装入的方式看到,可执行文件中的页可能被装入内存中的任意页。
  • 程序需要P4时,它可能会被装入FO~F3这4个页中的任意1个
  • 如果程序用物理地址直接操作,那每次页被装入时都需要重定位
  • 第1章中提到,在虚存中,现代的硬件MMU都提供地址转换
  • 有了硬件的地址转换和页映射机制,
    • OS动态加载可执行文件的方式跟静态加载有了很大区别

  • 各种可执行文件的装载过程的描述,
  • 大致能够明白这个过程,但似乎还有一层迷雾阻隔,
  • 本节站在OS角度来
    • 阐述一个可执行文件如何被装载,
    • 且同时在进程中执行

6.3.1进程的建立

  • 从OS角度看
  • 一个进程最关键的特征是它拥有独立的虚拟地址空间,这使得它有别于其他进程
  • 一个程序被执行同时都伴随着一个新进程的创建
  • 最通常的情形:
    • 创建一进程,然后装载相应的可执行文件且执行
    • 有虚存储的情况,上述过程最开始只需做三件事
  • 创一个独立的虚拟地址空间。
  • 读取可执行文件头,且建立颹拟空间与可执行文件的映射关系
  • 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。

  • 回忆第1章的页映射机制,一个虚拟空间
    • 由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,
  • 创建一个虚拟空间并不是创建空间
    • 而是创建映射函数所需要的相应的数据结构,
    • 在i386的Linux下,
    • 创建虚拟地空间实际上只是分配一个页目录就可以,
    • 甚至不设置页映射关系,
  • 映射关系等到后面程序发生页错误的时候再设置