【问题标题】:Executable Object Files and Virtual Memory可执行目标文件和虚拟内存
【发布时间】:2019-04-28 18:01:05
【问题描述】:

我是 Linux 和虚拟内存的初学者,仍在努力理解虚拟内存和可执行对象文件之间的关系。

假设我们有一个存储在硬盘上的可执行目标文件 a.out,假设最初 a.out 有一个带有全局变量的 .data 部分值为 2018 的变量。 当加载器运行时,它分配一个连续的虚拟页面块,将它们标记为无效(即未缓存)并将它们的页表条目指向 a.out 中的适当位置。加载程序实际上从不将任何数据从磁盘复制到内存中。第一次引用每个页面时,虚拟内存系统会根据需要自动调入数据。

我的问题是:假设程序在运行时将全局变量的值从 2018 更改为 2019,似乎包含全局变量的虚拟页面最终会分页到磁盘,这意味着 .data 部分的全局变量现在是 2019,所以我们更改了不应该更改的可执行目标文件?否则每次我们完成并再次运行程序时都会得到不同的值?

【问题讨论】:

    标签: linux linker operating-system executable virtual-memory


    【解决方案1】:

    一般情况下(并非专门针对 Linux)...

    当一个可执行文件启动时,操作系统(内核)创建一个虚拟地址空间和一个(最初为空的)进程,并检查可执行文件的标题。可执行文件的标题描述了“节”(例如.text.rodata.data.bss 等),其中每个节都有不同的属性——如果节的内容应该放在虚拟地址空间中,或者不是(例如是符号表或在运行时不使用的东西),内容是否是文件的一部分(例如.bss),以及该区域是否应该是可执行的、只读的或读取的/写。

    通常,可执行文件(使用的部分)由虚拟文件系统缓存;并且已经在 VFS 缓存中的文件片段可以映射(作为“写入时复制”)到新进程的虚拟地址空间。对于不在 VFS 缓存中的部分,文件的这些部分可以作为“需要获取”映射到新进程的虚拟地址空间中。

    然后启动进程(给定 CPU 时间)。

    如果进程从尚未加载的页面读取数据;操作系统(内核)暂停进程,从磁盘上的文件中获取页面到 VFS 缓存中,然后还将页面作为“写入时复制”映射到进程中;然后允许进程继续(允许进程从未加载的页面重试读取,这将在页面加载后工作)。

    如果进程写入仍然是“写时复制”的页面;操作系统(内核)暂停进程,分配一个新页面并将原始页面的数据复制到其中,然后用进程自己的副本替换原始页面;然后允许进程继续(允许进程重试写入,现在该进程拥有自己的副本)。

    如果进程从尚未加载的页面写入数据;操作系统(内核)结合了前面的两件事(从磁盘获取原始页面到 VFS 缓存,创建一个副本,将进程的副本映射到进程的虚拟地址空间)。

    如果操作系统开始耗尽可用 RAM;那么:

    • 在 VFS 缓存中但不与任何进程共享为“写入时复制”的文件数据页可以在 VFS 中释放,而无需执行任何其他操作。下次使用该文件时,这些页面将从磁盘上的文件中提取到 VFS 缓存中。

    • VFS 缓存中的文件数据页,也可作为“写入时复制”与任何进程共享,可以在 VFS 中释放,任何/所有进程中的副本标记为“尚未获取” .下次使用文件时(包括当进程访问“尚未获取”页面时),这些页面将从磁盘上的文件中获取到 VFS 缓存中,然后在进程中映射为“写入时复制” )。

    • 已修改的数据页(或者因为它们最初是“写入时复制”但被复制了,或者因为它们根本不是可执行文件的一部分 - 例如.bss 部分,可执行文件的堆空间等)可以保存到交换空间然后释放。当进程再次访问页面时,它们将从交换空间中获取。

    注意:如果可执行文件存储在不可靠的介质上(例如可能被划伤的 CD),“比平均水平更智能”的操作系统最初可能会将整个可执行文件加载到 VFS 缓存和/或交换空间中;因为除了使进程崩溃(例如SIGSEGV)之外,没有理智的方法来处理“从内存映射文件中读取错误”,而不是使进程崩溃(例如SIGSEGV)并使它看起来像可执行文件不是错误的,并且因为这提高了可靠性(因为您依赖于更可靠的交换,而不是依赖于不太可靠的划痕 CD)。还;如果操作系统防止文件损坏或恶意软件(例如,在可执行文件中内置了 CRC 或数字签名),那么操作系统可能(应该)将所有内容加载到内存(VFS 缓存)以在允许可执行文件之前检查 CRC 或数字签名执行,并且(对于安全系统,如果磁盘上的文件在可执行文件运行时被修改)释放 RAM 时可能会将未修改的页面存储在“更受信任的”交换空间中(与修改页面时相同)以避免从原始“不太受信任”文件中获取数据(部分原因是您不想在每次从文件加载页面时都进行整个数字签名检查)。

    我的问题是:假设程序在运行时将全局变量的值从 2018 更改为 2019,并且似乎包含全局变量的虚拟页面最终会分页到磁盘,这意味着 .data 部分现在全局变量是 2019 了,所以我们更改了不应该更改的可执行目标文件?

    包含2018 的页面将以“未获取”开始,然后(在访问时)加载到 VFS 缓存中并作为“写入时复制”映射到进程中。如果再次需要,操作系统可能会释放内存并从磁盘上的可执行文件中获取数据(未更改的数据)。

    当进程修改全局变量(将其更改为包含2019)时,操作系统会为进程创建它的副本。此后,如果操作系统想要释放内存,则操作系统需要将页面数据保存在交换空间中,如果再次访问,则从交换空间加载页面数据。可执行文件不会被修改,并且(对于该页面,对于该进程)该可执行文件不会再次使用。

    【讨论】:

    • 感谢您的详细解答。仍然是一个问题,如果两个进程 A 和 B 映射一个私有对象。私有区域的页表条目被标记为只读。如果 B 试图写入私有对象,它会触发保护故障处理程序复制 t 在物理内存中创建页面的新副本,更新 B 的页表条目以指向新副本。但是由于页表条目被标记为只读,这意味着我们不应该向这个对象写入任何内容,而是内核创建一个新页面供我们写入,这不是相互矛盾吗?
    • @amjad:用于“写入时复制”;通常页面最初被映射为只读,然后当进程写入它时,它会导致保护/页面错误,并且保护/页面错误处理程序创建一个副本并将副本映射到与读/写相同的位置(不读取-only) 以便进程可以写入其副本。
    • 感谢您的回答。另一个问题是,VM 是如何知道在 page out 时要写入磁盘的哪个部分/部分?由于页表条目只有物理页号,VM 如何知道将这个页放回磁盘的位置?内核是否也跟踪 RAM 中的磁盘号?
    • @amjad:我不确定 Linux 如何处理交换空间。对于我的操作系统设计,内核要求“交换管理器”分配和存储数据,“交换管理器”返回一个不透明的句柄;内核将此句柄存储在物理地址所在的页表条目中(页表条目从“存在+物理地址”变为“不存在+句柄”)。当再次访问时(由于“不存在”导致页面错误)内核从页表条目获取句柄,要求交换管理器将数据加载到任何物理页面,然后将页表条目更改为“存在+新物理地址”。
    • @amjad:我知道fork() 应该如何工作(“写入时复制”,每个页面都有某种“用户数”计数器,以跟踪页面被使用的位置) ;但我不知道它在 Linux 中如何工作的细节(例如mm_struct 等)。我只是假设 Linux 中的一切都始于(在 1990 年代)作为糟糕的旧操作系统设计(从 1960 年代开始)的低质量实现,其中实现经历了 3 年的“被百万猴子捣毁”以达到你今天看到的质量;旧的操作系统设计应该在 1980 年代不复存在,但由于非技术原因而没有;-)
    猜你喜欢
    • 2017-10-02
    • 1970-01-01
    • 1970-01-01
    • 2013-01-13
    • 2021-05-06
    • 2015-03-29
    • 1970-01-01
    • 2020-05-21
    • 2020-04-16
    相关资源
    最近更新 更多