![覆盖正在运行的可执行文件或 .so](https://linux22.com/image/121726/%E8%A6%86%E7%9B%96%E6%AD%A3%E5%9C%A8%E8%BF%90%E8%A1%8C%E7%9A%84%E5%8F%AF%E6%89%A7%E8%A1%8C%E6%96%87%E4%BB%B6%E6%88%96%20.so.png)
我有一个关于覆盖正在运行的可执行文件或覆盖一个或多个正在运行的程序正在使用的共享库 (.so) 文件的问题。
过去,由于显而易见的原因,覆盖正在运行的可执行文件是行不通的。甚至还有一个特定的 errno 值 ETXTBSY 涵盖了这种情况。
但很长一段时间以来,我注意到,当我不小心尝试覆盖正在运行的可执行文件时(例如,通过启动最后一步位于恰好正在运行的构建上),它会起作用cc -o exefile
!exefile
所以我的问题是,它是如何工作的,是否在任何地方都有记录,以及依赖它是否安全?
看起来有人可能已经调整了ld
取消链接其输出文件并创建一个新文件,只是为了消除这种情况下的错误。我不太清楚它是否一直在这样做,或者仅在需要时才这样做(也就是说,也许在它尝试覆盖现有文件并遇到 ETXTBSY 之后)。我在ld
手册页上没有看到任何提及这一点。 (我想知道为什么人们不抱怨ld
现在可能会破坏他们的硬链接,或更改文件所有权,等等。)
附录:这个问题并不是专门关于cc
/ ld
(尽管这最终成为答案的重要组成部分);问题实际上只是“为什么我再也看不到 ETXTBSY?这仍然是一个错误吗?”答案是,是的,这仍然是一个错误,只是实践中很少见的错误。 (另请参阅我刚刚发布到我自己的问题的澄清答案。)
答案1
它取决于内核,对于某些内核,它可能取决于可执行文件的类型,但我认为所有现代系统都会返回 ETXTBSY(”文本文件busy“)如果您尝试打开正在运行的可执行文件以进行写入或执行打开以进行写入的文件。文档表明BSD 上一直都是这样, 但早期的 Solaris 并非如此(后来的版本确实实现了这种保护),符合我的记忆。 Linux 上一直都是这样,或者至少 1.0。
适用于可执行文件的内容可能适用于动态库,也可能不适用于动态库。覆盖动态库会导致与覆盖可执行文件完全相同的问题:指令将突然从新文件中的相同旧地址加载,而新文件可能具有完全不同的内容。但事实上并非所有地方都是如此。特别是,在 Linux 上,程序调用open
系统调用来在底层打开动态库,其标志与任何数据文件相同,并且 Linux 很高兴地允许您重写库文件,即使正在运行的进程可能会从它加载代码任何时候。
大多数内核允许在执行时删除和重命名文件,就像它们允许在打开或写入时删除和重命名文件一样。就像打开的文件一样,在执行时被删除的文件只要在使用中,即直到可执行文件的最后一个实例退出,实际上都不会从存储介质中删除。 Linux 和 *BSD 允许,但 Solaris 和 HP-UX 不允许。
删除文件并写入同名的新文件是完全安全的:要加载的代码与包含该代码的打开(或正在执行)的文件之间的关联通过文件描述符而不是文件名进行。它还有一个额外的好处,即可以通过写入临时文件然后将该文件移动到位(系统rename
调用以原子方式用源文件替换现有目标文件)来自动完成。它比删除然后打开写入好得多,因为它不会暂时放置无效的、部分写入的可执行文件
是否cc
覆盖ld
其输出文件,或删除它并创建一个新文件,取决于实现。 GCC(至少是现代版本)和 Clang 都是这样做的,在这两种情况下都通过调用unlink
目标(如果存在)open
来创建一个新文件。 (我想知道为什么他们不执行 write-to-temp-then-rename 操作。)
我不建议依赖这种行为,除非作为一种保护措施,因为它不适用于每个系统(它可能适用于可执行文件的每个现代系统,但不适用于共享库),并且通用工具链不会在最好的办法。在构建脚本中,始终在临时文件下生成文件,然后将它们移动到位,除非您知道底层工具会执行此操作。
答案2
如果文件在具有打开的文件句柄时被删除,通常会发生的情况是,一旦最后一个文件句柄关闭,该文件就会被标记为删除。那时该文件将不再出现在目录列表中(例如),但将显示在例如lsof
输出中,标记为已删除但正在使用的文件。
lsof
为了简洁和清晰起见,对下面的内容进行了修剪:
$ cat - >> foo &
[1] 30779
$ lsof | grep 30779
cat 30779 ghoti 1w REG 252,0 0 262155 /home/ghoti/foo
[1]+ Stopped cat - >> foo
$ rm foo
$ ls foo
ls: cannot access 'foo': No such file or directory
$ lsof | grep 30779
cat 30779 ghoti 1w REG 252,0 0 262155 /home/ghoti/foo (deleted)
如果我,我仍然可以在仍在运行的情况下fg
写入(已删除) (实际上,我可以根据需要恢复文件,只要我这样做foo
cat
/proc/30779/fd/1
尽管cat
文件仍然打开时)。
答案3
回答我自己的问题(或者实际上,稍微澄清一下问题,然后解释澄清问题的答案):
我的问题是,“我再也没有看到 ETXTBSY。它仍然是一个错误吗?或者现代内核是否允许您覆盖正在运行的可执行文件而不会抱怨,并且(以某种方式)不会破坏正在运行的可执行文件?”
我开始严重怀疑现代内核在写入正在运行的可执行文件时正在实现某种奇特的写时复制语义。
但事实并非如此。 ETXTBSY 仍然肯定是一个错误。
我的困惑的答案很简单,写入正在运行的可执行文件在实践中几乎不会出现。如果您将一个新的可执行文件移动到位(并且旧的可执行文件仍在运行),您几乎永远不会真正覆盖它;你总是在移除和更换它。如果您正在使用mv
,则您将删除并替换它。如果您使用install
、 或dpkg -i
或类似的内容,则您将删除并替换它。仅当出于某种原因您尝试使用cp
它时,您才会尝试覆盖它,并且如果旧的仍在运行,则可能会面临获得 ETXTBSY 的风险。
然后,由于 的悄然变化ld
,尝试cc -o
在正在运行的可执行文件之上进行操作现在也属于“删除和替换”类别。
答案4
您的猜测是正确的: ld 写入一个新文件,您可以使用ls -li
.该-i
选项显示 inode 编号,该编号在每次编译后都会更改。我认为很多人并不关心破坏硬链接;程序通常不被编译,因此可执行文件由 ld 写入其最终目标。