覆盖内存映射的可执行文件时会发生什么?

时间:2010-12-15 19:04:08

标签: operating-system execution

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

说我有/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?

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

5 个答案:

答案 0 :(得分:10)

  1. 首先取决于您是rm /usr/bin/myprog然后创建一个新的,还是open()write()现有的/usr/bin/myprog }。

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

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

  4. 如果这是传统的mmap()'d文件,并且两个进程都打开/映射它,并且它们都没有在调用中指定MAP_PRIVATE(即写时复制)到mmap(),然后这两个进程将基本上查看相同的物理内存页面,并且提供它们都mmap() PROT_READ | PROT_WRITE,他们会看到彼此的修改。

  5. 如果这是一个传统的mmap()'d文件,并且进程1打开/映射它,然后进程2开始通过{驱动器在硬盘驱动器本身上摆弄文件{1}}调用(不是我的write()),进程1确实看到了这些更改。我猜内核注意到该文件正在被修改并重新加载受影响的页面。

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

  7. 第6点涉及可执行文件直接修改自身。第5点涉及修改MAP_PRIVATE级别的任意/usr/bin/myprog d文件。当我尝试使用mmap()在另一个进程中修改可执行文件(write()'d)时,我得到的结果与第5点不同。我可以对其进行各种可怕的更改带有mmap()个调用的可执行文件,没有任何反应。然后,当我退出进程并尝试再次运行它时,它崩溃了(当然 - 在我对可执行文件做的所有事情之后)。这让我很困惑。我无法将参数置换为write()以使其以这种方式运行 - 不是写入时复制,而是不受映射文件更改的影响。

  8. 好吧,我回到圣经(史蒂文斯),问题是write() vs mmap()MAP_PRIVATE是写时复制而MAP_SHARED不是。只要对其进行修改,MAP_PRIVATE就会复制映射的页面。未定义对原始文件的修改是否会传播到映射的MAP_SHARED页面,但是对于OS X则不会。 MAP_PRIVATE维护与原始文件的连接,允许对文件的更新传播到内存页面,反之亦然。如果映射了内存块MAP_PRIVATE,则不会对其进行任何修改。 MAP_SHARED OTOH允许通过写入映射页面来修改文件。

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

  10. OS X图像加载器非常积极地加载映射页面。我注意到,当vmmap一个文件时,它必须非常大,然后OS X决定让它们中的任何一个不驻留。一个1GB的文件被完全加载,但只有大约1.7GB的3GB文件被占用。这是在具有8GB物理RAM并运行64位OS X内核的计算机上。

答案 1 :(得分:8)

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

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

但是,只有在您实际覆盖同一文件时才会发生这一切。

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

在replacement-directory-entry的情况下,先前的可执行文件继续作为完全独立的(仍在执行的)文件存在,并且先前的可执行文件可以丢弃并重新加载其页面而没有问题 - 它仍然看到旧文件

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

答案 2 :(得分:2)

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

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

  

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

     

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

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

答案 3 :(得分:1)

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

例如,谷歌Chrome要求重新启动安装升级并在Windows,Linux和Mac OS X上生效。我希望能为您提供从何处开始寻找的线索。

答案 4 :(得分:0)

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

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