程序员的自我修养笔记之装载

可执行文件的装载与进程

介绍ELF文件在Linux下的装载过程,探寻可执行文件装载的本质git

  • 什么是进程的虚拟地址空间
  • 为何进程要有本身独立的虚拟地址空间
  • 几种装载方式
  • 进程虚拟地址空间的分布状况

进程虚拟地址空间

32位硬件平台决定了虚拟地址空间的地址为0 到2^32 -1,即0x00000000 ~ 0xFFFFFFFF,也就是4GB的虚拟空间大小;而63位的硬件平台具备64位寻址能力,它的虚拟地址空间达到了2^64 字节,即0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,总共17179869184GB程序员

而从程序的角度看,C语言中的指针所占空间可用于计算虚拟地址空间的大小,通常状况下,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,4字节。github

如下以32为地址空间为主,64位做为扩展。算法

默认状况下Linux系统将进程的虚拟地址空间做以下分配:windows

1550220847534

其中的操做系统使用的空间,进程是不被容许访问的,且进程并不能彻底使用剩下的3GB虚拟空间,其中一部分是预留给其余用途的。bash

1550220996919

PAE

Linux下Intel在1995年的Pentium Pro CPU便开始使用36位的物理地址,便可以访问64GB的物理内存。这时,操做系统只能有4GB的虚拟地址空间,没法所有读取完64GB的物理内存,而PAE就是为了解决这个问题出现的。数据结构

PAE(Physical Address Extension)是一种地址扩展方式,Inter修改了页映射的方式后,使得新的映射方式能够访问更多的物理内存。操做系统提供一个窗口映射的方法,将额外的内存映射进地址空间中。应用程序根据须要选择申请和映射。好比应用程序中的0x10000000~0x20000000这一段256MB的虚拟地址空间做为窗口,程序从高于4GB的物理空间中申请多个大小为256MB的物理空间,编号为A,B,C,而后根据须要将窗口映射到不一样的物理空间块,用到A时将0x10000000~0x20000000映射到A,用到B,C时在映射过去,如此重复。在Windows下,这种内存操做方式为AWE(Address Windows Extensions)。像Linux等UNIX系统则采用mmp()系统调用来实现app

装载方式

覆盖装入

没有发明虚拟存储以前使用得比较普遍,现已几乎被淘汰。函数

覆盖装入的方法把挖掘内存潜力的任务交给了程序员程序员在编写程序时必须手动将程序分割成若干块,而后编写小的辅助代码管理这些模块什么时候驻留在内存,什么时候被替换。这个辅助代码被称为覆盖管理器(Overlay Manager),好比下图ui

1550223485233

模块A与B之间相互没有调用依赖关系,所以两模块共享内存区域,当

使用A时则覆盖该内存,使用B时覆盖该内存,覆盖管理器则做为常驻内存。

多模块则以下,程序员须要手工将模块按照它们之间的调用依赖关系组织成树状结构

1550223740492

所以覆盖管理器须要保证一下亮点。

  • 树状结构中从任何一个模块到树的根(main)都叫调用路径,当模块被调用时,这个调用路径上的模块必须在内存之中。好比C模块正在执行时,B和main都须要在内存中,确保E执行完毕后能正确返回到模块B和main。

  • 禁止跨树间调用

    任意模块不容许跨树状结构进行调用,好比A不能够调用B,E,F。但不少时候两个模块都依赖于同一个模块,如模块E和模块C须要另一个模块G,则最方便的方法就是把模块G并入到main模块中,这样G就在E和C的调用路径上了。

页映射

页映射是虚拟存储机制的一部分,随着虚拟存储的发明而诞生。

页映射将内存和全部磁盘中的数据及指令按照**页(Page)**为单位划分若干页,之后全部的装载和操做单位就是页。

1550224180737

假设程序全部的指令和数据总共32KB,那么程序被分为8页,并编号P0~P7。但16KB内存没法将32KB程序装入,此时将按照动态装入的原理进行装入过程。若是程序执行入口在P0则装载管理器发现程序的P0不在内存中,则将内存F0分配给P0,并将P0的内存扎un购入F0中。运行后使用P5,则将P4装入F1,以此类推,以下所示:

1550224497404

若是程序继续运行须要访问P4,则装载管理器必须选择放弃目前正在使用的4哥内存页中的其中一个来装载P4,放弃的算法有不少,如:

  • FIFO先进先出,则放弃F0,P4装入F0
  • LUR最少使用,则放弃F2,P4装入F2

等算法。而这里所谓的装载管理器就是现代的操做系统,准确说影视就是操做系统的存储管理器。

从操做系统角度看可执行文件的装载并在进程中执行

进程的创建

进程关键特征在于它拥有独立的虚拟地址空间。一个程序被执行,每每在最开始时须要作三件事:

  • 建立独立的虚拟地址空间

    即建立映射函数所须要的相应的数据结构,而在i386的Linux下,建立虚拟地址空间实际上只是分配一个页目录,甚至不须要设置映射关系。也就是完成虚拟空间到物理内存的映射关系。

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

    完成虚拟空间与可执行文件的映射关系,这一步是整个装载过程当中最重要的一步,也就是传统意义上的“装载”。

    如图,考虑最简单的例子,虚拟地址如图,文件大小为0x000e1,对齐为0x1000。因为.text段大小不到0x1000,所以须要对齐。

    1550650714552

    这种映射关系是保存在操做系统内部的一个数据结构。Linux将进程虚拟空间中的一个段叫作虚拟内存区域(VMA.Virtual Memory Area)。windows叫作虚拟段(Virtual Section)。在上面例子中,会在进程相应的数据结构中设置有一个.text段的VMA,它在虚拟空间中的地址为0x08048000~0x08049000对应ELF文件中偏移为0的.text,属性为只读。

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

页错误

上述步骤执行完后,只是经过可执行文件头部信息创建起可执行文件和进程虚存之间的映射关系,并无将可执行文件的指令和数据装入内存。

假设在上面的例子中,程序入口地址为0x08048000,恰好是.text段的其实地址,CPU打算执行时发现为空页面时,便认为这是个页错误(Page Fault)。CPU将控制权交给操做系统,操做系统将查询上面说到的数据结构,找到空页面所在VMA,计算出相应的页面在可执行文件中的偏移,再在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间创建映射关系,再把控制权还给进程,进程从刚才页错误的位置从新开始执行,以下图所示,为可执行文件,进程虚存与物理内存之间的关系:

1550995358009

进程虚存空间分布

ELF文件连接视图和执行视图

在实际场景里面,ELF文件段数量是比较多的,但因为须要进行页对齐等操做,若是以一个段进行页的分配的话,势必会形成较大的浪费。

可是在操做系统的角度来看装载可执行文件(操做系统并不须要知道哪一个段名称是什么,做用如何等等信息),发现并不关心可执行文件实际内容,而只是关心跟装载相关的问题,最主要的就是段的权限问题。

段的权限组合基本是如下三种

  • 以代码段为表明的可读可执行段
  • 以数据段和BSS段为表明的可读可写段
  • 以只读数据段为表明的权限为只读的段

所以找到一个以权限种类为划分,将相同权限的段合并在一块儿进行映射的方案,而合并后的数据称之为Segment,如.text段和.init段合在一块儿看做为一个Segment,那么在装载时即可以把他们看做一个个总体进行装载,这样就能够达到明显减小页面内部碎片化的问题,从而节省空间。

对好比下图,左边为按段装载,又边为合并后按Segment装载

1550997079148

下面编写一个例子程序:

SectionMapping.c
#include <stdlib.h>

int main(){
	while(1){
		sleep(1000);
	}
	return 0;
}
gcc -static SectionMapping.c -o SectionMapping.elf
复制代码

而后再使用readelf

readelf -S SectionMapping.elf
复制代码

获得以下信息

There are 33 section headers, starting at offset 0xc4d50:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.ABI-tag     NOTE             0000000000400190  00000190
       0000000000000020  0000000000000000   A       0     0     4
  [ 2] .note.gnu.build-i NOTE             00000000004001b0  000001b0
       0000000000000024  0000000000000000   A       0     0     4
readelf: Warning: [ 3]: Link field (0) should index a symtab section.
  [ 3] .rela.plt         RELA             00000000004001d8  000001d8
       00000000000001f8  0000000000000018  AI       0    24     8
  [ 4] .init             PROGBITS         00000000004003d0  000003d0
       0000000000000017  0000000000000000  AX       0     0     4
  [ 5] .plt              PROGBITS         00000000004003e8  000003e8
       00000000000000a8  0000000000000000  AX       0     0     8
  [ 6] .text             PROGBITS         0000000000400490  00000490
       00000000000874fc  0000000000000000  AX       0     0     16
  [ 7] __libc_freeres_fn PROGBITS         0000000000487990  00087990
       00000000000024e9  0000000000000000  AX       0     0     16
  [ 8] __libc_thread_fre PROGBITS         0000000000489e80  00089e80
       00000000000003d7  0000000000000000  AX       0     0     16
  [ 9] .fini             PROGBITS         000000000048a258  0008a258
       0000000000000009  0000000000000000  AX       0     0     4
  [10] .rodata           PROGBITS         000000000048a280  0008a280
       000000000001bd7c  0000000000000000   A       0     0     32
  [11] __libc_subfreeres PROGBITS         00000000004a6000  000a6000
       0000000000000048  0000000000000000   A       0     0     8
  [12] __libc_IO_vtables PROGBITS         00000000004a6060  000a6060
       00000000000006a8  0000000000000000   A       0     0     32
  [13] __libc_atexit     PROGBITS         00000000004a6708  000a6708
       0000000000000008  0000000000000000   A       0     0     8
  [14] .stapsdt.base     PROGBITS         00000000004a6710  000a6710
       0000000000000001  0000000000000000   A       0     0     1
  [15] __libc_thread_sub PROGBITS         00000000004a6718  000a6718
       0000000000000010  0000000000000000   A       0     0     8
  [16] .eh_frame         PROGBITS         00000000004a6728  000a6728
       0000000000009c38  0000000000000000   A       0     0     8
  [17] .gcc_except_table PROGBITS         00000000004b0360  000b0360
       0000000000000085  0000000000000000   A       0     0     1
  [18] .tdata            PROGBITS         00000000006b0b40  000b0b40
       0000000000000020  0000000000000000 WAT       0     0     8
  [19] .tbss             NOBITS           00000000006b0b60  000b0b60
       0000000000000040  0000000000000000 WAT       0     0     8
  [20] .init_array       INIT_ARRAY       00000000006b0b60  000b0b60
       0000000000000010  0000000000000008  WA       0     0     8
  [21] .fini_array       FINI_ARRAY       00000000006b0b70  000b0b70
       0000000000000010  0000000000000008  WA       0     0     8
  [22] .data.rel.ro      PROGBITS         00000000006b0b80  000b0b80
       0000000000000464  0000000000000000  WA       0     0     32
  [23] .got              PROGBITS         00000000006b0fe8  000b0fe8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         00000000006b1000  000b1000
       00000000000000c0  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         00000000006b10c0  000b10c0
       0000000000001af0  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           00000000006b2bc0  000b2bb0
       0000000000001718  0000000000000000  WA       0     0     32
  [27] __libc_freeres_pt NOBITS           00000000006b42d8  000b2bb0
       0000000000000028  0000000000000000  WA       0     0     8
  [28] .comment          PROGBITS         0000000000000000  000b2bb0
       0000000000000025  0000000000000001  MS       0     0     1
  [29] .note.stapsdt     NOTE             0000000000000000  000b2bd8
       0000000000001408  0000000000000000           0     0     4
  [30] .symtab           SYMTAB           0000000000000000  000b3fe0
       000000000000a6c8  0000000000000018          31   693     8
  [31] .strtab           STRTAB           0000000000000000  000be6a8
       0000000000006532  0000000000000000           0     0     1
  [32] .shstrtab         STRTAB           0000000000000000  000c4bda
       0000000000000176  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
复制代码

查看ELF的Segment信息,正如称Section属性的结构叫作段表,描述Segment的结构为程序头(Program Header),它描述ELF文件该如何被操做系统映射进进程的虚拟空间:

readelf -l SectionMapping.elf
复制代码

结果以下:

Elf file type is EXEC (Executable file)
Entry point 0x400a00
There are 6 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000b03e5 0x00000000000b03e5  R E    0x200000
  LOAD           0x00000000000b0b40 0x00000000006b0b40 0x00000000006b0b40
                 0x0000000000002070 0x00000000000037c0  RW     0x200000
  NOTE           0x0000000000000190 0x0000000000400190 0x0000000000400190
                 0x0000000000000044 0x0000000000000044  R      0x4
  TLS            0x00000000000b0b40 0x00000000006b0b40 0x00000000006b0b40
                 0x0000000000000020 0x0000000000000060  R      0x8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x00000000000b0b40 0x00000000006b0b40 0x00000000006b0b40
                 0x00000000000004c0 0x00000000000004c0  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00     .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_IO_vtables __libc_atexit .stapsdt.base __libc_thread_subfreeres .eh_frame .gcc_except_table 
   01     .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs 
   02     .note.ABI-tag .note.gnu.build-id 
   03     .tdata .tbss 
   04     
   05     .tdata .init_array .fini_array .data.rel.ro .got 
复制代码

从装载的角度看,咱们只须要关心两个“LOAD”类型的Segment,其余只是在装载中起辅助做用。这里能够看到,文件被从新划分红三个部分:

  • 可读可执行LOAD段
  • 可读写的LOAD段
  • 没有被映射的段

所以全部相同属性的Section被归类到了同一个Segment,并映射到同一个VMA里面。因此说Segment和Section在不一样的角度给ELF文件进行划分。

  • 连接视图:从Section的角度进行划分
  • 执行视图:从Segment的角度进行划分

能够以下图表示可执行文件的段与进程虚拟空间的映射关系:

1550998021300

ELF可执行文件和共享库文件有个专门的数据结构叫**程序头表(Program Header Table)**用来保存Segment信息,由于ELF文件不须要被装载,所以没有程序头表具体结构以下,并与readelf -l读出来的数据一一对应

typedef struct{
    Elf32_Word        p_type;
    Elf32_Off            p_offset;
    Elf32_Addr         p_vaddr;
    Elf32_Addr         p_paddr;
    Elf32_Word        p_filesz;
    Elf32_Word        p_memsz;
    Elf32_Word        p_flags;
    Elf32_Word        p_align;
}Elf32_Phdr;
复制代码

基本含义以下

1550998603509

其中,p_memsz >= p_filesz但若是p_memsz <= p_filesz则表示Segment段在内存中分配的空间大小超过文件中实际的大小,这部分多余的空间则被所有填充为”0“。这样咱们构造ELF可执行文件时就不须要再额外设立BSS的Segment了,能够把数据Segment的p_memsz扩大,那些额外的部分就是BSS。数据段和BSS的区别在于,数据段从文件中初始化内容,而BSS则所有被初始化为0。所以能够看到前面的BSS其实已经被并入数据类型段里面,而没有显示出来。

堆和栈

查看进程虚拟空间分布如图所示:

1550999813925

其中意义能够见我另一篇文章,《/proc/{pid}/maps的文件结构解析》

其中,主设备号和次设备号及文件节点号都是0,表示没有映射到文件,这种VMA叫作匿名虚拟内存区域(Anonymous Virtual Memory Area)。咱们目前关注Heap和Stack这两个VMA几乎在全部进程中都存在,malloc()函数内存分配就是从堆里面进行分配的,堆由系统库管理,而vsyscall位于内核空间,具体做用待深究,不过在名称来看猜想应该是和内核通讯相关的VMA了。

一个进程可主要分以下几个区域:

  • 代码VMA,权限只读,可执行;有映像文件
  • 数据VMA,权限可读写;有映像文件
  • 堆VMA,可读可写不可执行;匿名,可向上扩展
  • 栈VMA,可读可写不可执行;匿名,可向下扩展

具体如图:

1551000603318

Linux内核装载ELF过程

  • bash进程调用fork()系统调用建立一个新的进程

  • 调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,而后等待用户输入命令。execve()被定义在unsitd.h文件中,原型以下

    int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

    三个参数分别是被执行的程序文件名,执行参数,环境变量

啊~~~这里步骤比较多且复杂,没有实际实验也看不太懂,就先直接贴图将就着看吧;

1551002209326

1551002228233

而后最后获得的结果就是返回地址改为被装载的ELF程序入口地址:

  • 静态连接:ELF文件的头文件中e_entry所指的地址
  • 动态连接:动态连接器地址

至此,可执行文件的装载部分已介绍完毕

相关文章
相关标签/搜索