【问题标题】:What happens to memory passed in arguments to exec*() family of functions?将参数传递给 exec*() 系列函数的内存会发生什么变化?
【发布时间】:2015-01-29 18:51:53
【问题描述】:

我了解当调用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";
}

【问题讨论】:

    标签: c++ memory exec


    【解决方案1】:

    来自execv man page

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

    因此,您提供了一个 null 终止 数组,其中包含 null 终止 C 字符串。手册页没有明确说明内存发生了什么,但推测字符串被复制到新进程,就像strcpy一样,并且新指针被提供给新进程的main。因为execv 不可能知道关于这些字符串的任何上下文(它们是静态的吗?本地的?malloc'd?),在极端情况下,指针数组似乎不太可能被浅浅地复制到新进程

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

    附带说明,execv 调用将立即将调用进程替换为新进程。这意味着不会调用 C++ 析构函数对于 std::stringstd::vector 和任何其他动态内存,这无关紧要 - 所有分配的内存都会自动回收,所以没有会泄漏。但是,其他副作用也不会发生,std::fstreams 也不会关闭他们的文件等。通常这无关紧要,因为具有严重副作用的析构函数是糟糕的设计实践,但这是值得的知道。

    【讨论】:

      【解决方案2】:

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

      【讨论】:

      • 嗯是的,但这些参数是指针。问题是询问指针的有效性。
      • @LightnessRacesinOrbit “论点”是指逻辑论点,无论它们是如何实现的。我会更新答案。
      【解决方案3】:

      让我们先处理简单的事情:因为正在替换进程映像,所以永远不会调用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”调用一个程序,内存中会有一个包含@的区域987654338@ -- 然后将指向它们的指针放入同一区域的指针数组中,并将指向该指针的指针传递给 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 也是如此。如果您有兴趣,可以找到相当长的历史 OS here 列表;他们基本上都是以同样的方式做到的。事实上,我不知道有一个符合 POSIX 标准的系统以另一种方式来做这件事。这样的系统理论上可以存在;据我所知,它们实际上并没有。

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

      【讨论】:

        猜你喜欢
        • 2015-09-21
        • 1970-01-01
        • 1970-01-01
        • 2015-10-16
        • 1970-01-01
        • 1970-01-01
        • 2017-04-01
        • 1970-01-01
        • 2017-04-30
        相关资源
        最近更新 更多