如何在 execvp() 的实现中替换 alloca?
在这里查看 execvp
的 NetBSD 实现:
请注意第 130 行的注释,在处理 ENOEXEC
的特殊情况下:
/*
* we can't use malloc here because, if we are doing
* vfork+exec, it leaks memory in the parent.
*/
if ((memp = alloca((cnt + 2) * sizeof(*memp))) == NULL)
goto done;
memp[0] = _PATH_BSHELL;
memp[1] = bp;
(void)memcpy(&memp[2], &argv[1], cnt * sizeof(*memp));
(void)execve(_PATH_BSHELL, __UNCONST(memp), environ);
goto done;
我正在尝试移植此实现execvp
到独立的 C++。 alloca
是非标准的,所以我想避免它。 (实际上我想要的函数是 FreeBSD 中的 execvpe ,但这更清楚地说明了问题。)
我想我明白为什么如果使用普通的 malloc 会泄漏内存 - 而execvp
的调用者可以执行父级中的代码,对 execve
的内部调用永远不会返回,因此该函数无法释放 memp
指针,并且有无法将指针返回给调用者。但是,我想不出一种方法来替换 alloca
- 这似乎是避免这种内存泄漏的必要魔法。我听说 C99 提供了可变长度数组,但遗憾的是我无法使用它,因为最终目标是 C++。
是否可以替换 alloca
的这种用法?如果强制要求保留在 C++/POSIX 内,那么使用此算法时是否会不可避免地出现内存泄漏?
Take a look at the NetBSD implementation of execvp
here:
Note the comment at line 130, in the special case for handling ENOEXEC
:
/*
* we can't use malloc here because, if we are doing
* vfork+exec, it leaks memory in the parent.
*/
if ((memp = alloca((cnt + 2) * sizeof(*memp))) == NULL)
goto done;
memp[0] = _PATH_BSHELL;
memp[1] = bp;
(void)memcpy(&memp[2], &argv[1], cnt * sizeof(*memp));
(void)execve(_PATH_BSHELL, __UNCONST(memp), environ);
goto done;
I am trying to port this implementation of execvp
to standalone C++. alloca
is nonstandard so I want to avoid it. (Actually the function I want is execvpe
from FreeBSD, but this demonstrates the problem more clearly.)
I think I understand why it would leak memory if plain malloc
was used - while the caller of execvp
can execute code in the parent, the inner call to execve
never returns so the function cannot free the memp
pointer, and there's no way to get the pointer back to the caller. However, I can't think of a way to replace alloca
- it seems to be necessary magic to avoid this memory leak. I have heard that C99 provides variable length arrays, which I cannot use sadly as the eventual target is C++.
Is it possible to replace this use of alloca
? If it's mandated to stay within C++/POSIX, is there an inevitable memory leak when using this algorithm?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
编辑:正如迈克尔在评论中指出的那样,由于优化编译器的堆栈相对寻址,下面写的内容在现实世界中实际上不起作用。因此,生产级分配需要编译器的帮助才能真正“工作”。但希望下面的代码可以提供一些关于幕后发生的事情的想法,以及如果没有堆栈相对寻址优化需要担心的话,像
alloca
这样的函数可能如何工作。BTW ,以防万一您仍然对如何为自己创建一个简单版本的
alloca
感到好奇,因为该函数基本上返回一个指向堆栈上分配空间的指针,您可以在汇编中编写一个函数可以正确地操作堆栈,并返回一个可以在调用者当前范围内使用的指针(一旦调用者返回,此版本的alloca
中的堆栈空间指针就会失效,因为从调用者返回清理堆栈)。假设您在使用 Unix 64 位 ABI 的 x86_64 平台上使用某种风格的 Linux,请将以下内容放入名为“my_alloca.s”的文件中:
然后放入您的 C/C++ 代码模块(即“.cpp”)中文件),您可以通过以下方式使用它:
您可以使用
gcc -c my_alloca.s
编译“my_alloca.s”。这将为您提供一个名为“my_alloca.o”的文件,然后您可以使用gcc -o
或使用ld
将其用于与其他目标文件链接。我能想到的这个实现的主要“陷阱”是,如果编译器没有通过使用激活记录和堆栈基指针(即, x86_64 中的 RBP 指针),而是为每个函数调用显式分配内存。然后,由于编译器不会知道我们在堆栈上分配的内存,因此当它在调用者返回时清理堆栈并尝试使用它认为被推送的调用者返回地址跳回时在函数调用开始时的堆栈上,它将跳转到指向 no-wheres-ville 的指令指针,并且您很可能会因总线错误或某种类型的访问错误而崩溃,因为您将尝试在不允许的内存位置执行代码。
实际上还可能发生其他危险的事情,例如编译器使用堆栈空间来分配参数(每个 Unix 64 位 ABI 不应该使用这个函数,因为只有一个参数),因为这会再次导致函数调用后立即进行堆栈清理,从而破坏了指针的有效性。但是对于像
execvp()
这样的函数,除非出现错误,否则不会返回,所以这不应该是一个大问题。总而言之,这样的功能将依赖于平台。
Edit: As Michael has pointed out in the comments, what is written below really won't work in the real-world due to stack-relative addressing by an optimizing compiler. Therefore a production-level
alloca
needs the help of the compiler to actually "work". But hopefully the code below could give some ideas about what's happening under the hood, and how a function likealloca
might have worked if there were no stack-relative addressing optimizations to worry about.BTW, just in case you were stil curious about how you could make a simple version of
alloca
for yourself, since that function basically returns a pointer to allocated space on the stack, you can write a function in assembly that can properly manipulate the stack, and return a pointer you can use in the current scope of the caller (once the caller returns, the stack space pointer from this version ofalloca
is invalidated since the return from the caller cleans up the stack).Assuming you're using some flavor of Linux on a x86_64 platform using the Unix 64-bit ABI, place the following inside a file called "my_alloca.s":
Then inside your C/C++ code module (i.e, your ".cpp" files), you can use it the following way:
You can compile "my_alloca.s" using
gcc -c my_alloca.s
. This will give you a file named "my_alloca.o" that you can then use to link with your other object files usinggcc -o
or usingld
.The main "gotcha" that I could think of with this implementation is that you could crash or end up with undefined behavior if the compiler did not work by allocating space on the stack using an activation record and a stack base-pointer (i.e., the
RBP
pointer in x86_64), but rather explicitly allocated memory for each function call. Then, since the compiler won't be aware of the memory we've allocated on the stack, when it cleans up the stack at the return of the caller and tries to jump back using what it believes is the caller's return address that was pushed on the stack at the beginning of the function call, it will jump to an instruction pointer that's pointing to no-wheres-ville and you'll most likely crash with a bus error or some type of access error since you'll be trying to execute code in a memory location you're not allowed to.There's actually other dangerous things that could happen, such as if the compiler used stack-space to allocate the arguments (it shouldn't for this function per the Unix 64-bit ABI since there's only a single argument), as that would again cause a stack clean-up right after the function call, messing up the validity of the pointer. But with a function like
execvp()
, which won't return unless there's an error, this shouldn't be so much of an issue.All-in-all, a function like this will be platform-dependent.
您可以将对
alloca
的调用替换为在调用vfork
之前对malloc
的调用。vfork
在调用者中返回后,可以删除内存。 (这是安全的,因为在调用exec
并启动新程序之前,vfork
不会返回。)然后调用者可以释放使用 malloc 分配的内存。这不会泄漏子进程中的内存,因为
exec
调用将子进程的映像完全替换为父进程的映像,从而隐式释放了分叉进程所持有的内存。另一种可能的解决方案是切换到 fork 而不是 vfork。这将需要调用者添加一些额外的代码,因为
fork
在exec
调用完成之前返回,因此调用者需要等待它。但是一旦fork
,新进程就可以安全地使用malloc
。我对 vfork 的理解是,它基本上是穷人的 fork,因为在内核具有写时复制页面之前,fork 非常昂贵。现代内核非常有效地实现了fork
,并且无需诉诸于有些危险的vfork
。You can replace the call to
alloca
with a call tomalloc
made before the call tovfork
. After thevfork
returns in the caller the memory can be deleted. (This is safe becausevfork
will not return untilexec
has been called and the new program started.) The caller can then free the memory it allocated with malloc.This doesn't leak memory in the child because the
exec
call completely replaces the child image with the image of the parent process, implicitly releasing the memory that the forked process was holding.Another possible solution is to switch to
fork
instead ofvfork
. This will require a little extra code in the caller becausefork
returns before theexec
call is complete so the caller will need to wait for it. But onceforked
the new process could usemalloc
safely. My understanding ofvfork
is it was basically a poor man'sfork
becausefork
was expensive in the days before kernels had copy-on-write pages. Modern kernels implementfork
very efficiently and there's no need resort to the somewhat dangerousvfork
.