如何在堆栈上分配并返回而不弹出堆栈?

时间:2020-02-12 04:39:15

标签: c

(我知道C语言规范没有指定分配是否发生在堆栈,堆或其他某种机制上-但出于这个问题,我们假设所有自动变量都存在于调用堆栈中, alloca(size_t)始终在堆栈上分配(与malloca不同),malloc / calloc等在堆栈上分配)。

在C语言中,如果您有一个函数来初始化内存的某些区域,则需要预先分配该内存,然后将一个指针(如果需要,带有长度)传递给初始化程序:

void plumbusProgram() {

    const size_t plumbusSize = 123;

    void* plumbusMemory = alloca( plumbusSize );

    struct plumbusVla* = initializePlumbus( plumbusMemory, plumbusSize  );

    doStuffWithPlumbus( plumbusVla );

    // `plumbusMemory` ceases to exist as allocated memory when this function returns.
}

如果要分配的内存大小是可变的,则它将负责了解分配大小的责任交给拥有该分配的函数,而不是由初始化内存的函数来负责。我从不喜欢这个要求,因为它破坏了SRP。

void plumbusProgram( enum plumbusType type ) {

    size_t plumbusMemorySize = getPlumbusMemorySize( type ); // <-- ewww

    void* plumbusMemory = alloca( plumbusMemorySize );

    struct plumbusVla* = initializePlumbus( plumbusMemory, plumbusSize  );

    doStuffWithPlumbus( plumbusVla );

    // `plumbusMemory` ceases to exist as allocated memory when this function returns.
}

那为什么initializePlumbus既不能分配又要初始化plumbusMemory ?当前,在可移植的C语言中(当然没有UB),initializePlumbus函数需要在堆上分配并返回指向堆的指针-这意味着现在调用方需要显式管理分配的堆的生存期通过在plumbusMemory返回之前调用free plumbusProgram,包括在每种可能的错误情况下(或在C ++中,使用智能指针)。

我意识到initializePlumbus仍然可以在堆栈上分配,只要它可以返回而不会弹出堆栈-但是每种方法都有问题。

首先,这是我希望程序的外观:

void plumbusProgram( enum plumbusType type ) {

    struct plumbusVla* = initializePlumbus( type ); // allocates the memory for `plumbusVla` inside `plumbusProgram`'s stack frame, so the memory is automatically deallocated when `plumbusProgram` returns.

    doStuffWithPlumbus( plumbusVla );

    // `plumbusVla`'s memory ceases to exist when this function returns.
}

以下是我考虑过的几种方法:

好的ol'UB:

void plumbusProgram( enum plumbusType type ) {

    size_t plumbusMemorySize;
    struct plumbusVla* = initializePlumbus( type, &plumbusMemorySize );
    // Artifically raise the stack pointer to beyond plumbusMemory so the next function-call (and anything else added to the stack) won't overwrite plumbusMemory:
    alloca( plumbusMemorySize );

    // It's now safe to call other functions:
    doStuffWithPlumbus( plumbusVla );

     // `plumbusVla`'s memory ceases to exist when this function returns.
}

struct plumbusVla* initializePlumbus( enum plumbusType type, size_t* plumbusMemorySize ) {

    *plumbusMemorySize = getPlumbusMemorySize( type );
    void* plumbusMemory = alloca( plumbusMemorySize );

    // (do rest of initialization here)

    return (struct plumbusVla*)plumbusMemory;
}

此处的缺点是顶级plumbusProgram函数仍然具有plumbusMemorySize的知识,并且不得不滥用alloca来移动堆栈指针。

setjmp / longjmp UB:

在堆上分配后,使用setjmp / longjmpinitializePlumbus内部跳回到plumbusProgram并没有帮助,因为在调用longjmp时,原始的堆栈指针-以及关于plumbusMemory大小的知识将丢失。

The docs say

例程接收控件可以访问的所有变量(寄存器变量除外)的值包含它们在调用longjmp时所具有的值。

因此,如果plumbusMemory的大小通过指向本地的指针传递回plumbusProgram,则堆栈状态仍将重置,因此值将丢失。存储plumbusMemory大小的唯一方法是使用非堆栈变量,例如堆变量(这违背了本练习的目的),单个全局变量(这意味着您不能拥有更多的变量)。大于1个已分配堆栈的对象)或一个全局固定大小的堆栈(仍限制了已分配堆栈的对象的数量)。

在分配之后(在initializePlumbus内部保存堆栈状态不起作用,因为在longjmp返回后从plumbusProgram调用initializePlumbus只会导致循环,因为PC也将从initializePlumbus内部恢复。

setcontext / getcontext

这些(不幸的是现在已经过时的)POSIX函数可能有用,但是我对它们不太熟悉。

内联汇编

这有点作弊-并使代码特定于单个ISA-但它完美地解决了问题-像这样:

void plumbusProgram( enum plumbusType type ) {

    struct plumbusVla* = initializePlumbus( plumbusMemory, plumbusSize );
}

__declspec( naked ) size_t initializePlumbus( enum plumbusType type ) {

    // Need to provide both prolog and epilog, even though I only want a custom epilog - so here's the __stdcall prolog:
    __asm {
        push ebp
        mov  ebp, esp
        sub  esp, __LOCAL_SIZE
    }

    *plumbusMemorySize = getPlumbusMemorySize( type );
    void* plumbusMemory = alloca( plumbusMemorySize );

    // (do rest of initialization here)

    // Custom epilog: Restores ebp but not esp:
    __asm {
        pop ebp
        jmp ebp ; return to caller without popping stack (instead of `ret`)
    }
}

...但是显然这是一个黑客。

令我感到惊讶的是,C(尤其是POSIX C)似乎没有任何内置方法来允许将 delegation 分配给另一个函数。 >

0 个答案:

没有答案
相关问题