可执行文件(ELF)的装载与进程

程序员的自我修养

可执行文件的装载与进程

进程虚拟地址空间

  • 什么是程序?什么是进程?

    • 程序是一个静态的概念,它就是一些预先编译好的指令和数据的集合
    • 进程是一个动态的概念.它是程序运行时的一个过程
    • CPU比作是人, 程序比作是菜谱, 硬件等资源比作是菜,厨具之类的东西.
    • 进程就是整个炒菜的过程 计算机安装程序的指示把输入数据加工成输出数据, 就好像厨师按照菜谱指导人把原料做成美味的菜一样
  • 每个进程都有自己独立的虚拟地址空间, 进程只能使用操作系统分配的地址空间内的地址

    • 如果访问未经允许的空间, 操作系统就会捕获到这些访问, 将进程强制结束, 比如Windows 进程因为非法操作需要关闭 , Linux 的Segment fault等
  • 在Linux下

    • 0XC0000000是操作系统和用户进程地址空间的划分线 ,系统用了1GB, 进程可用的3GB

      • 那如果进程想跑一个3GB以上的怎么办?或者说程序使用的空间能不能大于3GB呢?

        • 从虚存的角度来说是不行的

        • 从实际的内存来说是可以的

          • windows下有个叫PAE AWE的东西
          • 可以从高于4GB的内存空间里申请ABCD等多块物理空间, 然后根据需要把某段虚存映射到这不同的ABCD块

装载的方式

  • 最简单的办法就是把程序和数据全部存入内存中, 这就是静态装载

  • 根据程序局部性原理, 还可以把程序最常用的部分驻留在内存中 ,不常用的放在 硬盘上 这就是动态装载

    • 动态装载分成 覆盖装入overlay和页映射Paging

      • 覆盖装入是上古时期的产物了,程序员在写代码的时候要手动替换模块, 而且要思考清楚 模块的依赖关系, 最后可以用树 这种数据结构来描述

        • 子主题 1
      • 页映射

        • 可以说Modern OS都是采用这种方式

          • 页就是由操作系统的存储管理器来做一个 装载管理器的工作,
          • 由MMU来完成虚拟地址转换成物理地址的过程
          • 把一般为4KB 也就是0x00001000大小段的页 读进内存
          • 当然内存满了之后有替换算法, 比如FIFO,之类的

从操作系统的角度来看 可执行文件的装载

  • 进程的建立

    • 一个进程最关键的特点是 它拥有独立的虚拟地址空间
  • 进程建立的三个步骤

    • 1.创建一个独立的虚拟地址空间

      • 实际上很简单, 就是操作系统给你分配了一个页目录(Page Directory)
    • 2.读取可执行文件的头部 ,做好可执行文件ELF和虚拟地址空间的映射,

      • 首先回忆一下缺页中断会发生什么

        • 操作系统首先从空闲的物理内存中分配一个物理页, 然后我们就是要加载磁盘上的页到这个物理页上,最后设置好这个物理页的物理地址和虚拟地址的关系
        • 那么问题来了, 我们怎么知道程序当前需要的页到底在什么位置呢? 这正是第2点 , 可执行文件和虚存映射要做的事情
      • 实际上看图, 这种映射关系被保存在操作系统内部的一个数据结构 叫VMA(virtual Memory Area)

        • 比如操作系统创建进程后 会在进程相应的数据结构里设置一个对应.text段的VMA, 这个VMA还会带有一些权限的限制, 比如只读, 后续我们还会进行合并

        • 实际上操作系统发生段错误的时候, 通过查找这样的数据结构来定位页错误在可执行文件中的位置, 从而可以把正确的可执行文件的页加载进来

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

      • 这步其实最简单, 通过设置CPU的指令寄存器将CPU时间片交给进程,

        • 在操作系统层面比较复杂

          • 涉及到内核堆栈和用户堆栈的切换, CPU运行权限的切换
        • 不过对程序来说,

          • 不就是执行了一条跳转指令吗, 跳到ELF文件的入口地址
  • 页错误

    • 再重复一下刚才的过程, 就当是总结了吧

      • 比如那个入口地址是0x08048000, 执行是发现页面0x08048000- 0x08049000是个空页面, 这时候触发缺页中断, CPU将控制权交给OS, OS查询那个VMA ,然后计算出对应ELF文件的偏移, 然后找一个空闲的物理地址, 建立好虚存和物理内存的映射关系(应该是由MMU)来完成的 ,最后回到进程刚才page fault的地方继续执行
  • 进程虚存分布

    • 刚才说的虚存和ELF文件的映射关系会产生碎片的问题, 而你站在操作系统的角度来看它其实并不关系这虚存对应的到底是.bss段还是.text段 ,操作系统只关心这些段的权限问题(read write exec)

      • 所以把相同权限的section合并成一个虚存段segment是一个很自然的想法

        • 子主题 1
      • 这样做的好处是显著减少了页面内部碎片, 从而节省了内存空间

      • 其实无非就是虚存的segment合并了 ELF的几个section罢了

        • 一般ELF会分成两个段

          • VMA0
          • VMA1
  • 堆和栈

    • 首先在linux下可以 cat /proc/21963/map

      • 这个可以看到究竟划分成了几个段

      • 子主题 3

      • 一般来说是5个

        • VMA0

        • VMA1

        • stack VMA

        • heap VMA

        • vdso

          • 这个地址是属于大于0xC0000000的, 也就是属于内核的地址了
          • 这个是进程可以用来访问内核, 做一些通信
    • 进程除了那些segement之外还有自己的stack, 和Heap

    • 每个线程都有属于自己的堆栈

      • 比如这个进程的heap 140KB, stack 88KB

      • 那如果是单线程的话

        • 整个heap都是这个线程的
    • 堆在linux下理论3GB, 实际大概可以2.9GB

      • windows

        • 理论2G

          • 实际大概1.5G

进程虚存空间分布

  • ELF文件链接视图和执行视图

    • 操作系统并不关心可执行文件各个段的内容, 值只关心和装载相关的问题, 最主要是段的权限(可读, 可写 ,可执行)
    • 子主题 2
  • 进程栈初始化

    • 进程刚启动的时候, 必须知道一些进程运行的环境, 最基本的就是环境变量和 进程的运行参数(argc, argv)
    • 子主题 2
    • 进程启动 以后, 程序的库部分会把堆栈里的初始化信息中的参数信息传给main函数, 也就是我们熟知的argc和argv

Linux内核装载ELF过程简介

  • 首先在用户层面,bash进程会调用 fork系统调用创建一个新的进程, 然后新的进程调用execve()系统调用 执行指定的ELF文件, 原先的bash进程 返回继续等待过程启动的新进程结束, 然后继续等待用户输入命令

    • execve()在unistd.h
  • minibash

  • 在进入execve系统调用后, Linux内核开始进入真正的装载工作.

    • 在内核中,execve系统调用相应的入口是sys_execve()
    • 在进行一些参数的复制后, 调用do_execve()
    • do_execve()会先查找被执行的文件, 如果找到了, 读前128个字节,
    • 因为linux支持的可执行文件不止一种, a.out java等
    • 我们通过魔数来判断究竟是哪种可执行文件
    • 当do_execve()读取了128个byte后, 调用search_binary_handle()去搜索和匹配 合适的 可执行文件装载处理过程
    • 比如ELF可执行文件对应的装载过程的函数 名叫 load_elf_binary
    • a.out叫 load_aout_binary
    • 脚本类叫 load_script_binary
  • load_elf_binary

    • 1.检查文件有效性 比如魔数, segment数量

    • 2.寻找.interp段, 设置动态链接器路径

    • 3.根据ELF文件程序头表的描述 ,对ELF文件进行映射, 比如代码, 数据,只读数据

    • 4.初始化ELF进程环境,

    • 5.将系统调用的返回地址修改成ELF文件可执行文件的入口点

      • 这个入口点对于静态链接的

        • e_entry所指的地址
      • 对于动态链接

        • 入口是动态链接器
  • 当load_elf_binary()执行完成后,系统调用的返回地址已经修改成被装载的ELF文件的入口地址了, sys_execve()系统调用()从内核态返回到用户态的时候, EIP寄存器直接跳转到了 ELF程序的入口地址

  • 至此, 新的程序开始执行, ELF可执行文件装载完成

分支主题 2

分支主题 3

XMind: ZEN - Trial Version

posted @ 2020-09-26 16:35  Yan_Hao  阅读(1327)  评论(0编辑  收藏  举报