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

发布于 2024-11-09 05:58:01 字数 1197 浏览 2 评论 0原文

在这里查看 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 内,那么使用此算法时是否会不可避免地出现内存泄漏?

Take a look at the NetBSD implementation of execvp here:

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

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

轮廓§ 2024-11-16 05:58:01

编辑:正如迈克尔在评论中指出的那样,由于优化编译器的堆栈相对寻址,下面写的内容在现实世界中实际上不起作用。因此,生产级分配需要编译器的帮助才能真正“工作”。但希望下面的代码可以提供一些关于幕后发生的事情的想法,以及如果没有堆栈相对寻址优化需要担心的话,像 alloca 这样的函数可能如何工作。

BTW ,以防万一您仍然对如何为自己创建一个简单版本的 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() 这样的函数,除非出现错误,否则不会返回,所以这不应该是一个大问题。

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

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 like alloca 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 of alloca 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":

.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

Then inside your C/C++ code module (i.e, your ".cpp" files), you can use it the following way:

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
}

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 using gcc -o or using ld.

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.

记忆之渊 2024-11-16 05:58:01

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

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

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

You can replace the call to alloca with a call to malloc made before the call to vfork. After the vfork returns in the caller the memory can be deleted. (This is safe because vfork will not return until exec 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 of vfork. This will require a little extra code in the caller because fork returns before the exec call is complete so the caller will need to wait for it. But once forked the new process could use malloc safely. My understanding of vfork is it was basically a poor man's fork because fork was expensive in the days before kernels had copy-on-write pages. Modern kernels implement fork very efficiently and there's no need resort to the somewhat dangerous vfork.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文