C++协程导致堆栈溢出

kg7wmglp  于 2023-02-01  发布在  其他
关注(0)|答案(1)|浏览(198)

在Visual Studio 2022(C++20 build)中,我收到了一个堆栈溢出异常,代码如下(运算符new从未被调用):

struct cCoro
{
    struct promise_type
    {
        cCoro get_return_object() {
            return {
                // Uses C++20 designated initializer syntax
                .Handle = std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() {}
        void return_void() {}

        void* operator new (size_t sz) {
            printf("%s %I64d\n", __FUNCTION__, sz);
            return malloc(sz);
        }
    };

    std::coroutine_handle<promise_type> Handle;
};

cCoro stack_or_heap()
{
    const int sz = 64 * 1024 * 1024; // larger than a normal thread stack ... blows stack!!!???
    uint8_t buffer[sz];

    co_await std::suspend_always();

    // ... use buffer here
}

int main()
{
    auto h = stack_or_heap(); // <== stack overflow here ...
}

按如下方式更改缓冲区大小(并调用运算符new):

cCoro stack_or_heap()
{
    const int sz = 64 * 1024; // Works
    uint8_t buffer[sz];

    co_await std::suspend_always();

    // ... use buffer here
}

我不明白,C++协程中的“局部”变量是否在堆栈上?
我希望分配被传递给操作符new -不管缓冲区的大小。我不希望得到堆栈溢出...
看一下godbolt反汇编,代码在进入协程时将缓冲区大小添加到堆栈中--并且还将缓冲区大小和协程状态的额外字节传递给操作符new

cCoro stack_or_heap(void) PROC           ; stack_or_heap
$LN1:
        mov     eax, 67109128                   ; 04000108H
        call    __chkstk
        sub     rsp, rax
        xor     eax, eax
        test    eax, eax
        je      SHORT $LN8@stack_or_h
        lea     rax, QWORD PTR $T2[rsp]
        mov     QWORD PTR tv73[rsp], rax
        jmp     SHORT $LN9@stack_or_h
$LN8@stack_or_h:
        mov     eax, 67109040                   ; 040000b0H
        mov     ecx, eax
        call    static void * cCoro::promise_type::operator new(unsigned __int64)       ; cCoro::promise_type::operator new
        mov     QWORD PTR tv73[rsp], rax

我不明白为什么线程的堆栈会和缓冲区[sz]有关,当它作为C++协程状态的一部分被分配时(通过new操作符)?缓冲区[sz]不需要在堆栈帧中,对吗?
为什么要向堆栈帧添加空间,并且通过new操作符为同一缓冲区分配空间?

yhxst69z

yhxst69z1#

局部变量是协程状态的一部分。根据某种定义,它们是否被认为是“在堆栈上”是实现要解决的问题。C++实现允许对各种操作的可用空间有各种限制,包括协程状态的大小。
如果这种情况是由于协程的状态超出了协程函数允许使用的堆栈大小的限制,那么它可能会认为这种情况是“堆栈溢出”。但是同样,这完全取决于实现。
也许问题在于你把“栈”和“堆”看作是脱节的概念。一个对象在一个或另一个上,但不是同时在两个上。虽然这是一个概念上有用的区别,但实际上并不正确。
记忆就是记忆在底部没有“栈存储器”或“堆存储器”。
术语“堆”通常用来指一些内存池,其获取和释放由应用程序的运行时显式管理,而“堆栈”通常指函数的自动变量使用的内存,其中的分配是根据您以何种顺序调用哪些函数来自动管理的。
但这只是一个高层次的概述,在底部,只有记忆。
“栈”是内存中的一个区域,一个特定的执行线程连接到这个区域。也就是说,当一个CPU线程运行时,它有一个概念上的内存区域,这个区域就是它的“栈”。它不关心这个内存来自哪里。
要点是:内存是“堆栈”,因为它被“用作堆栈帧”。它不是内存的固有属性,与内存的来源“无关”。
协程的要点是它的执行可以被暂停和恢复。要做到这一点,任何局部变量的内容必须被保留。但通常情况下,线程堆栈使用的内存是连续的。要使协程工作,协程的局部变量必须在与任何特定线程堆栈断开连接的存储器中。
这就是内存分配的用武之地。协程状态是一组数据,其中包括用于管理协程的对象,但它 * 也 * 包括协程用作其堆栈的内存块。当协程启动时,必须分配此内存的存储空间。当协程完成时,必须释放此内存。
记住:“堆栈”并不关心内存来自哪里。内存是一个基于使用的“堆栈”,而不是它是如何分配的。因此,从“堆”分配的一块内存实际上可以用作堆栈。这里没有矛盾。

相关问题