【问题标题】:What happens when you overwrite a memory-mapped executable?当您覆盖内存映射的可执行文件时会发生什么?
【发布时间】:2011-05-26 02:58:11
【问题描述】:

根据我的一个问题,我很想知道当一个人覆盖可执行文件时会发生什么。我需要检查我对此事的理解。

假设我有/usr/bin/myprog。我运行它,所以操作系统加载了/usr/bin/myprog,可能是通过http://en.wikipedia.org/wiki/Memory-mapped_file#Common_uses

无论出于何种原因,该进程仍保留在内存中,我决定实际上我已经修复了一个错误并覆盖了/usr/bin/myprog

所以,据我所知:

  • 如果myprog 的实例已加载,并且我替换了已加载myprog 的文件,则myprog 的实例未修改。
  • 如果我运行 myprog 的新实例,它将使用新代码。

我说的对吗?

但是,根据有关内存映射文件的文章,这种技术允许开发人员将文件的某些部分视为物理内存。

因此,我发现自己理解事物的方式存在矛盾。如果页面真的只是按需加载,那么假设 myprog 不是 100% 分页,这篇维基百科文章暗示新页面将从磁盘上的文件中加载,该文件在加载原始图像后发生了变化。

但是,我很确定我的两个编译图像不会相同,并且每个文件的相关地址偏移量也不相同。因此,假设发生这种情况,指令指针将会丢失……我很确定操作系统不会将两个不同图像的一部分作为同一进程的一部分加载到内存中。

那么内存映射/需求分页的组合如何用于程序的执行呢?覆盖该文件是否会在每个可执行文件的页面上触发页面错误以确保为当前运行的进程加载它?

我对此做了一个快速实验:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char** argv)
{
    printf("Program resident...");
    while(1)
    {
        printf("??? Just notifying you I'm still here...\n");
        usleep(1000000);
    }

    return 0;
}

果然我可以 a) 在它运行时替换这个可执行文件 b) 它的输出没有改变。

那么发生了什么?对于我可以做的事情(Linux 或 Windows)的任何建议,我将特别感谢。

谢谢大家。

编辑:我所指的问题引发了这个问题:Upgrades without reboot - what kinds of problems happen in practice?

另外,我知道这与编程无关,而是更新可执行文件的结果。不过,我仍然很感兴趣,我想不出比这更好的地方了。

【问题讨论】:

    标签: operating-system execution


    【解决方案1】:

    我发现这个链接的解释更加简洁。查看“更新”下作者更新原始帖子的部分。

    http://it.toolbox.com/blogs/locutus/why-linux-can-be-updated-without-rebooting-12826

    整个文件不会加载到内存中,它们会被读入集群中的缓冲区(这是一个技术术语,通常为 4k,但您可以在设置文件系统时进行设置)。

    当您打开一个文件时,内核会跟随链接,并为 inode 分配一个文件描述符(它在内部跟踪的一个数字)。当您删除文件时,您正在“取消链接”inode;文件描述符仍然指向它。您可以在删除旧文件后创建一个与旧文件同名的新文件,有效地“替换”它,但它会指向不同的 inode。任何仍然打开旧文件的程序仍然可以通过文件描述符访问旧文件,但是您已经有效地升级了程序。一旦程序终止(或关闭文件)并启动(或尝试再次访问它),它就会访问新文件,并且您拥有它,完全就地替换文件!

    因此,在 Linux 中,如您所说,您的可执行文件可能只能按需逐页读取,但它是通过原始打开的文件句柄读取的,而不是通过替换您正在运行的可执行程序的新文件的更新 inode 读取。

    【讨论】:

      【解决方案2】:
      1. 会发生什么首先取决于你是否rm /usr/bin/myprog然后创建一个新的,或者你是否将open()write()转换为现有的/usr/bin/myprog

        李>
      2. 如果你 rm 旧的 /usr/bin/myprog 文件,然后创建一个新的同名文件,那么内核/文件系统驱动程序会为新版本提供一个新的 inode,而旧的 inode 仍然存在在/proc 文件系统中,直到打开它的进程关闭它。您现有的进程/usr/bin/myprog 有它自己的私有版本的文件,未经修改,直到它close()s 文件描述符。

      3. 所有操作系统(Windows、Linux,可能是 OS X)都使用按需分页内存映射(mmap() 用于 posix,我不记得 Windows 的等价物 - VirtualAlloc()?)用于进程加载。 这样,可执行文件中未被触及的任何部分都不会加载到内存中。

      4. 1234563 ,那么这两个进程本质上将查看相同的物理内存页面,并且假设它们都称为mmap()PROT_READ | PROT_WRITE,他们会看到彼此的修改。
      5. 如果这是一个传统的mmap()'d 文件,并且进程 1 已经打开/映射了它,然后进程 2 开始通过write() 摆弄硬盘本身上的文件调用(不是我的mmaping),进程 1 确实看到了这些变化。我猜内核注意到文件正在被修改并重新加载受影响的页面。

      6. 我不知道可执行映像是否有任何特殊的mmap() 行为?如果我破解了指向我的一个函数的指针并修改了代码,它会将页面标记为脏吗?脏页会被写回/usr/bin/myprog吗?当我尝试这个时,它会出现段错误,所以我猜虽然 _TEXT 页面使用MAP_SHARED 映射,但它们也可能没有得到PROT_WRITE,因此在写入时会出现段错误。当然,_DATA 部分也被加载到内存中,并且需要修改这些部分,但是可以将它们标记为 MAP_PRIVATE(写时复制)-因此它们可能不会保持与 /usr/bin/myprog 文件的连接.

      7. 第 6 点涉及直接修改自身的可执行文件。第 5 点涉及在write() 级别修改任意mmap()d 文件。当我尝试使用write() 在另一个进程中修改可执行文件(mmap()'d)时,我没有得到与第 5 点相同的结果。我可以使用裸机对可执行文件进行各种可怕的更改write() 打电话,什么也没发生。然后,当我退出进程并尝试再次运行它时,它会崩溃(当然 - 在我对可执行文件所做的一切之后)。这让我很困惑。我无法将参数置换为 mmap() 以使其以这种方式运行 - 不是写时复制,但不受映射文件更改的影响。

      8. 好吧,我回到圣经 (Stevens),最大的问题是 MAP_PRIVATEMAP_SHAREDMAP_PRIVATE 是写时复制,MAP_SHARED 不是。 MAP_PRIVATE 将在您对映射页面进行修改后立即对其进行复制。未定义对原始文件的修改是否会传播到映射的MAP_PRIVATE 页面,但对于 OS X,它们不会。 MAP_SHARED 保持与原始文件的连接,允许对文件的更新传播到内存页面,反之亦然。如果一个内存块被映射MAP_PRIVATE,那么你对它所做的任何修改都不会被写入磁盘。 MAP_SHARED OTOH 允许通过写入映射页面来修改文件。

      9. 图像加载器将可执行文件映射为MAP_PRIVATE。这解释了第 6 点中的行为 - 破解指向函数代码的指针然后修改它,即使您有权这样做,也不会将数据写回磁盘。从理论上讲,应该可以在操作系统映像加载器mmap() 之后立即更改可执行文件/usr/bin/myprog,但是每当我查看带有vmmap 的非常大的可执行文件时,它们的TEXT 部分似乎总是完全驻留。我不知道这是否是因为 OS X 的图像加载器会触及所有页面以确保它们被复制,或者 OS X 的页面管理器是否只是非常积极地使页面常驻(确实如此),但我没有能够在main() 启动后立即在其 TEXT 部分未完全驻留的 OS X 中生成可执行文件。

      10. OS X 图像加载器在加载映射页面方面非常激进。我注意到,当mmap()'ing 一个文件时,它必须非常大,然后 OS X 才会决定将其中的任何一个保留为非驻留。一个 1GB 的文件被完全加载,但一个 3GB 的文件只有大约 1.7GB 被常驻。这是在具有 8GB 物理 RAM 并运行 64 位 OS X 内核的机器上。

      【讨论】:

      • +1 我认为我的努力。 +2 如果我能给它。我也很困惑;对于正在发生的事情,我还没有找到一个像样的解释。我期待同样的事情 - 我知道我可以重新编译 /usr/bin/myprog 并且我所做的任何更改都不会反映在正在运行的内容中,但一切都表明按需加载。我一直在查看 linux 树,看看是否可以找出发生了什么(如果制作了副本等)。
      【解决方案3】:

      在 Linux 下,如果您在运行时替换可执行文件,结果是不可预知的,并且可能会崩溃。已修改的页面(例如“bss”初始化数据)不会受到影响,但尚未修改的页面(例如大多数代码)会受到影响。

      我的猜测是,在您的情况下,字符串位于修改(复制)页面的部分中,因此不受影响。

      然而,只有当你真正覆盖同一个文件时,所有这一切才会发生。

      大多数时候,当您替换一个可执行文件时,您将用一个不同的文件替换目录条目。这通常通过在现有文件上重命名临时文件(在同一目录中)来完成。这就是(例如)包管理器所做的。

      在替换目录条目的情况下,前一个可执行文件继续作为一个完全独立(仍在执行)的文件存在,并且前一个可执行文件可以毫无问题地丢弃其页面并重新加载 - 它仍然可以看到旧文件.

      链接器对其输出做了什么,我不知道。但是 /usr/bin/install 会创建一个新文件。我认为这种行为是故意的。

      【讨论】:

      • 我明白了。所以事实上包管理器是故意意识到这个过程......很有趣。好吧。我认为这回答了它,结合 Ted 的回答,我现在对实际发生的事情有了更多的了解。
      • 对于这适用于 OS X(和一般 BSD)以及在 posix 中定义良好的价值,请参阅unlink(2)
      【解决方案4】:

      在 Mac OS X 上,当我升级任何正在运行的应用程序时,升级程序会要求完全关闭应用程序以继续升级,在某些情况下,它会继续,但升级直到应用程序才会生效重新开始。它有时会列出正在使用的导致升级阻塞的库的名称。我这样说是因为,应用程序似乎锁定了它所依赖的库,并且升级程序似乎知道在使用它们时不要碰它们。因此,不要过度编写正在运行的应用程序的任何部分或全部内容。

      例如,Google Chrome 会要求重新启动以安装升级并在 Windows、Linux 和 Mac OS X 上生效。我希望这可以为您提供从哪里开始查找的线索。

      【讨论】:

        【解决方案5】:

        您通常是正确的:二进制文件仅存储可执行文件的图像。在任何特定时刻实际存储在内存中的是副本。

        但是,这些图像通常有几个部分 - 或片段。其中之一存储实际的机器指令,程序的编译代码 - 它总是完全加载。其他部分可能包含静态数据 - 您可能在整个代码中使用的各种常量(尤其是字符串)。这些部分可以由操作系统按需加载。

        【讨论】:

          猜你喜欢
          • 2018-07-15
          • 1970-01-01
          • 2018-07-21
          • 1970-01-01
          • 2015-01-15
          • 2018-02-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多