传递给exec*()函数族的内存会发生什么?

6

我了解当调用 exec*()时,旧进程的内存会被新程序完全替换。但是,那些参数的内存,例如argv呢?如果我的代码像这样,使用C++数据结构,比如 std::string 的内存,是否安全,或者这些变量将会消失,损坏 argv

#include <unistd.h>
#include <string>
#include <string.h>
#include <vector>
#include <iostream>

void
execExample(const std::vector<std::string> &arguments)
{
  char **argv = new char *[arguments.size() + 2];
  char *path = "/path/to/my/executable";
  unsigned int idx = 0;

  argv[idx] = path;

  for (; ++idx < arguments.size() + 1; ) {
    argv[idx] = const_cast<char *>(arguments[idx - 1].c_str());
  }

  argv[idx] = 0;

  execv(path, argv); // Does not return if successful.

  std::cerr << "exec failed: " << strerror(errno) << ".\n";
}
3个回答

4
execv手册页

execv()execvp()execvpe()函数提供了一个表示新程序可用参数列表的指向以空值结尾的字符串指针数组。按照惯例,第一个参数应该指向正在执行的文件名。指针数组必须以一个空指针结尾。[强调添加]

因此,您需要提供一个以空值结尾的C字符串数组。手册没有明确说明内存会发生什么,但假定字符串将被复制到新进程中,就像通过strcpy一样,并且新指针将提供给新进程的main函数。由于execv不可能知道这些字符串的任何上下文信息(它们是静态的?本地的?malloc分配的?),所以我认为极不可能将指针数组浅复制到新进程中。
要回答您的问题,这意味着几乎可以使用任何以空值结尾的char*来源(包括std::string,通过str.c_str()str.data())。值得注意的是,在C++11之前,std::strings不需要以空值结尾,只要c_str成员返回指向空值结尾的字符串的指针即可。我不知道任何未以空值结尾的std::string实现,尽管值得注意的是,与C字符串不同,std::string可能包含\0字符作为字符串数据的一部分,而不是作为终止符。
顺便说一句,execv调用将立即用新进程替换调用进程。这意味着C++析构函数不会被调用。在std::stringstd::vector和任何其他动态内存的情况下,这并不重要-所有分配的内存都会自动回收,因此不会泄漏任何东西。但是,其他副作用也不会发生-std::fstream不会关闭其文件等。通常,这永远不会有影响,因为具有重大副作用的析构函数是不良设计实践,但这是需要注意的。

3
字符串将被复制到新创建的内存空间中。只要在调用exec时它们是有效的,您就不必担心。

嗯,但这些参数是指针。问题在于询问指针的有效性。 - Lightness Races in Orbit
@LightnessRacesinOrbit 通过“the arguments”我指的是逻辑参数,无论它们如何实现。我会更新答案。 - David Schwartz

1
让我们先处理简单的事情:因为进程图像正在被替换,所以std::string的析构函数永远不会被调用,因此内存不会消失(那样)。
我假设你是在询问类UNIX操作系统,因为Windows上不存在unistd.h,所以相关标准是POSIX。它在这个领域故意模糊,并且只规定:

argv[]envp[]指针数组及其指向的字符串,在调用exec函数时除了替换进程图像的结果之外,不得被修改。

这意味着exec将确保参数不会被替换进程图像而失效,但是POSIX并不关心exec如何实现这一点。这是您可以依赖的部分:您的参数将保持有效,不会变得损坏。
关于"实际操作中:" POSIX确实有一个想法,即当标准被编写时实现如何执行,而最近的实现并没有真正改变基本机制。 让我们在字里行间读一下:

可用于新进程的组合参数和环境列表的字节数为{ARG_MAX}。

ARG_MAX被定义here为最小值4096。

如果我们假设为参数和环境分配了固定大小的空间(或者至少可以增长到固定的最大大小),则此要求是有意义的,并且只有在进程映像被替换之前将参数复制到该空间才有意义。 POSIX不会强求这样做,但是有隐含的假设存在,事实上这是许多(也许所有)系统使用的方式。 此外,它们通常(或许总是)以相同的方式执行。

让我们看看Linux。取以下两个程序foo

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

int main() {
  char *p = strdup("foobar");
  printf("%p\n", p);

  execl("bar", "bar", p, NULL);
}

and bar:

#include <stdio.h>

int main(int argc, char *argv[]) {
  printf("%p\n", argv[1]);
  return 0;
}

调用foo会在x86-64 Linux上输出:

0x7f6010
0x7fffbefd6ae5

意思是我传递的字符串在exec期间改变了位置。地址
0x7fffbefd6ae5

该程序位于主线程的调用栈顶部(由ASLR向下移动了一些,以 0x7fffffffffff 为基准)。在Linux上发生的情况(您可以使用gdb查看)是将参数直接复制到此区域中,如果使用“bar baz qux xyzzy”调用程序,则会有一个包含 "bar\0baz\0qux\0xyzzy" 的内存区域,然后将它们的指针放入同一区域的指针数组中,并将指向该数组的指针传递给main函数。(环境也会被复制到该区域,但这不是问题的一部分。)

在Linux上,该区域沿内存页边界分配;直到Linux 2.6.31,它最多可以增长到32个页面(128 KB)。自2.6.32以来,限制为堆栈大小的四分之一(由ulimit确定)。

让我们来看看FreeBSD:使用相同的程序,在i386 FreeBSD 9.1上输出如下:

0x28404050
0xbfbfee58

了解到FreeBSD的堆栈从0xbfc00000开始(在9.1中尚未启用ASLR),我们可以看到这里发生了同样的事情。FreeBSD使用固定的最大大小为256KB,MacOS X也是如此。如果您有兴趣,可以在这里找到一个相当长的历史操作系统列表;它们基本上都以相同的方式完成。实际上,我不知道有哪个符合POSIX标准的系统会以其他方式执行。这样的系统理论上可能存在;据我所知,实际上不存在。

关于Windows的简要介绍:它似乎也在做同样的事情;经过几次尝试,在barargv[1]直接位于execl之后堆栈顶部的argv之后,argv[0]直接位于其后面。我无法找到任何关于此的文档,但您可以说我有经验证据表明它也没有做任何聪明的事情。


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