当执行文件在执行过程中被替换,如何处理"/proc/self/exe"的readlink()函数?

12

在我的C++应用程序中,我的应用程序在一个fork()的子进程中执行execv()来使用相同的可执行文件以不同参数处理一些工作,并通过管道与父进程通信。为了获取自身的路径名,在Linux端口上我执行以下代码(在Macintosh上有不同的代码):

  const size_t bufSize = PATH_MAX + 1;
  char dirNameBuffer[bufSize];
  // Read the symbolic link '/proc/self/exe'.
  const char *linkName = "/proc/self/exe";
  const int ret = int(readlink(linkName, dirNameBuffer, bufSize - 1));

然而,如果在可执行文件运行时,我将磁盘上的二进制文件替换为更新版本,则readlink()字符串结果为:"/usr/local/bin/myExecutable(已删除)"

我知道我的可执行文件已被新版本替换,并且用于/proc/self/exe的原始文件已经被替换,但是当我尝试使用execv()时,由于结果中有额外的后缀"(已删除)",它会失败并显示errno 2 - No such file or directory.

我想让execv()要么使用旧的自身可执行文件,要么使用更新的可执行文件。我可以检测到以"(已删除)"结尾的字符串,并修改它以省略该部分并解析为更新的可执行文件,但我认为这种方法有些笨拙。

当正在执行的原始可执行文件被更新为新版本时,如何使用一组新参数执行当前可执行文件(或其替换品)的execv()函数?


1
为什么不总是使用传递给mainargv[0] - Jonathon Reinhart
1
由于argv [0]并不总是给出可执行文件的路径名,而可能仅命名可执行文件,因此@JonathonReinhart。 - WilliamKF
我本来想说,在调用main()时,getcwd + argv[0]应该就足够了。但我想也许有可能创建一个不会以这种方式执行你的shell。 - Jonathon Reinhart
1
你可不可以在fork之后放弃使用execv?你可以直接调用想要跳转的任何函数,而不是重新执行同一个程序。 - John Kugelman
1
如果有人从以(删除)结尾的路径名运行您的程序,则修剪(删除)将无效。 - user253751
显示剩余4条评论
3个回答

9

不必使用readlink来查找自己的可执行文件路径,可以直接调用/proc/self/exe上的open。由于内核已经对当前正在执行的进程有一个打开的fd,因此这将给您一个fd,无论路径是否已被替换为新的可执行文件。

接下来,您可以使用fexecve代替execv,它接受一个fd参数作为可执行文件,而不是filename参数。

int fd = open("/proc/self/exe", O_RDONLY);
fexecve(fd, argv, envp);

上面的代码为了简洁起见省略了错误处理。

2
还没有确认的机会,但是读起来听起来很棒! - WilliamKF

3

一种解决方法是在可执行程序启动时(例如在 main() 开始附近)读取链接 /proc/self/exe 的值一次,并将其静态存储以备将来使用:

  static string savedBinary;
  static bool initialized = false;

  // To deal with issue of long running executable having its binary replaced
  // with a newer one on disk, we compute the resolved binary once at startup.
  if (!initialized) {
    const size_t bufSize = PATH_MAX + 1;
    char dirNameBuffer[bufSize];
    // Read the symbolic link '/proc/self/exe'.
    const char *linkName = "/proc/self/exe";
    const int ret = int(readlink(linkName, dirNameBuffer, bufSize - 1));

    savedBinary = dirNameBuffer;

    // On at least Linux, if the executable is replaced, readlink() of
    // "/proc/self/exe" gives "/usr/local/bin/flume (deleted)".
    // Therefore, we just compute the binary location statically once at
    // startup, before it can possibly be replaced, but we leave this code
    // here as an extra precaution.
    const string deleted(" (deleted)");
    const size_t deletedSize = deleted.size();
    const size_t pathSize = savedBinary.size();

    if (pathSize > deletedSize) {
      const size_t matchPos = pathSize - deletedSize;

      if (0 == savedBinary.compare(matchPos, deletedSize, deleted)) {
        // Deleted original binary, Issue warning, throw an exception, or exit.
        // Or cludge the original path with: savedBinary.erase(matchPos);
      }
    }
    initialized = true;
  }

  // Use savedBinary value.

以这种方式,很不可能在主函数将其二进制路径缓存为微秒内替换原始可执行文件。因此,长时间运行的应用程序(例如数小时或数天)可能会在磁盘上被替换,但根据最初的问题,它可以使用fork()execv()更新的二进制文件,该文件可能已经修复了一个错误。这具有跨平台工作的附加好处,因此不同的Macintosh代码读取二进制路径也可以在启动后受到保护。 警告编辑注意: readlink不会将字符串置空,因此在调用readlink之前没有用零填充缓冲区的情况下,上述程序可能会意外失效。

不太可能并不意味着不可能。对于这种情况,我仍然会进行错误检查。但这很酷。 - inetknght
我相信我有一个完美的解决方案 - 请看我刚刚添加的答案。 - andrewrk
@andrewrk 谢谢,我喜欢你的解决方案。你已经确认它在可执行文件中可以工作吗?目前我无法测试,但是通过阅读,我的眼睛告诉我它可以工作! - WilliamKF
我还没有测试过它,只是从查看手册和在IRC上询问的结果拼凑起来的。 - andrewrk

1
你得到符号链接中的 (deleted) 部分的原因是,你已经用不同的文件替换了正确的程序二进制文本,可执行文件的符号链接再也不合法了。假设你使用这个符号链接来获取此程序的符号表或加载嵌入在其中的某些数据,并且你更改了该程序...那么表格将不正确,甚至可能导致程序崩溃。正在执行的程序的可执行文件不再可用(你已经删除它),而你放置在其位置的程序与你正在执行的二进制文件不对应。
当你 unlink(2) 一个正在执行的程序时,内核会标记 /proc 中的该符号链接,以便程序可以:
  • 检测到二进制文件已被删除且不再可访问。
  • 仍然允许你收集它最后一次拥有的一些信息(而不是从 /proc 树中删除符号链接)。

在内核执行的文件中,您无法写入文件,但是没有人阻止您删除该文件。 只要您执行它,该文件将继续存在于文件系统中,但是没有名称指向它(一旦进程 exit(2),其空间将被释放)。 在内核内存中的inode计数降至零时,内核才会擦除其内容,这发生在对该文件的所有使用(引用)到期时。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接