读书笔记-程序员的自我修养(六)

可执行文件的装载与进程

前言:源文件经过编译,链接过程,生成了目标可执行文件,可执行文件只有装载到内存以后才能被CPU执行

  • 程序进程:前者理解为一些预先编译好的指令和数据集合的一个 文件,是一个 静态 的概念,后者理解为程序运行时的一个 过程,是一个 动态 的概念

  • 每个进程都被操作系统分配以 虚拟的地址空间(Virtual Address Space) 来供运行时分配使用,如果被操作系统捕捉到进程非法访问了额外控件,将当做 非法操作强制结束进程,常见的 Windows:”进程因非法操作需要关闭” 和 Linux:Segmentation fault

  • 限制: 对于32位系统而言,允许分配给进程最大为:4GB的地址空间使用,而64位系统,则17179869184GB,即使 4GB,操作系统也会分配占用之后,最终留给进程最大3GB的地址空间使用,再通俗一点:整个进程在执行的时候,所有代码、数据包括通过 C 语言 malloc() 等方法申请的虚拟空间之和不得超过 3GB

  • PAE(Physical Address Extension)和 AWE(Address Windowing Extension):Intel 扩展了地址线,由 对应的32根(32位)扩展到了 36根 地址线,为此 多出来了256MB的物理空间,使用 窗口映射 技术,变向的增大了内存空间,进而也打破了 3GB 的限制,作为一种补救地址空间不够大的非常规手段

  • 装载的方式

    • 原理:程序执行时所需要的指令和数据必须 在内存中 才能够正常运行

    • 装载的方法:都是利用了 程序局部性 的原理,即某一时刻程序只用到了局部的一些执行和数据,并非全局

      • 以时间换取空间(淘汰):覆盖装入(Overlay)

      • 页映射(Paging),通常32位的Intel都是用4096字节的页作为页长,假设内存为16K,那么会被分为4个页,假设程序需要32K的内存,即需要8个页的空间,那么如何在4个页中不影响程序执行的情况下,自如装载8个页呢? 内存页的分配算法

        • 先进先出算法(FIFO

        • 最少使用算法(LUR

  • 操作系统角度 看可执行文件的装载,干了三件事

    • 创建 一个独立的虚拟地址空间(这也是进程最关键的特征,实质上是创建映射函数所需要的相应的数据结构)

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

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

    • 操作系统通过不断的 页错误 来不停地将程序通过虚拟地址管理器装载到物理内存,进程才得以正常运行

  • 进程虚拟空间 分布

    • 区分概念 的关系:ELF段在映射时长度应该都是系统页长度的 整数倍,如果不是,那么多余部分也会占用一个页,进而造成 浪费,实际上,操作系统装载可执行文件时,只会根据段的 权限 来区分,对于相同权限的段,把它们合并到一起当做一个段来映射

    • 普及概念 Linux 中将进程虚拟空间中的一个段叫做 虚拟内存区域(VMA,Virtual Memory Area),Windows 叫做 虚拟段(Virtual Section)

    • 两次合并第一次是链接器把各个目标文件中的相同段(Section,链接视图)合并,统一存放于可执行文件中,第二次是操作系统按照权限把可执行文件中的各个段(Segement,执行视图)合并,映射到虚拟内存区域中

    • 核心操作系统 通过给进程空间划分出一个个 VMA管理 进程的虚拟空间,基本原则是将相同 权限 属性的、有相同 映像文件 的映射成一个VMA,一个进程基本上可以分为如下几 VMA区域:

      • 代码VMA,权限只读、可执行;有映像文件

      • 数据VMA,权限可读写,可执行;有映像文件

      • 堆VMA,权限可读写、可执行;无映像文件,匿名,可向上扩展

      • 栈VMA,权限可读写、不可执行;无映像文件,匿名,可向下扩展

    • 一些限制

      • 堆的 最大 申请数量:受操作系统版本、程序本身大小、用到的动态/共享库数量、大小、程序栈数量、大小等因素影响(随机地址空间分布技术

      • 段地址 对齐:受页和段的长度 整数倍映射 问题,Unix采取了 段合并 映射的方法:各个段接壤部分共享一个物理页面

      • 进程栈 初始化:进程初始化启动之前,一些系统环境变量和进程的运行 参数 会提前保存到 Stack VMA 中,程序的库部分会把堆栈里面的初始化信息中的参数信息传递给 main()argcargv 两个参数

  • 大致了解:Linux 内核装载 ELF 过程简介

    1. bash 进程会调用 fork() 系统调用创建一个新的进程,然后新的进程调用 execve() 系统调用加载执行指定的 ELF 文件
    2. 检查 ELF 可执行文件格式的有效性,比如魔数、程序头表中段的数量
    3. 寻找动态链接的 .interp 段,设置动态链接器路径
    4. 根据 ELF 可执行文件的程序头表的描述,对 ELF 文件进行映射,比如代码、数据、只读数据
    5. 初始化 ELF 进程环境,动态链接准备
    6. 将系统调用的返回地址修改成 ELF 可执行文件的入口点
  • 大致了解:Windows PE 的装载 过程简介

    1. 先读取文件的第一个页,包含了 DOS 头、PE文件头 和 段表
    2. 检查进程地址空间中,目标地址是否可用
    3. 使用段表中提供的信息,将 PE 文件中所有的段一一映射到地址空间中相应的位置
    4. 如果装载地址不是目标地址,则进行 Rebasing
    5. 装载所有 PE 文件所需要的 DLL 文件
    6. 对 PE 文件中的所有导入符号进行解析
    7. 根据 PE 头中制定的参数,建立初始化栈和堆
    8. 建立主线程并启动进程
如需转载,请注明出处