Windows自定义栈半随机触发0x80000001异常原因排查
原问题背景
我想了解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

