假设我有一个共享库a.so
,这是我的可执行文件第一次加载它。我的理解是,在 VMA 的中间,共享库文本部分被映射。我有两个问题;
(1( ld.so 是否要将此共享内存文本部分页面加载到物理内存,然后映射到该进程的 VMA?
(2( 假设启动了第二个可执行文件,它使用相同的共享库a.so
。ld.so 是否要确定此共享库已加载到物理内存中?如何理解这一点?
准确地说,保留物理内存或管理或选择虚拟内存和物理内存之间的映射不是ld.so
的工作,而是内核的工作。当ld.so
加载共享库时,它通过mmap
系统调用执行此操作,内核分配所需的物理内存(1(并在库文件和物理内存之间创建虚拟映射。然后由mmap
返回的是映射库的虚拟基址,然后动态加载程序将使用该基址作为为对该库函数的调用提供服务的基础。
ld.so
是否要确定此共享库已加载到物理内存中?如何理解这一点?
不是ld.so
,而是要识别这一点的内核。这是一个复杂的过程,但为了简单起见,内核会跟踪哪个文件被映射到哪里,并且可以检测何时再次请求映射已经映射的文件,如果可能的话,避免物理内存分配。
如果同一个文件(即具有相同路径的文件(被多次映射,内核将查看现有的映射,如果可能的话,它将重用相同的物理页面以避免浪费内存。因此,理想情况下,如果共享库被多次加载,则只能物理分配一次。
但实际上,事情并没有那么简单。由于内存也可以写入,因此显然,只有当需要共享的页面与文件的原始内容不变时,才会发生物理页面的这种"共享"(否则映射同一文件或库的不同进程会相互干扰(。对于代码部分(.text
(,这基本上总是正确的,因为它们通常是只读的,而对于其他类似的部分(如只读数据(。如果未修改 RW 部分,也可能发生这种情况(2(。简而言之,已加载库的.text
段通常只分配到物理内存中一次。
(1( 实际上,内核首先创建映射,然后只有在进程尝试通过映射读取或写入时才会分配物理内存。这可以防止在不需要内存时浪费内存。
(2( 这种共享物理内存的技术通过写入时复制机制进行管理,在该机制中,内核最初映射"干净"页面,并在写入时将它们标记为"脏",并根据需要复制它们。
Linux 共享库通常是位置独立的(由于地址空间随机化,可执行文件也是如此(。与位置无关的代码使用 GOT 和 PLT 重定位机制在运行时通过外部链接定位所有符号。
当动态链接器ld.so
加载与位置无关的可执行文件或共享库时,只需在进程内存中修改 GOT 和 PLT 段,.text
包含代码的段将作为只读映射到进程内存中。当另一个进程加载相同的可执行文件或共享库时,它最终会映射相同的页面框架,其中.text
段已由其他进程加载,但不是特定于进程地址空间布局的 GOT 和 PLT。
ld.so
基本上在加载 ELF 文件时会mmap(NULL, text_segment_len, PROT_EXEC | PROT_READ, MAP_SHARED, elf_file_fd, text_segment_offset)
。这将填充内核页面缓存(在第一次访问时(,以便进行此调用的另一个进程映射页面缓存中已存在的页面框架。
有关详细信息,请参阅与位置无关的代码。