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

时间:2015-01-29 18:51:54

标签: c++ memory exec

据我所知,在调用exec*()时,旧进程的内存完全被新程序取代。但是,诸如argv之类的参数的记忆呢?如果我有这样的代码,使用来自std::string等C ++数据结构的内存是否安全,或者这些内容是否会消失,会破坏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 个答案:

答案 0 :(得分:2)

将字符串复制到新创建的内存空间中。只要他们在致电exec时有效,您就不必担心。

答案 1 :(得分:2)

来自execv man page

  

execv(),execvp()和execvpe()函数提供指向以null结尾的字符串的指针数组,表示新程序可用的参数列表。按照惯例,第一个参数应指向与正在执行的文件关联的文件名。 指针数组必须以NULL指针终止。 [强调补充]

因此,您提供 null终止 C字符串的 null终止数组。手册页没有明确说明内存会发生什么,但可能是字符串被复制,就像strcpy一样,被复制到新进程,并且新指针被提供给{{1}新流程因为main可能无法知道有关这些字符串的任何内容(它们是静态的吗?本地?execv&#39; d?),对我来说似乎不太可能指针将被浅层复制到新进程

要解决您的确切问题,这意味着几乎任何以空值终止的malloc来源(包括char*,来自std::stringstr.c_str())都可以使用作为传递给str.data()的数组的一部分。值得注意的是,在C ++ 11之前,只要execv成员返回一个指针,std::strings 以null结尾的字符串。我不知道c_str的任何实现都没有空终止,但值得注意的是,与c-strings std::string s 不同包含std::string个字符作为字符串数据的一部分,而不是终结符。

作为旁注,\0调用将立即用新的调用替换调用进程。这意味着不会调用C ++析构函数对于execvstd::string和任何其他动态内存,这并不重要 - 所有分配的内存自动回收,所以什么都不会泄漏。然而,其他副作用不会发生,要么std::vector不能关闭他们的文件等等。一般来说这不会有问题,因为副作用很大的析构函数很差设计实践,但需要注意的事项。

答案 2 :(得分:0)

让我们先处理简单的事情:因为正在替换过程图像,所以永远不会调用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);
}

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”调用一个程序,内存中会有一个区域包含{ {1}} - 然后获取指向它们的指针并将其放入同一区域中的指针数组中,并将指向该指针的指针传递给main。 (环境也被复制到这个区域,但这不是问题的一部分。)

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

让我们来看看FreeBSD:使用相同的程序,输出是(On i386 FreeBSD 9.1):

"bar\0baz\0qux\0xyzzy"

知道FreeBSD的堆栈开始于0x28404050 0xbfbfee58 (在9.1中没有ASLR),我们可以看到同样的事情发生在这里。 FreeBSD使用固定的最大大小为256KB,MacOS X也是如此。如果您感兴趣,可以找到相当长的历史操作系统列表here;他们都以同样的方式做到了。事实上,我不知道一个单一的POSIX兼容系统以另一种方式。这样的系统可以在理论上存在;据我所知,他们没有在实践中。

简要介绍一下Windows:看起来做同样的事情;在几次尝试中,0xbfc00000中的argv[1]位于bar后面,argv[0]位于argv之后,位于堆栈顶部的execl后面。我无法找到任何关于此的文档,但你可以说我有经验证据表明它也没有做任何聪明的事情。