美文网首页
《程序员的自我修养》笔记3——可执行文件的装载

《程序员的自我修养》笔记3——可执行文件的装载

作者: wipping的技术小栈 | 来源:发表于2020-06-06 16:01 被阅读0次

    一、进程的装载方式

    1.1 覆盖装载

    覆盖装载 现在可能被淘汰了,这是一种非常久远的装载方式,它在计算机发展初期提供了内存受限下,程序正常运行的解决方式。
    程序员需要编译 辅助代码 来管理代码模块的装载和卸载,这个 辅助代码 也称为 覆盖管理器

    假设有一个程序,分为 3 个模块:main(1024Byte)A模块(512Byte)B模块(256Byte),其中 A模块B模块 不会相互调用。理论上需要有 1792Byte 的内存空间才能完整装载程序。

    内存图

    为了解决这个问题,程序需要带有 覆盖管理器,且其中 A模块B模块 共享一块内存。在 main 需要调用 A模块 时,将 B模块 换出内存,反之亦然。这样我们仅需要 1536Byte 的内存,比理论上所需要的内存减轻不少。

    覆盖管理器 在组织内存时,需要 将模块按照它们之间的调用依赖关系组织成树状结构,如下图所示:

    树状模块

    覆盖管理器 需要保证

    • 调用路径 上的必须都在内存中。(树状结构中的 任何一个模块根模块 都叫 调用路径)
    • 禁止跨树调用

    1.2 页映射

    在操作系统中,将 内存页(Page) 进行组织,详情可以查看我前面的文章 ARM体系架构——MMU

    现在假设 32bit 的机器,每页为 4KB。其内存有 16KB 并分成 4页,即F0-F3。程序一共 32KB 分成 8页,即 P0-P7。我们可以假定程序有个 装载管理器 来管理页的换入换出。
    那么在程序启动后假设有下图的装载关系:

    内存装载
    如果程序需要访问 P4 时,那么 装载管理器 需要选择一个 内存页 来换入换出,目前有多种 选择算法。其余同理,同理不断的换入换出 内存页,就可以实现动态装载。这个 *装载管理器 也就是我们现在说的 操作系统或者 存储管理器

    常用的选择算法有:

    • FIFO算法(先进先出算法)
    • LUR算法(最少使用算法)

    二、装载可执行程序

    前文讲了 2种 装载方式,如果它们在使用 物理地址 的情况,在这种情况下每次换入换出都需要进行 重定位。现代计算器都拥有 MMU,其提供了 地址转换功能。有了 MMU ,操作系统 动态加载 可执行文件的方式就有了很大的改变。

    2.1 进程的建立

    1. 建立虚拟空间:创建一个 独立的虚拟地址空间。虚拟空间由一组 页映射函数 将虚拟空间中的各个页 映射 到对应的物理空间。创建虚拟空间并不是创建 实际的内存空间,而是创建 映射关系 所需要的 数据结构。在 Linux 下,创建虚拟空间实际上是分配一个 页目录
    2. 建立映射关系:读取 可执行文件头,并建立 虚拟空间 与可执行文件的 映射关系。在代码上,这种 映射关系 表现为一种数据结构,在 Linux c称为 虚拟内存区域(VMA,Virtual Memory Area)VMA 记录了 在可执行文件中的 位置。当程序发生 缺页异常 时,操作系统从 物理内存 中分配一个 物理页,然后从 存储介质 中读取对应的 *程序页物理内存页中,再设置 虚拟页物理页 的映射关系。 VMA 记录了发生异常的 虚拟地址页可执行文件 中的位置,从而可以读取对应的
    3. 设置入口地址:将 CPU指令寄存器 设置成可执行文件的 入口地址,启动运行。ELF文件头 保存了程序的 入口地址

    需要注意的是,虚拟内存 的映射是以 为单位,一般是 4KB。如果一个段小于该大小,由于对齐原因,该段会占用一个 内存页。比如一个 .txt段 小于 4KB,那么在内存中该段会占用一个 4KB 大小的 内存页

    2.2 VMA

    VMA虚拟内存区域(Virtual Memory Area),可以使用下面的查看进程的 虚拟空间分布

    cat /proc/pid/maps

    其中 pid 为对应的进程号,其输出可以看笔者的文章 进程内存优化

    一个进程一般有以下几种 VMA

    • 代码VMA:可读可执行,映射到程序文件
    • 代码VMA:可读可写,映射到程序文件
    • 堆VMA:可读可写可执行,不映射到程序文件中,匿名,向上扩展
    • 代码VMA:可读写,不映射到程序文件,匿名,向下扩展
    maps

    2.3 页错误

    1. 程序访问没有映射到物理页的虚拟地址
    2. 查找该虚拟地址所在的虚拟页对应的VMA
    3. 根据VMA计算出对应页面在可执行文件中的偏移
    4. 物理内存分配物理页
    5. 建立物理页到虚拟页的映射
    6. 将可执行文件中的页读取到内存中
    7. 返回程序继续执行

    三、链接视图和执行视图

    ELF文件 在被映射时,是以 页长度(4KB) 为单位的。如果每个 段(section) 的长度不是 页长度 的整数倍,那么将浪费内存。一个 ELF文件 往往有很多个 ,那么这种浪费将进一步的扩大。

    由于操作系统在装载可执行文件时并不关心 段(section) 的实际内容,而是只关心装载的相关属性,主要就是 段权限(可读、可写、可执行)。权限一般有以下几种组合:

    • 可读可执行,比如 代码段
    • 可读可写,比如 数据段
    • 只读,比如 只读数据段

    对于相同权限的段,把它们合并到一起当做一个段进行映射,这个段也称为 segment。一个 segment 包含一个或多个属性类似的 Section。操作系统将每个 segment 映射到进程虚拟空间后就对应一个 VMA。由此减少内存浪费

    SegmentSection 是从不同角度来看到 ELF文件。从 Segment 看就是 执行视图,从 Section 看就是 链接视图

    用以下这段代码作为示例:

    /* SectionMapping.c */
    #include <stdlib.h>
    #include <unistd.h>
    
    int main()
    {
        while(1)
        {
            sleep(1000);
        }
        return 0;
    }
    

    使用以下命令进行 编译

    arm-linux-gnueabihf-gcc -static SectionMapping.c -o SectionMapping.elf

    使用以下命令查看 section

    arm-linux-gnueabihf-readelf -S SectionMapping.elf

    section

    使用以下命令查看 segment

    arm-linux-gnueabihf-readelf -l SectionMapping.elf

    segment

    根据上面的结果我们就可以看出一个程序中 sectionsegment 的关系。

    3.1 程序头表

    ELFke可执行文件 有个专门的数据结构用于描述 segment ,称为 程序头表(Program Header Table)。由于 目标文件 不需要装载到内存,所以没有 程序头表

    typedef struct
    {
      Elf32_Word    p_type;                 /* Segment type */
      Elf32_Off     p_offset;               /* Segment file offset */
      Elf32_Addr    p_vaddr;                /* Segment virtual address */
      Elf32_Addr    p_paddr;                /* Segment physical address */
      Elf32_Word    p_filesz;               /* Segment size in file */
      Elf32_Word    p_memsz;                /* Segment size in memory */
      Elf32_Word    p_flags;                /* Segment flags */
      Elf32_Word    p_align;                /* Segment alignment */
    } Elf32_Phdr;
    
    • p_typesegment 类型,一般有 LOADDYNAMICINTERP
    • p_offsetsegment 在文件中的偏移
    • p_vaddrSegment 在虚拟空间中的 起始地址
    • p_paddrSegment物理装载地址,一般与 p_vaddr 的值一致。
    • p_filezSegment文件 所占的空间长度
    • p_memseSegment虚拟空间 中所占长度。由于内存对齐,该值一般 不小于p_filez,而有些 Segment 扩充的多余部分可以用于建立 BSS段
    • p_flagsSegment 的权限属性,一般有 R(可读)W(可写)X(可执行)
    • p_alignSegment 的对齐属性

    3.2 段地址对齐

    在一段物理内牧场呢和进程虚拟地址空间之间建立映射关系,这段内存空间的长度必须是 页长度(4096) 的整数倍,并且其其实地址也需要为 页长度(4096) 的整数倍。由于该限制,可执行文件应该尽量优化空间和地址的安排,以节省空间。不然将会造成空间上的浪费。

    下面结合书中例子进行讲解:
    操作系统为了解决 内存浪费 的问题,将各个段 接壤的部分 共享一个 物理页面,然后将该物理页面分别映射 2次

    长度(字节) 偏移 权限
    SEG0 127 34 可读可执行
    SEG1 9899 164 可读可写
    SEG2 1988 只读

    每个 的长度都不是 页长度 的整数倍。由于对齐要求,长度不足一个页的部分则占据一个页。按照这种做法,则 ELF文件 中各个段的信息如下:

    起始虚拟地址 大小 有效字节 偏移 权限
    SEG0 0x08048000 0x1000 127 34 可读可执行
    SEG0 0x08049000 0x3000 9899 164 可读可写
    SEG0 0x0804C000 0x1000 1988 0 只读

    从上表可知,总长度只有 12014字节,但占据了 5 个页,即 20480字节。空间利用率为 58.6%

    为了解决上面的问题,操作系统让各个 段的接壤部分 共享一个物理页面,然后将该物理页面分别映射 2次。如下图所示:

    映射图

    操作系统可以通过访问 VMA1 来访问与 SEG0 共享的物理页,同理通过访问 VMA0 也可以实现对 SEG0 的访问。

    通过这种方式,ELF文件 从占据 5个 页面优化为占用 3个 页面,从而减少了内存浪费。

    通过下面的指令来查看例程代码的 segment信息

    arm-linux-gnueabihf-readelf -l SectionMapping.elf

    segment信息

    注意:这里的例子与书中不是很相符,笔者下面仅按照自己的理解进行讲述

    • 代码段:起始地址为 0x00010000,偏移为 0x000000
    • 数据段:起始地址为 0x00077F70,偏移为 0x057F70

    根据上面的信息,可以大概得出 数据段 占据了 0x57F70 的长度。代码段数据段 相邻,代码段虚拟起始地址0x10000,那么数据段的 虚拟起始地址 应该为 0x67F70。由于对齐是按照 0x10000 进行的,且将 VMA0(代码段)VMA1(数据段) 共享页面映射了 2 次,所以地址就修改为 0x77F70。也就是说 0x67F700x77F70 之间的页面由 代码段 进行访问,而这段空间就是共享的页面。

    相关文章

      网友评论

          本文标题:《程序员的自我修养》笔记3——可执行文件的装载

          本文链接:https://www.haomeiwen.com/subject/ijfqzhtx.html