为啥Linux上的动态链接可执行文件在自己的内存空间中拥有libc的完整内存空间?
Posted
技术标签:
【中文标题】为啥Linux上的动态链接可执行文件在自己的内存空间中拥有libc的完整内存空间?【英文标题】:Why does a dynamically linked executable on Linux have the complete memory space of libc in its own memory space?为什么Linux上的动态链接可执行文件在自己的内存空间中拥有libc的完整内存空间? 【发布时间】:2014-11-20 15:50:08 【问题描述】:我在 64 位 Ubuntu 系统上使用 gcc -Wall -m32 test.c -o test
编译了以下 C 代码:
#include <stdio.h>
#include <stdlib.h>
int main()
char * buffer;
buffer = (char*) malloc (1048576);
printf("hi\n");
sleep(20);
return 0;
现在,当我运行代码并执行cat /proc/PID/maps
来查看进程正在使用的虚拟内存范围时,我看到以下内容:
08048000-08049000 r-xp 00000000 08:06 3805439 /home/me/test
08049000-0804a000 r--p 00000000 08:06 3805439 /home/me/test
0804a000-0804b000 rw-p 00001000 08:06 3805439 /home/me/test
f7475000-f7577000 rw-p 00000000 00:00 0
f7577000-f7720000 r-xp 00000000 08:06 8002662 /lib/i386-linux-gnu/libc-2.19.so
f7720000-f7721000 ---p 001a9000 08:06 8002662 /lib/i386-linux-gnu/libc-2.19.so
f7721000-f7723000 r--p 001a9000 08:06 8002662 /lib/i386-linux-gnu/libc-2.19.so
f7723000-f7724000 rw-p 001ab000 08:06 8002662 /lib/i386-linux-gnu/libc-2.19.so
f7724000-f7727000 rw-p 00000000 00:00 0
f7746000-f7748000 rw-p 00000000 00:00 0
f7748000-f7749000 r-xp 00000000 00:00 0 [vdso]
f7749000-f7769000 r-xp 00000000 08:06 8002671 /lib/i386-linux-gnu/ld-2.19.so
f7769000-f776a000 r--p 0001f000 08:06 8002671 /lib/i386-linux-gnu/ld-2.19.so
f776a000-f776b000 rw-p 00020000 08:06 8002671 /lib/i386-linux-gnu/ld-2.19.so
ffa60000-ffa81000 rw-p 00000000 00:00 0 [stack]
所以代码区域在 08048000 和 0804b000 之间,那么 f7475000-f7577000 中的缓冲区的堆上有 1048576 字节。但是在 f7577000 和 f7724000 之间,动态链接的 libc 大约有 1758972 字节(这几乎是 HDD 上库的大小)。这是为什么? ld 低一点也是一样。
为什么系统将整个 libc 和 ld 共享对象映射到进程的内存范围?我以为只有一个指向 libc 的指针,它只在系统范围内加载一次?
此外,我绝对不需要记忆中的整个 1758972 字节。这里发生了什么?
/lib/i386-linux-gnu/libc-2.19.so 在系统范围内是否只在内存中一次?
【问题讨论】:
MMU 很棒。 精彩评论,通过 MMU 内存映射实现共享对象的魔力。 【参考方案1】:为什么系统将整个 libc 和 ld 共享对象映射到进程的内存范围?我以为只有一个指向 libc 的指针,它只在系统范围内加载一次?
它会在系统范围内映射一次,然后将这些页面映射到每个进程的虚拟内存地址空间。这些页面由每个进程共享(至少是只读部分)
你不能只拥有一个“指针”,因为指针只能引用进程自己的地址空间中的东西,所以如果一个库不在那个地址空间中,你将如何取消引用指针?这也意味着该过程需要说“好的,我想要这个函数,它在我的地址空间中吗?不,但我有一个指针,所以遵循它”,这将更加复杂。相反,操作系统和 MMU 硬件执行所需的间接和映射,使其显示为每个进程的单个平面地址空间。
此外,我绝对不需要记忆中的整个 1758972 字节。这里发生了什么?
由于使用 libc.so 的每个进程都获取相同的页面,因此只需将整个内容映射一次并共享它,而不是弄清楚每个进程需要哪些特定页面,效率要高得多。
/lib/i386-linux-gnu/libc-2.19.so 在系统范围内是否只在内存中一次?
是的,因为相同的页面被映射到每个进程中。
这一切都适用于任何 ELF 共享库,而不仅仅是 libc.so
【讨论】:
首先,它必须映射到每个进程的内存范围。并且它不必在每个进程中始终位于相同的地址;这就是-fPIC
的用途。并且整个库会总是映射到每个进程的地址空间中,而不管是否共享;映射.so
的粒度是文件。最后,只有实际使用的页面才会在物理内存中。
完全同意,虽然我不是说它有相同的地址,也看不出我在哪里暗示过。
@JonathanWakely 这是如何在内核中实现的?有没有类似页表的东西存储虚拟地址和物理地址的映射关系?
IIUC 动态链接器使用mmap
将共享库的只读部分加载到进程的地址空间中。内核足够聪明,可以知道同一文件的相同部分是否已经mmap
ed 到内存中,因此现有页面可以由不同的进程共享。 akkadia.org/drepper/dsohowto.pdf 有很多关于动态链接器如何知道库的哪些部分可以安全地以只读方式加载和共享的详细信息。【参考方案2】:
你必须能够访问libc.so
中的元素,所以它必须是
映射到你的记忆中。您无法访问任何未映射的内容。
至于出现一次还是多次,你得解释
正是你的意思。我将被映射到每个地址空间
使用它的进程(几乎是每个进程)。但只有
每个过程中实际使用的部分实际上是
加载到主存中。而text
段将直接映射
来自.so
文件,在交换区域中没有任何后备存储。 (一世
至少相信。我从未真正看过 Linux,但这是
大多数虚拟内存的工作方式。这就是为什么库中的代码通常会
必须用-fPIC
编译。)
【讨论】:
【参考方案3】:我以为只有一个指向加载的 libc 的指针 全系统内存一次?
这样的指针会指向什么?指针只能指向你内存空间中的东西。
不过,它实际上在内存中只有一次在系统范围内。通过内存管理的魔力,它可以出现在多个进程的内存中,但实际上只出现在内存中一次。
仅映射您实际使用的 libc 部分在理论上似乎是可行的,但可能不值得这么复杂,因为在 64 位机器上没有任何好处。这将涉及对处理共享库的机制的重大添加。
【讨论】:
以上是关于为啥Linux上的动态链接可执行文件在自己的内存空间中拥有libc的完整内存空间?的主要内容,如果未能解决你的问题,请参考以下文章