浅析程序的装载和运行

1 ELF 文件格式

1.1 ELF

ELF(Executable and Linkable Format)是一种对象文件的格式,是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。类似于 Windows 的 PE,ELF 是 Linux 主要的可执行文件的格式。

ELF 文件由 4 部分组成,分别是 ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。如下图所示1,就是一个典型的 ELF 文件。

在这里插入图片描述
实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有 ELF 头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

  • ELF header: 描述整个文件的组织。
  • Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。
  • sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。从图中我们也可以看出,segments与sections是包含的关系,一个segment包含若干个section。
  • Section Header Table: 包含了文件各个section的属性信息。

1.2 示例

以最简单的一个 C 程序为例

#include <stdio.h>
#include <unistd.h>

void add(int a, int b)
{
    
    
	printf("%d + %d = %d\n", a, b, a + b);
}

int main(int argc, char const *argv[])
{
    
    
	add(1, 2);
	sleep(1);
	return 0;
}

普通的一条 gcc 编译命令

gcc test.c -o test

实际上包括以下几个阶段

  1. 预处理 gcc -E,进行编译之前的预处理操作。
  2. 编译 gcc -S,编译成汇编文件。
  3. 汇编 as,将编译的文件汇编成机器码。
  4. 链接 ld,将不同的目标文件链接在一起。

每个过程不加以叙述,这不是本次讲解的重点。

使用 readelf --segments 查看 ELF 文件的段(其实就是读取 Program Header Table)

lys@ubuntu:~/Documents/workspace$ readelf --segments test -W

Elf file type is DYN (Shared object file)
Entry point 0x1060
There are 11 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x000268 0x000268 R   0x8
  INTERP         0x0002a8 0x00000000000002a8 0x00000000000002a8 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x0005a0 0x0005a0 R   0x1000
  LOAD           0x001000 0x0000000000001000 0x0000000000001000 0x00021d 0x00021d R E 0x1000
  LOAD           0x002000 0x0000000000002000 0x0000000000002000 0x000180 0x000180 R   0x1000
  LOAD           0x002de8 0x0000000000003de8 0x0000000000003de8 0x000250 0x000258 RW  0x1000
  DYNAMIC        0x002df8 0x0000000000003df8 0x0000000000003df8 0x0001e0 0x0001e0 RW  0x8
  NOTE           0x0002c4 0x00000000000002c4 0x00000000000002c4 0x000044 0x000044 R   0x4
  GNU_EH_FRAME   0x002014 0x0000000000002014 0x0000000000002014 0x000044 0x000044 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x002de8 0x0000000000003de8 0x0000000000003de8 0x000218 0x000218 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt 
   03     .init .plt .plt.got .text .fini 
   04     .rodata .eh_frame_hdr .eh_frame 
   05     .init_array .fini_array .dynamic .got .got.plt .data .bss 
   06     .dynamic 
   07     .note.gnu.build-id .note.ABI-tag 
   08     .eh_frame_hdr 
   09     
   10     .init_array .fini_array .dynamic .got 

使用 readelf --sections 查看 ELF 文件的节

lys@ubuntu:~/Documents/workspace$ readelf --sections test -W
There are 30 section headers, starting at offset 0x39b0:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        00000000000002a8 0002a8 00001c 00   A  0   0  1
  [ 2] .note.gnu.build-id NOTE            00000000000002c4 0002c4 000024 00   A  0   0  4
  [ 3] .note.ABI-tag     NOTE            00000000000002e8 0002e8 000020 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0000000000000308 000308 000024 00   A  5   0  8
  [ 5] .dynsym           DYNSYM          0000000000000330 000330 0000c0 18   A  6   1  8
  [ 6] .dynstr           STRTAB          00000000000003f0 0003f0 00008a 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          000000000000047a 00047a 000010 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         0000000000000490 000490 000020 00   A  6   1  8
  [ 9] .rela.dyn         RELA            00000000000004b0 0004b0 0000c0 18   A  5   0  8
  [10] .rela.plt         RELA            0000000000000570 000570 000030 18  AI  5  23  8
  [11] .init             PROGBITS        0000000000001000 001000 000017 00  AX  0   0  4
  [12] .plt              PROGBITS        0000000000001020 001020 000030 10  AX  0   0 16
  [13] .plt.got          PROGBITS        0000000000001050 001050 000008 08  AX  0   0  8
  [14] .text             PROGBITS        0000000000001060 001060 0001b1 00  AX  0   0 16
  [15] .fini             PROGBITS        0000000000001214 001214 000009 00  AX  0   0  4
  [16] .rodata           PROGBITS        0000000000002000 002000 000012 00   A  0   0  4
  [17] .eh_frame_hdr     PROGBITS        0000000000002014 002014 000044 00   A  0   0  4
  [18] .eh_frame         PROGBITS        0000000000002058 002058 000128 00   A  0   0  8
  [19] .init_array       INIT_ARRAY      0000000000003de8 002de8 000008 08  WA  0   0  8
  [20] .fini_array       FINI_ARRAY      0000000000003df0 002df0 000008 08  WA  0   0  8
  [21] .dynamic          DYNAMIC         0000000000003df8 002df8 0001e0 10  WA  6   0  8
  [22] .got              PROGBITS        0000000000003fd8 002fd8 000028 08  WA  0   0  8
  [23] .got.plt          PROGBITS        0000000000004000 003000 000028 08  WA  0   0  8
  [24] .data             PROGBITS        0000000000004028 003028 000010 00  WA  0   0  8
  [25] .bss              NOBITS          0000000000004038 003038 000008 00  WA  0   0  1
  [26] .comment          PROGBITS        0000000000000000 003038 00001d 01  MS  0   0  1
  [27] .symtab           SYMTAB          0000000000000000 003058 000630 18     28  45  8
  [28] .strtab           STRTAB          0000000000000000 003688 00021b 00      0   0  1
  [29] .shstrtab         STRTAB          0000000000000000 0038a3 000107 00      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)

.got 与 .got.plt

ELF 将 GOT 拆分成了两个表,叫 .got 和 .got.plt。GOT(Global Offset Table)全局偏移表,提供对共享函数库的访问入口,由动态链接器在运行时修改2

  • .got: 全局变量的引用地址
  • .got.plt: 外部函数的引用地址

备注:在 Ubuntu 16.04 以及 Kali 2019 上,编译的程序是有 .got.plt 节的,但是在 Ubuntu 18.04 及以上版本时,并没有 .got.plt 节,原先 .got.plt 合并成 .got。这可能是高版本 gcc 或者链接器、操作系统等进行了相应的修改。

bss、data 与 rodata

  • bss:未初始化或者初始化为 0 的全局变量或静态变量,权限为读写。
  • data:已经初始化的全局变量或静态变量,权限为读写。
  • rodata:常量、字符串。

.symtab 与 .dynsym

  • .symtab 保存了所有的符号信息(ElfN_Sym 类型)。
  • .dynsym 保存了引用来自外部文件符号的全局符号,如 printf 这样的库函数

.dynsym 是 .symtab 的子集,那么为啥需要重合的 .dynsym 节呢?这是因为 .symtab 在运行时,并不会装载到内存,而 .dynsym 是运行时必须的,需要被载入内存。

在这里插入图片描述
上图中 .dynsym 标记为 A(Access),即这个节会装载到虚拟空间中。

.ctors与 .dtors

这是在 C++ 中,我们会关注的两个节,即构造器析构器。这两个节保存了指向析构函数和析构函数的指针,构造函数是在 main 函数执行前需要执行的代码;析构函数是在 main 函数之后需要执行的代码。

2 程序的装载

什么是程序的装载?就是说可执行文件按照文件中的段映射(加载)到虚拟地址空间。所以与装载紧密相关的是 ELF 中的 Program Header Table 以及 Segments

ELF 哪些段会被映射到虚拟地址空间呢?

在这里插入图片描述
LOAD 类型的 segment 是真正需要被装载的。而 IDA 能够解析出的正是这些段。上图中,有一个明显不一样的地方,在 0x002de8 的地方,文件偏移与虚拟地址出现了偏差,这说明 ELF 文件并非是将整体直接加载到内存中也并非是文件偏移了多少,在内存中就偏移了多少

在这里插入图片描述
这一进步体现在 IDA 十六进制视图中,地址出现了断层,非连续,说明 IDA 不会解析完整的 ELF,而是读取那些会被加载至内存(虚拟地址空间)的部分,也就是 LOAD 类型的段。

Linux 将进程虚拟地址空间中的一个段叫做虚拟内存区域VMA,Virutal Memory Area)。对于相同权限的节,合并到一起,形成一个段进行映射。如下图所示

在这里插入图片描述

左侧是 /proc/*/maps 进程虚拟地址空间的内存映射,右侧是 IDA (Ctrl + s)读取的 ELF 程序头表,实际上也是反应了 ELF 文件需要加载进内存的部分。

由于可执行文件在装载时实际上时被映射的虚拟空间,所以可执行文件很多时候又被叫做映像文件(image)3

程序加载的基地址=映像基地址(image base),由于开启了地址随机化,每次运行并不是固定的,这里是 0x0000555555554000

值得注意的一点是,上图中绿色部分,IDA 解析出的节的权限可写,而实际的虚拟空间中,只读,这一点值得深思?

3 运行

3.1 main 函数启动之前的一些操作

main 函数只是用户代码的入口,它会由系统库去调用,在 main 函数之前,系统库会做一些初始化工作,比如分配全局变量的内存,初始化堆、线程等,当 main 函数执行完后,会通过 exit() 函数做一些清理工作。

编译器为可执行文件增加了一个启动例程4,ELF 头部的入口地址就指向该启动例程,然后在启动例程中有下面一句:__libc_start_main@plt 通过它调用 C 库的 _libc_start_main,再调用我们的 main。

在这里插入图片描述
由于 main 函数是被启动例程调用的,所以从 main 函数 return 时仍返回到启动例程中,main 函数的返回值被启动例程得到,如果将启动例程表示成等价的 C 代码(实际上启动例程一般是直接用汇编写的),则它调用main函数的形式是:exit(main(argc, argv));

3.2 实际验证

使用 gdb 调试,在程序即将退出的时候,我们发现函数调用栈如下

在这里插入图片描述
说明 main 函数运行之前的流程应该是这样的

  _start(启动例程)
     +
     |
     v
__libc_start_main
     +
     |
     v
   main

调用栈已经处于退出的状态,给出的地址并非是函数实际开始的地址,而是上个函数结束时的返回地址。例如 _start 函数

$ python -c "print '0x%x' % (0x508a - 0x4000)"
0x108a

该地址指向的是 _start 函数中的 hlt 指令,代表 __libc_start_main 退出时,_start 即将要执行的指令。

0000000000001060 <_start>:
    1060:	31 ed                	xor    ebp,ebp
    1062:	49 89 d1             	mov    r9,rdx
    1065:	5e                   	pop    rsi
    1066:	48 89 e2             	mov    rdx,rsp
    1069:	48 83 e4 f0          	and    rsp,0xfffffffffffffff0
    106d:	50                   	push   rax
    106e:	54                   	push   rsp
    106f:	4c 8d 05 9a 01 00 00 	lea    r8,[rip+0x19a]        # 1210 <__libc_csu_fini>
    1076:	48 8d 0d 33 01 00 00 	lea    rcx,[rip+0x133]        # 11b0 <__libc_csu_init>
    107d:	48 8d 3d f4 00 00 00 	lea    rdi,[rip+0xf4]        # 1178 <main>
    1084:	ff 15 56 2f 00 00    	call   QWORD PTR [rip+0x2f56]        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    108a:	f4                   	hlt    
    108b:	0f 1f 44 00 00       	nop    DWORD PTR [rax+rax*1+0x0]

4 总结

本次只是个人在分析maps虚拟空间时,发现可执行文件总是会分几个区映射,进而产生了疑问:一个二进制程序,到底是哪些部分会映射到内存中?《程序员的自我修养》早就告诉我们答案了,但是之前理解的还是很肤浅,看书只能让人有个大概的印象,很多东西只有亲自实践才能体会到背后的原理。


  1. ELF文件格式解析 https://blog.csdn.net/feglass/article/details/51469511 ↩︎

  2. 《Linux 二进制分析》 ↩︎

  3. 《程序员的自我修养——链接、装载与库》 ↩︎

  4. 百度知道 https://zhidao.baidu.com/question/496338341691254644.html ↩︎

猜你喜欢

转载自blog.csdn.net/song_lee/article/details/110091840