1

看看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是非标准的,所以我想避免它。(实际上我想要的功能execvpe来自FreeBSD,但这更清楚地说明了问题。)

我想我理解为什么如果malloc使用 plain 会泄漏内存 - 虽然调用者execvp可以在父级中执行代码,但内部调用execve永远不会返回,因此函数无法释放memp指针,并且无法将指针返回到呼叫者。但是,我想不出一种替代方法alloca——避免这种内存泄漏似乎是必要的魔法。我听说 C99 提供了可变长度数组,但我无法使用它,因为最终的目标是 C++。

是否可以替换这种使用alloca?如果要求它保留在 C++/POSIX 中,那么在使用此算法时是否存在不可避免的内存泄漏?

4

2 回答 2

0

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

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

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

于 2011-05-23T23:40:41.937 回答
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.o”的文件,然后您可以使用该文件与您的其他目标文件(使用gcc -o或 using链接ld)。

我能想到的这个实现的主要“陷阱”是,如果编译器无法通过使用激活记录和堆栈基指针在堆栈上分配空间(即RBPx86_64 中的指针),而是为每个函数调用显式分配内存。然后,由于编译器不会知道我们在堆栈上分配的内存,当它在调用者返回时清理堆栈并尝试使用它认为是被推送的调用者的返回地址跳转回来在函数调用开始时的堆栈上,它将跳转到指向 no-wheres-ville 的指令指针,并且您很可能会因总线错误或某种类型的访问错误而崩溃,因为您将尝试在不允许的内存位置执行代码。

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

总而言之,这样的功能将依赖于平台。

于 2011-05-23T20:25:05.760 回答