【问题标题】:How to replace alloca in an implementation of execvp()?如何在 execvp() 的实现中替换 alloca?
【发布时间】:2011-08-31 08:40:03
【问题描述】:

在此处查看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 的内部调用永远不会返回,因此函数无法释放 @ 987654331@ 指针,并且无法将指针返回给调用者。但是,我想不出替换alloca 的方法——避免这种内存泄漏似乎是必要的魔法。我听说 C99 提供了可变长度的数组,但遗憾的是我不能使用它,因为最终的目标是 C++。

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

【问题讨论】:

  • 你说这是一个独立的C++环境?它是否提供vfork?如果它不提供vfork,或者如果vfork 在这个环境中与fork 相同,那么您可以调用mallocnew 而不必担心内存泄漏。在forked 孩子中,你所做的任何事情都不会修改父母的记忆。
  • 啊!出于某种原因,我假设该评论也适用于 fork() 。由于环境不提供vfork,此题无效!非常感谢。
  • @Rob 我认为只调用 fork 而不是 vfork 是无效的,因为 vfork 的行为与 fork 不同(一个立即返回,另一个仅在新进程调用 exec 或_exit)。

标签: c++ posix exec alloca


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

    • 您的第一个“陷阱”实际上在优化构建中很常见 - 编译器可以避免创建基于 rbp 的堆栈框架和使用来自 rsp 的偏移量简单访问基于堆栈的本地变量。用户编写的alloca() 非常危险——它确实需要与编译器合作。见stackoverflow.com/questions/714692/alloca-implementation/…
    • 感谢您的链接。我认为编译器方面一定有一些“帮助”以获得完整证明alloca。以上面显示的样式编写时,如果无法控制编译器如何组装调用者函数,则会出现太多故障点。我确实尝试通过对齐堆栈而不是简单地减去堆栈指针来缓解问题,以及遵循 Unix ABI 进行函数调用。虽然它似乎与 -O2 优化一起工作得很好,但你是完全正确的——即使是编译器的一个堆栈相对寻址实例也会导致崩溃等。
    猜你喜欢
    • 1970-01-01
    • 2011-03-30
    • 2017-05-22
    • 2010-10-17
    • 1970-01-01
    • 2021-08-13
    • 1970-01-01
    • 2016-01-16
    • 2021-05-06
    相关资源
    最近更新 更多