将参数传递给 exec*() 函数族的内存会发生什么变化?
What happens to memory passed in arguments to exec*() family of functions?
我了解到调用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";
}
字符串被复制到新创建的内存中space。只要您调用 exec
时它们有效,您就不必担心。
The execv(), execvp(), and execvpe() functions provide an array of pointers to null-terminated strings that represent the argument list available to the new program. The first argument, by convention, should point to the filename associated with the file being executed. The array of pointers must be terminated by a NULL pointer. [Emphasis added]
因此,您提供了一个 空终止 数组 空终止 C 字符串。手册页没有明确说明内存发生了什么,但大概是将字符串复制到新进程,就像 strcpy
一样,并将新指针提供给 main
新流程。因为 execv
不可能知道关于这些字符串的任何上下文(它们是静态的吗?本地的?malloc
'd?),在极端情况下对我来说似乎不太可能浅拷贝指针数组到新进程
为了解决您的确切问题,这意味着几乎任何以 null 结尾的 char*
来源(包括 std::string
,通过 str.c_str()
或 str.data()
)都可以用作传递给 execv
的数组的一部分。值得注意的是,在 C++11 之前,std::strings
不需要 以 null 终止,只要 c_str
成员 returns指向以 null 结尾的字符串的指针。我不知道 std::string
的任何实现都不是空终止的,但值得注意的是,与 c 字符串不同 std::string
s may 包含[=24=]
个字符作为字符串数据的一部分,而不是作为终止符。
附带说明一下,execv
调用将立即 将调用进程替换为新进程。这意味着 C++ 析构函数 将不会被调用。 在 std::string
、std::vector
和任何其他动态内存的情况下,这无关紧要 - 全部分配内存会自动回收,因此不会泄漏任何内容。然而,其他副作用不会发生,或者 - std::fstream
s 不会关闭他们的文件等。通常这无关紧要,因为具有严重副作用的析构函数是糟糕的设计实践,但它是一些东西要知道。
让我们先处理简单的事情:因为进程映像正在被替换,std::string
的析构函数将永远不会被调用,所以内存不会消失(那样)。
我假设你问的是类 UNIX 操作系统,因为 unistd.h
在 Windows 上不存在,所以相关标准是 POSIX。故意在这方面含糊其辞,只说
The argv[] and envp[] arrays of pointers and the strings to which those arrays point shall not be modified by a call to one of the exec functions, except as a consequence of replacing the process image.
这意味着 exec
应该注意参数不会因替换过程映像而失效,但是 POSIX 不关心 exec
如何实现这一点.这是您可以信赖的一点:您的论点将保持有效并且不会被破坏。
至于 "in practice:" POSIX 确实知道在编写标准时实现是如何实现的,而且最近的实现并没有真正改变基本机制。让我们读一下字里行间:
The number of bytes available for the new process' combined argument and environment lists is {ARG_MAX}.
ARG_MAX
被定义为 here 最小值 4096。
如果我们假设为参数和环境分配了固定大小的 space(或至少可以增长到固定的最大大小的 space),则此要求是有意义的,只有在替换过程映像之前将参数复制到那里才有意义。 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" 调用一个程序,内存中会有一个区域包含 "bar[=25=]baz[=25=]qux[=25=]xyzzy"
的指针——然后将指向它们的指针放入同一区域的指针数组中,然后将指向它的指针传递到 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]
直接位于 execl
之后堆栈顶部的 argv
之后。我找不到这方面的任何文档,但你可以说我有经验证据表明它也没有做任何聪明的事情。
我了解到调用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";
}
字符串被复制到新创建的内存中space。只要您调用 exec
时它们有效,您就不必担心。
The execv(), execvp(), and execvpe() functions provide an array of pointers to null-terminated strings that represent the argument list available to the new program. The first argument, by convention, should point to the filename associated with the file being executed. The array of pointers must be terminated by a NULL pointer. [Emphasis added]
因此,您提供了一个 空终止 数组 空终止 C 字符串。手册页没有明确说明内存发生了什么,但大概是将字符串复制到新进程,就像 strcpy
一样,并将新指针提供给 main
新流程。因为 execv
不可能知道关于这些字符串的任何上下文(它们是静态的吗?本地的?malloc
'd?),在极端情况下对我来说似乎不太可能浅拷贝指针数组到新进程
为了解决您的确切问题,这意味着几乎任何以 null 结尾的 char*
来源(包括 std::string
,通过 str.c_str()
或 str.data()
)都可以用作传递给 execv
的数组的一部分。值得注意的是,在 C++11 之前,std::strings
不需要 以 null 终止,只要 c_str
成员 returns指向以 null 结尾的字符串的指针。我不知道 std::string
的任何实现都不是空终止的,但值得注意的是,与 c 字符串不同 std::string
s may 包含[=24=]
个字符作为字符串数据的一部分,而不是作为终止符。
附带说明一下,execv
调用将立即 将调用进程替换为新进程。这意味着 C++ 析构函数 将不会被调用。 在 std::string
、std::vector
和任何其他动态内存的情况下,这无关紧要 - 全部分配内存会自动回收,因此不会泄漏任何内容。然而,其他副作用不会发生,或者 - std::fstream
s 不会关闭他们的文件等。通常这无关紧要,因为具有严重副作用的析构函数是糟糕的设计实践,但它是一些东西要知道。
让我们先处理简单的事情:因为进程映像正在被替换,std::string
的析构函数将永远不会被调用,所以内存不会消失(那样)。
我假设你问的是类 UNIX 操作系统,因为 unistd.h
在 Windows 上不存在,所以相关标准是 POSIX。故意在这方面含糊其辞,只说
The argv[] and envp[] arrays of pointers and the strings to which those arrays point shall not be modified by a call to one of the exec functions, except as a consequence of replacing the process image.
这意味着 exec
应该注意参数不会因替换过程映像而失效,但是 POSIX 不关心 exec
如何实现这一点.这是您可以信赖的一点:您的论点将保持有效并且不会被破坏。
至于 "in practice:" POSIX 确实知道在编写标准时实现是如何实现的,而且最近的实现并没有真正改变基本机制。让我们读一下字里行间:
The number of bytes available for the new process' combined argument and environment lists is {ARG_MAX}.
ARG_MAX
被定义为 here 最小值 4096。
如果我们假设为参数和环境分配了固定大小的 space(或至少可以增长到固定的最大大小的 space),则此要求是有意义的,只有在替换过程映像之前将参数复制到那里才有意义。 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" 调用一个程序,内存中会有一个区域包含 "bar[=25=]baz[=25=]qux[=25=]xyzzy"
的指针——然后将指向它们的指针放入同一区域的指针数组中,然后将指向它的指针传递到 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]
直接位于 execl
之后堆栈顶部的 argv
之后。我找不到这方面的任何文档,但你可以说我有经验证据表明它也没有做任何聪明的事情。