zoukankan      html  css  js  c++  java
  • 可执行文件(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

  • 相关阅读:
    包含min函数的栈
    栈的应用
    给定金额m和红包数量n
    顺时针打印矩阵
    二叉树的镜像
    elementUI table表头错位问题
    金额格式化
    ajax跨域问题全解
    JavaScript 的 this 原理
    vue技术分享-你可能不知道的7个秘密
  • 原文地址:https://www.cnblogs.com/yahoo17/p/13735538.html
Copyright © 2011-2022 走看看