You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows自定义栈半随机触发0x80000001异常原因排查

Windows自定义栈增长时随机触发STATUS_GUARD_PAGE_VIOLATION异常的原因分析

原问题背景

我想了解Windows下原生应用栈增长的精确机制——已知基础原理是用保护页(guard-pages)作为一次性访问警报,触发后分配下一页直到空间耗尽,过程会查询TEB获取栈限制,但需要知道访问保护页时的精确流程:异常在哪里处理、调用了哪些函数。

我遇到一个罕见的栈增长Bug:在自定义编译的栈满协程代码中,偶尔会在访问初始分配页后的第一页时触发SEH异常:

0x80000001: Not implemented (Parameter: 0x0000000000000001, 0x00000011805BEFE8)

该异常对应STATUS_GUARD_PAGE_VIOLATION,按常理这类异常会被系统自动处理以实现栈增长,但我的场景中处理机制失效,应用直接收到异常。正常情况下甚至无法拦截这类访问违例:

__try
{
    // 内部调用_chstck,但无异常触发
    _alloca(160000);
}
__except(1)
{
    int i = 0;
}

唯一能稳定触发该错误的操作是:访问栈上的保护页,但未将其设置为RSP且未在TEB中配置:

auto stack = event::YieldStack::Create();
// 触发和我遇到的完全相同的SEH异常
*(char**)(stack.GetStack() - 4097) = nullptr;

错误出现概率极低(99.9%时间正常运行),推测是代码破坏了异常处理机制,或者系统认为当前RSP不属于线程栈。我通过RtlCreateUserStack创建栈:

YieldStack YieldStack::Create(void)
{
    INITIAL_TEB InitialTeb;
    // STACK_SIZE = 4096*64;
    const auto status = RtlCreateUserStack(4096, STACK_SIZE, 0, 0x1000, 0x10000, &InitialTeb);
    if (status)
        sdk::logError("Error while creating user-stack: {}", status);
    
    return { (char*)InitialTeb.StackBase };
}

切换栈前会更新NtCurrentTeb:

inline void setStackLimits(ExecutionStateJIT& state, char* pStackBase)
{
    // 设置Windows特定栈变量
    auto* pTIB = (NT_TIB*)NtCurrentTeb();

    state.pOldStackBase = pTIB->StackBase;
    state.pOldStackLimit = pTIB->StackLimit;

    pTIB->StackBase = pStackBase;
    pTIB->StackLimit = pStackBase - YieldStack::STACK_SIZE;
}

// 后续执行汇编代码切换栈
mov rsp,pStackBase;

已验证所有值正确:RSP属于正确栈的偏移、TEB显示栈的正确基址/限制、异常发生时RSP指向的数据(含当前页)全程可读。触发错误的代码都是栈访问操作,例如:

mov qword ptr [rsp],0; // 此时RSP指向下一页

希望有人能指出代码中的明显错误,或完整解释栈增长机制以排查故障。

编辑:可复现示例

我已将问题简化为可复现代码,需用Windows版clang-cl编译(MSVC因自定义ASM无法运行),附加选项添加/clang:-masm=intel,Release模式构建:

#include <Windows.h>

#pragma comment(lib, "ntdll.lib")

typedef struct _INITIAL_TEB
{
    PVOID OldStackBase;
    PVOID OldStackLimit;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID StackAllocationBase;
} INITIAL_TEB, * PINITIAL_TEB;

extern "C" NTSYSAPI NTSTATUS NTAPI RtlCreateUserStack(
    _In_opt_ SIZE_T CommittedStackSize,
    _In_opt_ SIZE_T MaximumStackSize,
    _In_opt_ ULONG_PTR ZeroBits,
    _In_ SIZE_T PageSize,
    _In_ ULONG_PTR ReserveAlignment,
    _Out_ PINITIAL_TEB InitialTeb);

static constexpr auto PAGE_SIZE = 4096;
static constexpr auto STACK_SIZE = PAGE_SIZE * 64;

DWORD64 g_oldStackBase;
DWORD64 g_oldStackLimit;
DWORD64 g_oldRSP;

NT_TIB64* g_pTEB = (NT_TIB64*)NtCurrentTeb();
uint32_t g_stackCount = 0;

inline void setStackLimits(char* pStackBase, char* pStackLimit)
{
    // 设置Windows特定栈变量
    auto* pTEB = (NT_TIB64*)NtCurrentTeb();

    g_oldStackBase = pTEB->StackBase;
    g_oldStackLimit = pTEB->StackLimit;

    pTEB->StackBase = DWORD64(pStackBase);
    pTEB->StackLimit = DWORD64(pStackLimit);
}

inline void restoreStackLimits(void)
{
    auto* pTEB = (NT_TIB64*)NtCurrentTeb();

    pTEB->StackBase = g_oldStackBase;
    pTEB->StackLimit = g_oldStackLimit;
}

// 重置栈限制并返回原栈
[[noreturn]] __attribute__((noinline)) inline void switchBack(void)
{
    restoreStackLimits();

    asm volatile("mov rsp,%0" : : "m"(g_oldRSP) : );
}

// 强制访问栈的后续页面(超出初始提交范围)
inline void growStack(void)
{
    // 必须至少两页大小才能稳定触发错误
    auto* pData = (char*)_alloca(8192);
    // 防止编译器优化
    asm volatile("" : : "r"(pData) : );
}

inline void executeSwitched(void)
{
    growStack();

    switchBack();
}

// 准备栈并切换到目标方法
__attribute__((noinline)) inline void switchStack(char* pStack, char* pLimit)
{
    auto* pStackBase = pStack;
    // 在栈底放置nullptr标记栈结束
    pStack -= sizeof(void*);
    new (pStack) const void* (nullptr);
    // 在栈顶放置目标函数,让ret指令跳转到该函数
    pStack -= sizeof(void*);
    new (pStack) const void*(&executeSwitched);

    asm volatile("mov %0,rsp" : "=m"(g_oldRSP) : :);

    setStackLimits(pStackBase, pLimit);

    // RSP不在破坏列表中,无需保存
    asm volatile("mov rsp,%0" : : "r"(pStack) : );
}

inline void executeFiber(LPVOID param)
{
    growStack();

    SwitchToFiber(param);
}

// 设为true则用纤程运行
static constexpr bool USE_FIBER = false;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    if (USE_FIBER)
    {
        const auto mainFiber = ConvertThreadToFiber(nullptr);

        while (true)
        {
            const auto fiber = CreateFiberEx(PAGE_SIZE, STACK_SIZE, 0, &executeFiber, mainFiber);
            if (!fiber)
                return -2;

            SwitchToFiber(fiber);

            g_stackCount++;
        }
    }
    else
    {
        while (true)
        {
            INITIAL_TEB teb;
            // 将第一个参数改为STACK_SIZE可修复崩溃
            if (RtlCreateUserStack(PAGE_SIZE, STACK_SIZE, 0, PAGE_SIZE, 65536, &teb))
                return -1;

            switchStack((char*)teb.StackBase, (char*)teb.StackLimit);

            g_stackCount++;
        }
    }

    return 0;
}

代码在多次切换栈后会随机抛出0x80000001异常,迭代次数从几百到几千不等,但很快会触发。关键在于持续分配栈但不释放;若使用后释放,异常极少出现。已确认问题与资源或地址空间耗尽无关,两者均有大量剩余。

对比纤程(fiber):即使不释放持续创建新栈也无异常。调试纤程源码发现核心差异:纤程始终提交整个栈范围,而我仅提交第一页。将我的代码改为提交整个栈也能解决问题——显然无保护页就不会触发保护页异常。

但此方案不适用于我的场景:我需要大量此类栈,为保证性能必须池化复用(否则性能下降3倍),提交整个栈会浪费大量内存。因此问题更新为:

  • 可复现示例中半随机触发未处理0x80000001异常的原因是什么?
  • 我的栈配置是否存在错误?还是当前场景下必然会出现此类问题,这也是纤程默认提交整个栈的原因?

我无法处理该异常:通过向量异常处理程序拦截后返回EXCEPTION_CONTINUE_EXECUTION会导致下一页触发访问违例(栈未增长)。我可以尝试手动修复栈页,但更想了解问题根源。

编辑2

我修改代码用CreateFiberEx模拟非完全提交栈,未出现Bug。但测试另一自定义纤程实现时,出现相同异常。因此问题可能并非我的代码存在明显遗漏。


内容的提问来源于stack exchange,提问作者Juliean

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 09:44:54