7.4. 可执行文件与进程虚拟地址空间的映射

系统启动一个进程时,首先需要把可执行文件中的必要内容加载到内存,程序随后才能运行。不过,并不是ELF文件中的所有数据都会被加载。对于典型可执行文件,只有程序头表中类型为LOAD的段需要映射到内存,其他段主要用于辅助判断如何加载,并不一定进入进程地址空间。一个典型可执行文件加载到虚拟内存后的情况如图6-5所示。

可执行文件与进程虚拟内存的映射关系

图6-5中,只有段1和段2需要加载到内存,分别放入VMA2和VMA1。VMA全称为Virtual Memory Area,即虚拟内存区域。一个VMA可以占用一个或多个页,其大小是页大小的整数倍。通常,不同VMA具有不同访问权限。例如,VMA1存放代码,权限为只读且可执行;VMA2存放数据,权限为可读写。由图6-5可知,可执行文件映射到内存时,ELF文件头以及调试信息(如.debug_info.debug_str段等)不需要加载;真正需要加载的是部分段,例如.text.bss.data等,也就是段类型为LOAD的段。

实际开发中,可以通过/proc/pid/maps节点查看某个进程的虚拟地址空间布局,其中pid为目标进程号。例如,查看进程号为30828的虚拟地址空间布局,可执行命令cat /proc/30828/maps,示例输出如下:

$ cat /proc/30828/maps
120000000-120004000 r-xp 00000000 08:00 100794371    /home/v/c-test/a.out
120004000-12000c000 rwxp 00004000 08:00 100794371    /home/v/c-test/a.out
fff718c000-fff72dc000 r-xp 00000000 08:11 3539724    /usr/lib/loongarch64-linux-gnu/libc-2.28.so
fff72dc000-fff72f0000 r-xp 0014c000 08:11 3539724    /usr/lib/loongarch64-linux-gnu/libc-2.28.so
fff72f0000-fff72f4000 rwxp 00160000 08:11 3539724    /usr/lib/loongarch64-linux-gnu/libc-2.28.so
fff72f4000-fff72f8000 rwxp 00000000 00:00 0
fff7318000-fff7338000 r-xp 00000000 08:11 3539080    /usr/lib/loongarch64-linux-gnu/ld-2.28.so
fff7338000-fff733c000 r-xp 0001c000 08:11 3539080    /usr/lib/loongarch64-linux-gnu/ld-2.28.so
fff733c000-fff7340000 rwxp 00020000 08:11 3539080    /usr/lib/loongarch64-linux-gnu/ld-2.28.so
fffb8ac000-fffb8d0000 rw-p 00000000 00:00 0                     [stack]
ffff07c000-ffff080000 r--p 00000000 00:00 0                     [vvar]
ffff080000-ffff084000 r-xp 00000000 00:00 0                     [vdso]

输出第一列是VMA地址范围;第二列是VMA权限,可能包含可读(r)、可写(w)、可执行(x)、私有(p)、可共享(s)等标记。例如,r-xp表示该虚拟地址区域可读、可执行,并采用私有映射,但不可写。第三列是VMA对应Segment在映射文件中的偏移;第四列表示映像文件所在设备的主设备号和次设备号;第五列表示映像文件节点号;最后一列是映像文件路径。

该示例中,进程共占用12个VMA,起始地址为0x12000000。前两个VMA(12000000~120004000、120004000~12000c000)映射到ELF文件的两个Segment,分别用于存放当前程序的代码段(权限为r-xp)和数据段(权限为rwxp)。动态库libc-2.28.so映射到后续3个VMA。0x12000c000~0xfff718c000之间的地址未映射,属于堆区(Heap),程序可通过malloc等函数申请使用。ld-2.28.so是Linux下的动态链接器,负责对libc-2.28.so中与绝对地址相关的代码和数据进行动态重定位,它占用3个VMA。地址fffb8ac000~fffb8d0000为进程栈空间。最后两个VMA(ffff07c000~ffff080000、ffff080000~ffff084000)分别由vvar和vdso模块占用。这两个区域由内核映射到进程中,用于让程序在某些场景下绕过syscall系统调用,直接通过特定接口与内核快速通信。例如,多数体系架构中的gettimeofday可通过vdso实现,以提高执行速度。