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

时间:2011-05-23 18:07:17

标签: c++ posix exec alloca

在这里查看execvp的NetBSD实现:

http://cvsweb.netbsd.se/cgi-bin/bsdweb.cgi/src/lib/libc/gen/execvp.c?rev=1.30.16.2;content-type=text%2Fplain

请注意第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中,使用这个算法时是否存在不可避免的内存泄漏?

2 个答案:

答案 0 :(得分:0)

编辑:正如迈克尔在评论中指出的那样,由于优化编译器的堆栈相对寻址,下面所写的内容实际上无法在现实世界中起作用。因此,生产级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-wheres-ville的指令指针,你很可能会因为总线错误或某种类型的访问错误而崩溃,因为你将尝试在你不允许的内存位置执行代码。

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

总而言之,像这样的功能将取决于平台。

答案 1 :(得分:0)

在调用alloca之前,您可以通过拨打malloc制作来替换对vfork的来电。在调用者返回vfork后,可以删除内存。 (这是安全的,因为vfork在调用exec并且新程序启动之前不会返回。)然后调用者可以释放它使用malloc分配的内存。

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

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