通常,将可执行文件加载到内存是由 linux 加载程序 ld 驱动的进程。例如,如果我创建一个非常简单的 C 程序,让我们说:
void main {}
用gcc编译这个程序后,我们得到一个可执行文件(ELF格式)a.out。如果我们通过运行 ldd 来分析这个非常简单的程序所具有的依赖关系,我们会发现:
linux-gate.so.1 => (0x00545000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0x00ccb000)
/lib/ld-linux.so.2 (0x00594000)
第一个 linux-gate.so 被内核公开以进行系统调用。 ld-linux.so 实际上是 linux 加载器。它负责在内存中加载任何可执行文件并运行它。如果我们查看我们生成的 a.out(例如通过使用 hexedit 工具),我们可以看到它的标头包含对 ld-linux 所在位置的引用:
.ELF........................4...8.......
4. ...(.........4...4...4... ... .......
........T...T...T.......................
........................................
........................(...(...(.......
................h...h...h...D...D.......
....P.td............4...4...........Q.td
............................R.td........
..................../lib/ld-linux.so.2..
............GNU.........................
....GNU....F*QLk$,.....)..Yl............
一旦您启动该进程,ld-linux 加载程序首先会检查您需要(依赖)哪些共享库以及它们是否可用。如果您依赖某些不可用的共享库,ld-linux 将不会加载进程(ld-linux 会查看您的 LD_LIBRARY_PATH 环境变量、/etc/ld.so.cache 文件,最后查看默认路径:/lib 和/usr/lib(man ld-linux 了解更多信息)。
一旦 ld-linux 确保所有的库都在那里,它就会分配内存来加载进程。通常一个可执行文件有几个段,为简单起见,我们可以将它们简化为文本(代码)、bss(未初始化数据)、数据(初始化和静态数据)。当进程被加载到内存中时,加载器保留保存所有这些部分所需的内存量,并将进程所依赖的所有共享库映射到进程的虚拟空间。可以通过咨询查看linux中某个进程的map列表:
cat /proc/pid_of_process/maps
如果我运行上面简单程序的修改版本(通过添加对 usleep 的调用以获取进程 pid)并检查其映射,我们会得到以下内容(_ 只是为了隐藏真正的路径我的家出现了):
003a5000-003a6000 r-xp 00000000 00:00 0 [vdso]
0075a000-008fd000 r-xp 00000000 08:03 2137894 /lib/i386-linux-gnu/libc-2.15.so
008fd000-008ff000 r--p 001a3000 08:03 2137894 /lib/i386-linux-gnu/libc-2.15.so
008ff000-00900000 rw-p 001a5000 08:03 2137894 /lib/i386-linux-gnu/libc-2.15.so
00900000-00903000 rw-p 00000000 00:00 0
00e4a000-00e6a000 r-xp 00000000 08:03 2137906 /lib/i386-linux-gnu/ld-2.15.so
00e6a000-00e6b000 r--p 0001f000 08:03 2137906 /lib/i386-linux-gnu/ld-2.15.so
00e6b000-00e6c000 rw-p 00020000 08:03 2137906 /lib/i386-linux-gnu/ld-2.15.so
08048000-08049000 r-xp 00000000 08:05 3589145 /______________/test/a.out
08049000-0804a000 r--p 00000000 08:05 3589145 /______________/test/a.out
0804a000-0804b000 rw-p 00001000 08:05 3589145 /______________/test/a.out
b771f000-b7720000 rw-p 00000000 00:00 0
b7745000-b7747000 rw-p 00000000 00:00 0
bf884000-bf8a5000 rw-p 00000000 00:00 0 [stack]
这实际上是进程的虚拟内存映射。这些页面映射到物理内存,每个进程都有自己的 PMT(程序映射表),用于在虚拟地址和物理地址之间进行转换。一般来说,进程内存的布局如下:
(来自http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/)
所以,记住这些信息并回到你原来的问题,
那么,问题来了,如果一开始就分配了0x0到0xMAX,是不是一开始就分配了0x0到0xMAX大于3GB(因为有栈,控制...)?
答案是没有这样的保留。加载程序保留运行进程所需的物理内存。之后,根据进程需要(动态内存分配)及其行为,它的堆和堆栈区域可能会增长和缩小。每次进程需要访问一些实际不存在于物理内存中的内存(虚拟)时,就会发出一个页面错误,并将该页面从磁盘加载到物理内存中的保留位置。有时为了做到这一点,内核必须将属于另一个进程的一些页面换出到磁盘。物理内存是一种有限的资源,操作系统必须正确处理它才能负担所有正在运行的进程。
使用这种策略,Linux 内核能够运行多个进程,其中每个进程通常在物理内存中(特别是在过去)中具有 4GB(32 位系统)的虚拟内存。通常,即使您动态保留内存(例如通过使用 malloc),调用也会成功,但实际上您还没有保留此物理内存。一旦尝试使用它(通过读取或写入此内存),您的进程就会得到它。
这可能是一个很长的答案。我希望我没有遗漏很多细节,希望它可以帮助您了解 linux 中进程内存的解剖结构。