程序员自我修养阅读笔记——可执行文件的装载过程

Posted 落樱弥城

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序员自我修养阅读笔记——可执行文件的装载过程相关的知识,希望对你有一定的参考价值。

1 可执行文件的装载过程

1.1 进程虚拟地址空间

  一个可执行文件被装载到内存变成程序后(进程和程序的区别在于一个是静态的一个是动态的,程序就是菜谱,进程就是厨师参考菜谱做菜的过程),拥有自己独立的地址空间。该地址空间是一个虚拟的地址空间,在该进程看来该空间内包含内核和自身,32bit系统该空间的大小是4GB,64bit系统是2的64次方-1bit。也就是说,一个程序实际上能够用到的虚拟内存空间实际上是小于理论值的,因为操作系统需要占用。

1.2 装载的方式

  静态装入:将程序执行时所需要的指令和数据全部载入到内存中。
  动态装入:利用程序的局部性原理,仅仅将程序中需要运行的部分装入,类似虚拟页的换入换出。
  动态转入的两种实现:

  • 覆盖装入:覆盖装入的方法把挖掘内存潜力的任务交给了程序员,程序员在编写程序的时候必须手工将程序分割成若干块,然后编写一个小的辅助代码来管理这些模块何时应该驻留内存而何时应该被替换掉;
  • 页映射:页映射是将内存和所有磁盘中的数据和指令按照页为单位划分成若干个页,以后所有的装载和操作的单位就是页,利用页面调度算法进行页面调度。

1.3 进程的创建过程

  一般进程的创建需要做三件事情:

  • 创建独立的虚拟内存空间。进程首次创建时只会分配一个页目录,页映射关系是在有后续发生页错误时进行设置;
  • 读取可执行文件,建立虚拟地址空间和可执行文件的映射关系。系统会根据可执行文件的大小建立相关的映射关系。
  • 将CPU指令寄存器设置成可执行文件的入口,开始进入进程调度循环。

1.4 进程虚拟地址空间分布

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

  创建进程时,由于可执行文件往往存在多个段,如果针对每个段都单独进行内存映射,这样很容易造成内存的浪费。比如数据段可能就2个字节,但是如果依然按照一个页大小分配,剩余的4k-2的内存都浪费了。如果站在操作系系统的角度看,操作系统并不关心不同段的内容,只关心段的权限(可读?可写?可执行?)。因此可以通过将相同权限的段合并统一进行映射减少内存的浪费。在ELF中多个段的合并的集合就是一个Segment,而Segment中每个段都是相同类型的Section。

  • 代码段可读可执行;
  • 数据段和BSS段可读可写;
  • 只读数据段只读。

  下面利用下面简单的程序展示如何合并

//编译命令clang -static main.c -o main.elf
#include <unistd.h>

int main()
    while(1)
        sleep(1000);
    

    return 0;

  使用readelf -S main.elf查看详细的Section。

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
readelf: Warning: [ 2]: Link field (0) should index a symtab section.
  [ 2] .rela.plt         RELA             00000000004001b0  000001b0 0000000000000228  0000000000000018  AI       0    19     8
  [ 3] .init             PROGBITS         00000000004003d8  000003d8 0000000000000017  0000000000000000  AX       0     0     4
  [ 4] .plt              PROGBITS         00000000004003f0  000003f0 00000000000000b8  0000000000000000  AX       0     0     8
  [ 5] .text             PROGBITS         00000000004004b0  000004b0 000000000008f3d0  0000000000000000  AX       0     0     16
  [ 6] __libc_freeres_fn PROGBITS         000000000048f880  0008f880 0000000000001523  0000000000000000  AX       0     0     16
  [ 7] __libc_thread_fre PROGBITS         0000000000490db0  00090db0 00000000000010eb  0000000000000000  AX       0     0     16
  [ 8] .fini             PROGBITS         0000000000491e9c  00091e9c 0000000000000009  0000000000000000  AX       0     0     4
  [ 9] .rodata           PROGBITS         0000000000491ec0  00091ec0 000000000001926c  0000000000000000   A       0     0     32
  [10] .stapsdt.base     PROGBITS         00000000004ab12c  000ab12c 0000000000000001  0000000000000000   A       0     0     1
  [11] .eh_frame         PROGBITS         00000000004ab130  000ab130 000000000000a578  0000000000000000   A       0     0     8
  [12] .gcc_except_table PROGBITS         00000000004b56a8  000b56a8 000000000000008e  0000000000000000   A       0     0     1
  [13] .tdata            PROGBITS         00000000006b6120  000b6120 0000000000000020  0000000000000000 WAT       0     0     8
  [14] .tbss             NOBITS           00000000006b6140  000b6140 0000000000000040  0000000000000000 WAT       0     0     8
  [15] .init_array       INIT_ARRAY       00000000006b6140  000b6140 0000000000000010  0000000000000008  WA       0     0     8
  [16] .fini_array       FINI_ARRAY       00000000006b6150  000b6150 0000000000000010  0000000000000008  WA       0     0     8
  [17] .data.rel.ro      PROGBITS         00000000006b6160  000b6160 0000000000002d94  0000000000000000  WA       0     0     32
  [18] .got              PROGBITS         00000000006b8ef8  000b8ef8 00000000000000f8  0000000000000000  WA       0     0     8
  [19] .got.plt          PROGBITS         00000000006b9000  000b9000 00000000000000d0  0000000000000008  WA       0     0     8
  [20] .data             PROGBITS         00000000006b90e0  000b90e0 0000000000001af0  0000000000000000  WA       0     0     32
  [21] __libc_subfreeres PROGBITS         00000000006babd0  000babd0 0000000000000048  0000000000000000  WA       0     0     8
  [22] __libc_IO_vtables PROGBITS         00000000006bac20  000bac20 00000000000006a8  0000000000000000  WA       0     0     32
  [23] __libc_atexit     PROGBITS         00000000006bb2c8  000bb2c8 0000000000000008  0000000000000000  WA       0     0     8
  [24] __libc_thread_sub PROGBITS         00000000006bb2d0  000bb2d0 0000000000000008  0000000000000000  WA       0     0     8
  [25] .bss              NOBITS           00000000006bb2e0  000bb2d8 00000000000016f8  0000000000000000  WA       0     0     32
  [26] __libc_freeres_pt NOBITS           00000000006bc9d8  000bb2d8 0000000000000028  0000000000000000  WA       0     0     8
  [27] .comment          PROGBITS         0000000000000000  000bb2d8 000000000000005f  0000000000000001  MS       0     0     1
  [28] .note.stapsdt     NOTE             0000000000000000  000bb338 0000000000001638  0000000000000000           0     0     4
  [29] .symtab           SYMTAB           0000000000000000  000bc970 000000000000a980  0000000000000018          30   677     8
  [30] .strtab           STRTAB           0000000000000000  000c72f0 0000000000006916  0000000000000000           0     0     1
  [31] .shstrtab         STRTAB           0000000000000000  000cdc06 0000000000000163  0000000000000000           0     0     1

  使用readelf -l main.elf查看合并后的Segment。

Program Headers:
  Type           Offset             VirtAddr           PhysAddr           FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000b5736 0x00000000000b5736  R E    0x200000
  LOAD           0x00000000000b6120 0x00000000006b6120 0x00000000006b6120 0x00000000000051b8 0x00000000000068e0  RW     0x200000
  NOTE           0x0000000000000190 0x0000000000400190 0x0000000000400190 0x0000000000000020 0x0000000000000020  R      0x4
  TLS            0x00000000000b6120 0x00000000006b6120 0x00000000006b6120 0x0000000000000020 0x0000000000000060  R      0x8
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x00000000000b6120 0x00000000006b6120 0x00000000006b6120 0x0000000000002ee0 0x0000000000002ee0  R      0x1

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

  从上面的结果能够看出32个Section被映射成了5个Segment,而我们只关心其中涉及LOAD的Segment,可以看到分别被合并为00和01两个Segment。

  对应到ELF文件中(ELF文件和动态库文件,目标文件不涉及转载不存在)都有一个程序表头来表示Segment的信息,其基本结构如下:

typedef struct elf32_phdr
  Elf32_Word    p_type;     //段的类型,LOAD,DYNAMIC等
  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;

1.4.2 堆和栈

  在操作系统里面,VMA(虚拟内存区域)除了被用来映射可执行文件中的各个Segment以外,它还可以有其他的作用,操作系统通过使用VMA来对进程的地址空间进行管理。我们知道进程在执行的时候它还需要用到堆和栈等空间,事实上它们在进程的虚拟空间中的表现也是以VMA的形式存在的,很多情况下,一个进程中的堆和栈分别都有一个对应的VMA。
  执行刚才编译的可执行文件,查看linux系统中的proc目录下的maps就能看到VMA的映射情况。

➜  Desktop ./main.elf &
[1] 265
➜  Desktop cat /proc/265/maps
00400000-004b5000 r-xp 00000000 00:00 207690                     /mnt/c/Users/ares/Desktop/main.elf
004b5000-004b6000 r-xp 000b5000 00:00 207690                     /mnt/c/Users/ares/Desktop/main.elf
006b6000-006bc000 rw-p 000b6000 00:00 207690                     /mnt/c/Users/ares/Desktop/main.elf
006bc000-006bd000 rw-p 00000000 00:00 0
01d4f000-01d72000 rw-p 00000000 00:00 0                          [heap]
7fffcfdf5000-7fffd05f5000 rw-p 00000000 00:00 0                  [stack]
7fffd0baa000-7fffd0bab000 r-xp 00000000 00:00 0                  [vdso]

  能够看到栈空间大小是1Mb,堆是17Kb。

1.4.3 VMA类型

  根据上面的对比,可以将VMA划分为下面几种(划分按照我的机器上的结果划分,而不是按照书上来的):

  • 代码VMA:可读可执行;
  • 数据VMA:可读可写;
  • 堆VMA:可读可写;
  • 栈VMA:可读可写。

2 Linux内核装载ELF文件过程

  Linux通过bash创建进程的方式:

  • 首先内核会调用fork()创建当前进程的复制品,当前进程也就是父进程一般是bash
  • 然后会调用系统调用execve系列函数装载对应的可执行文件;
  • 装载elf文件时会:
    • 检查ELF可执行文件的有效性;
    • 寻找动态链接.interp段,设置动态链接的路径;
    • 根据ELF可执行文件的程序头表的描述,对ELF文件进行映射;
    • 初始化ELF进程环境;
    • 将系统调用的返回地址修改为ELF可执行文件的入口点:
      • 如果ELF文件为静态链接,则入口点就是ELF文件的入口点;
      • 如果ELF文件为动态链接,则入口点就是动态连接器。

3 Windows PE装载

  PE文件的装载跟ELF有所不同,由于PE文件中,所有段的起始地址都是页的倍数,段的长度如果不是页的整数倍,那么在映射时向上补齐到页的整数倍,我们也可以简单地认为在32位的PE文件中,段的起始地址 和长度都是4096字节的整数倍。由于这个特点,PE文件的映射过错会比ELF简单得多,因为它无需考虑如ELF里面诸多段地址对齐之类的问题,虽然这样会浪费一些磁盘和内存空间。PE可执行文件的段的数量一般很少,不像ELF中经常有十多个"Section",最后不得不使用“Segment”的概念把它们合并到一起装载,PE文件中,链接器在生产可执行文件时,往往将所有的段尽可能地合并,所以一般只有代码段、数据段、只读数据段和BSS等为数不多的几个段。

  • 先读取文件中的第一个页,在这个页中,包含了DOS头、PE文件头和段表;
  • 检查进程地址空间中,目标地址是否可用,如果不可用,则另外选一个装载地址。这个问题对于可执行文件来说基本不存在,因为它往往是进程第一个装入的模块,所以目标地址不太可能被占用;
  • 使用段表中提供的信息,将PE文件中所有的段一一映射到地址空间中相应的位置;
  • 如果装载地址不是目标地址,则进行Rebasing;
  • 装载所有PE文件所需要的DLL文件;
  • 对PE文件中的所有导入符号进行解析;
  • 根据PE头中指定的参数,建立初始化栈和堆;
  • 建立主线程并且启动进程。

以上是关于程序员自我修养阅读笔记——可执行文件的装载过程的主要内容,如果未能解决你的问题,请参考以下文章

《程序员自我修养》阅读笔记-可执行文件装载与进程

读书笔记|《程序员的自我修养》- 04 可执行文件的装载与进程

读书笔记|《程序员的自我修养》- 04 可执行文件的装载与进程

读书笔记|《程序员的自我修养》- 04 可执行文件的装载与进程

《程序员自我修养》阅读笔记-动态链接

《程序员自我修养》阅读笔记-静态链接