如何在execvp()的实现中替换alloca



在这里查看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 -old与其他对象文件链接。

我能想到的这个实现的主要"问题"是,如果编译器没有使用激活记录和堆栈基指针(即x86_64中的RBP指针)在堆栈上分配空间,而是为每个函数调用显式分配内存,那么您可能会崩溃或最终出现未定义的行为。然后,由于编译器不会意识到我们在堆栈上分配的内存,当它在调用方返回时清理堆栈,并试图使用它认为是调用方的返回地址(在函数调用开始时推送到堆栈上)跳回来时,它将跳转到一个指向no where ville的指令指针,你很可能会因总线错误或某种类型的访问错误而崩溃,因为你将试图在不允许的内存位置执行代码。

实际上,还有其他危险的事情可能发生,比如编译器使用堆栈空间来分配参数(根据Unix 64位ABI,不应该为这个函数分配参数,因为只有一个参数),因为这会在函数调用后再次导致堆栈清理,扰乱指针的有效性。但是,对于像execvp()这样的函数,除非出现错误,否则不会返回,这应该不是什么大问题。

总而言之,像这样的函数将依赖于平台。

您可以将对alloca的调用替换为在对vfork的调用之前对malloc的调用。在调用方返回vfork之后,可以删除存储器。(这是安全的,因为在调用exec并启动新程序之前,vfork不会返回。)然后,调用者可以释放它用malloc分配的内存。

这不会泄漏子进程中的内存,因为exec调用将子进程映像完全替换为父进程的映像,从而隐式释放分叉进程所持有的内存。

另一种可能的解决方案是切换到fork而不是vfork。这将需要调用方中的一些额外代码,因为forkexec调用完成之前返回,因此调用方需要等待它。但一旦forked,新进程就可以安全地使用malloc。我对vfork的理解是,它基本上是穷人的fork,因为在内核在写页面上复制之前,fork是昂贵的。现代内核非常有效地实现了fork,不需要求助于有点危险的vfork

相关内容

  • 没有找到相关文章

最新更新