一、ELF文件

ELF(Executable and Linkable Format)文件是linux下的二进制可执行文件,它同时兼容可执行文件和可链接文件。

一个ELF文件包含两个部分:一个固定长度的文件头和多个可扩展的数据块。其中,文件头是整个可执行文件的总地图,描述了整个文件的组织结构。可扩展数据块分为两类,对应着不同的视图——在链接视图下,数据块的单位是节(Section),用多个节区头索引所有内容;而在执行视图下,数据块的单位是段(Segment),用程序头(Program Header)索引所有的段。如下图所示:

通过readelf -S命令可以打印出文件的节区头部分,这里使用ln命令作为源程序分析:

[ma@localhost:~]$ readelf -S /bin/ls
There are 29 section headers, starting at offset 0x1a358:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400200  00000200
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             000000000040021c  0000021c
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             000000000040023c  0000023c
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400260  00000260
       0000000000000064  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002c8  000002c8
       0000000000000be8  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400eb0  00000eb0
       00000000000005c4  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000401474  00001474
       00000000000000fe  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000401578  00001578
       00000000000000a0  0000000000000000   A       6     3     8
  [ 9] .rela.dyn         RELA             0000000000401618  00001618
       00000000000001b0  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004017c8  000017c8
       0000000000000990  0000000000000018   A       5    12     8
  [11] .init             PROGBITS         0000000000402158  00002158
       0000000000000018  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000402170  00002170
       0000000000000670  0000000000000010  AX       0     0     4
  [13] .text             PROGBITS         00000000004027e0  000027e0
       000000000000fb68  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000412348  00012348
       000000000000000e  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000412360  00012360
       0000000000003b27  0000000000000000   A       0     0     32
  [16] .eh_frame_hdr     PROGBITS         0000000000415e88  00015e88
       00000000000006b4  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000416540  00016540
       0000000000001fdc  0000000000000000   A       0     0     8
  [18] .ctors            PROGBITS         0000000000619000  00019000
       0000000000000010  0000000000000000  WA       0     0     8
  [19] .dtors            PROGBITS         0000000000619010  00019010
       0000000000000010  0000000000000000  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000619020  00019020
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .data.rel.ro      PROGBITS         0000000000619040  00019040
       0000000000000a48  0000000000000000  WA       0     0     32
  [22] .dynamic          DYNAMIC          0000000000619a88  00019a88
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000619c58  00019c58
       0000000000000098  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000619cf0  00019cf0
       0000000000000348  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         000000000061a040  0001a040
       0000000000000200  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           000000000061a240  0001a240
       0000000000000d20  0000000000000000  WA       0     0     32
  [27] .gnu_debuglink    PROGBITS         0000000000000000  0001a240
       0000000000000010  0000000000000000           0     0     4
  [28] .shstrtab         STRTAB           0000000000000000  0001a250
       0000000000000101  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

节区头包含了我们经常见到的字段:.data/.text/.init等,这里只描述一下这几个常见节区的作用。

1.1 .text 节区

该节区存储了源文件编译生成的机器指令。对开发者来说,这可能是最重要的一个节区,所有的程序逻辑都放在这里。但开发者对该节区能做的控制却很少,能影响它的因素只有开发者写的程序逻辑,以及编译时使用的选项。比如,使用 -O1 优化选项编译程序可以生成尽量紧凑的 .text 节区,而用 -O2 优化选项会使编译器倾向于生成执行速度更快的指令组合,但有可能让 .text 节区的体积轻微地增大。

1.2 .rodata 节区

从字面上就能看出,ro表示read only,即不可写,因此该节区存储了程序中的常量数据,例如:

const char *p = "helloworld";

1.3 .data 节区

所有的全局和静态的已初始化变量会存放在这个节区中,这个节区是可读可写的。

1.4 .bss 节区

该节区存储了所有未初始化或初始化为 0 的全局和静态变量,该节区的设计初衷就是为了节省目标文件的存储空间。变量未被初始化,或者虽被初始化了,但值为 0,就没必要浪费空间,再在目标文件中存储大量的 0 值。

1.5 .got 和 .plt 节区

这两个节区存储了动态链接用到的全局入口表和跳转表。当程序中用到动态链接库中的某个函数时,会在该节区内记录相应的数据。

二、进程内存分布

2.1 虚拟地址空间

linux为每个进程都分配了4G(32位平台)的虚拟地址空间,虚拟空间可以认为是操作系统给每个进程准备的沙盒。就像电影《黑客帝国》中 Matrix 给每个人准备的充满营养液的容器一样。实际上,每个进程只存活在自己的虚拟世界里,却感觉自己独占了所有的系统资源(内存)。

当一个进程要使用某块内存时,它会将自己世界里的一个内存地址告诉操作系统,剩下的事情就由操作系统接管了。操作系统中的内存管理策略将决定映射哪块真实的物理内存,供应用使用。操作系统会竭尽全力满足所有进程合法的内存访问请求。一旦发现应用试图访问非法内存,它将会把进程杀死,防止它做“坏事”影响到系统或其他进程。

这样做,一方面为了安全,防止进程操作其他进程或者系统内核的数据;另一方面为了保证系统可同时运行多个进程,且单个进程使用的内存空间可以超过实际的物理内存容量。

该做法的另一结果则是降低了每个进程内存管理的复杂度,进程只需关心如何使用自己线性排列的虚拟地址,而不需关心物理内存的实际容量,以及如何使用真实的物理内存。

2.2 虚拟地址空间分布

虚拟内存的排布规则如下所示:

从下往上依次是0-4G的内存空间,分别分给了不同的区段。可以用一段程序来验证这一个观点:

#include <stdlib.h>

static const char *p = "HelloWorld";
static int s_init_var = 0;
static int s_not_init;

int g_init_var = 0;
int g_not_init;

int main() {
    const char *q = "abc";

    int a, b;
    int *pa = (int *)malloc(sizeof(int));
    int *pb = (int *)malloc(sizeof(int));

    printf("static variable address: %p %p\n",
            &s_init_var, &s_not_init);
    printf("global variable address: %p %p\n",
            &g_init_var, &g_not_init);
    printf("const variable:%p[static] %p\n",
            p, q);
    printf("a and b: %p %p\n", &a, &b);
    printf("pa and pb: %p %p\n", pa, pb);

    return 0;
}

代码分别创建了静态变量、全局变量以及临时变量等多种不同场景下的变量,打印出它们的地址。使用gcc编译运行:

static variable address: 0x600a0c 0x600a10
global variable address: 0x600a08 0x600a14
const variable:0x4006c8 0x4006d3
a and b: 0x7ffda942df14 0x7ffda942df10
pa and pb: 0xa82010 0xa82030

可以看到已经初始化的静态变量s_init_var和已经初始化的全局变量g_init_var两者地址紧紧相邻,间距刚好隔了4个字节,因为他们被初始化了,都放在了.data区域。而未初始化的两个虽然也是相连在一起的,但是和初始化了的变量还是隔了一段距离,但距离不是很大,因为.bss.data地址差别不大。

而被const修饰的两个变量地址也是一样,处在同一个地址空间,和上面的静态变量以及全局变量相比,地址比它们小,这也就说明了.rodata是在.data.bss下面的。

最后的a/bpa/pb则验证了栈区地址和堆区地址的位置,堆是处在靠下的地址,而栈从地址位数上来说就已经很高了。

这里还能看出一点:局部变量b在a后面申请,但是它的地址比a要小;而pb同样也是在pa后面申请,地址却比pa大。
这说明了栈的地址空间是从上往下申请的,而堆则正常由下往上。

三、参考文档

文章部分摘抄于:攻克 Linux 系统编程

原文写得很不错,有兴趣可自行参考。

最后修改:2020 年 03 月 17 日
如果觉得我的文章对你有用,请随意赞赏