在这里查看execvp
的NetBSD实现:
http://cvsweb.netbsd.se/cgi-bin/bsdweb.cgi/src/lib/libc/gen/execvp.c?rev=1.30.16.2;内容类型=文本%2普通
注意第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中,那么在使用该算法时是否会不可避免地出现内存泄漏?
编辑:正如Michael在评论中指出的,由于优化编译器的堆栈相对寻址,下面所写的内容在现实世界中真的不起作用。因此,生产级alloca
需要编译器的帮助才能真正"工作"。但希望下面的代码能给出一些关于引擎盖下发生了什么的想法,以及如果没有堆栈相关寻址优化需要担心的话,像alloca
这样的函数可能是如何工作的
顺便说一句,为了防止你仍然好奇如何为自己制作一个简单版本的alloca
,因为该函数基本上返回一个指向堆栈上分配空间的指针,你可以在汇编中编写一个可以正确操作堆栈的函数,并返回一个可以在调用程序的当前作用域中使用的指针(一旦调用程序返回,则此版本的alloca
中的堆栈空间指针将无效,因为调用程序的返回会清理堆栈)。
假设您在使用Unix 64位ABI的x86_64平台上使用某种风格的Linux,请将以下内容放入名为"my_alloca.s"的文件中:
.section .text
.global my_alloca
my_alloca:
movq (%rsp), %r11 # save the return address in temp register
subq %rdi, %rsp # allocate space on stack from first argument
movq $0x10, %rax
negq %rax
andq %rax, %rsp # align the stack to 16-byte boundary
movq %rsp, %rax # save address in return register
pushq %r11 # push return address on stack
ret # return back to caller
然后在您的C/C++代码模块(即".cpp"文件)中,您可以通过以下方式使用它:
extern my_alloca(unsigned int size);
void function()
{
void* stack_allocation = my_alloca(BUFFERSIZE);
//...do something with the allocated space
return; //WARNING: stack_allocation will be invalid after return
}
您可以使用gcc -c my_alloca.s
编译"my_alloca.s"。这将为您提供一个名为"my_alloca.o"的文件,然后您可以使用该文件使用gcc -o
或ld
与其他对象文件链接。
我能想到的这个实现的主要"问题"是,如果编译器没有使用激活记录和堆栈基指针(即x86_64中的RBP
指针)在堆栈上分配空间,而是为每个函数调用显式分配内存,那么您可能会崩溃或最终出现未定义的行为。然后,由于编译器不会意识到我们在堆栈上分配的内存,当它在调用方返回时清理堆栈,并试图使用它认为是调用方的返回地址(在函数调用开始时推送到堆栈上)跳回来时,它将跳转到一个指向no where ville的指令指针,你很可能会因总线错误或某种类型的访问错误而崩溃,因为你将试图在不允许的内存位置执行代码。
实际上,还有其他危险的事情可能发生,比如编译器使用堆栈空间来分配参数(根据Unix 64位ABI,不应该为这个函数分配参数,因为只有一个参数),因为这会在函数调用后再次导致堆栈清理,扰乱指针的有效性。但是,对于像execvp()
这样的函数,除非出现错误,否则不会返回,这应该不是什么大问题。
总而言之,像这样的函数将依赖于平台。
您可以将对alloca
的调用替换为在对vfork
的调用之前对malloc
的调用。在调用方返回vfork
之后,可以删除存储器。(这是安全的,因为在调用exec
并启动新程序之前,vfork
不会返回。)然后,调用者可以释放它用malloc分配的内存。
这不会泄漏子进程中的内存,因为exec
调用将子进程映像完全替换为父进程的映像,从而隐式释放分叉进程所持有的内存。
另一种可能的解决方案是切换到fork
而不是vfork
。这将需要调用方中的一些额外代码,因为fork
在exec
调用完成之前返回,因此调用方需要等待它。但一旦forked
,新进程就可以安全地使用malloc
。我对vfork
的理解是,它基本上是穷人的fork
,因为在内核在写页面上复制之前,fork
是昂贵的。现代内核非常有效地实现了fork
,不需要求助于有点危险的vfork
。