在 Windows 上以编程方式设置堆栈大小

Set stack size programmatically on Windows

本文关键字:设置 堆栈 方式 编程 Windows      更新时间:2023-10-16

WinAPI 中是否可以像在 Linux 上那样在运行时为当前线程设置setrlimit堆栈大小? 我的意思是,如果当前线程对于当前要求来说太小,则增加它。 这是在一个库中,该库可能由其他编程语言的线程调用,因此在编译时设置堆栈大小不是一个选项。

如果没有,关于将堆栈指针更改为动态分配的内存块的程序集蹦床等解决方案的任何想法?

常见问题解答:代理线程是一个万无一失的解决方案(除非调用方线程的堆栈非常小)。但是,线程切换似乎是一个性能杀手。我需要大量的堆栈来进行递归或_alloca.这也是为了性能,因为堆分配很慢,特别是如果多个线程从堆并行分配(它们被相同的libc/CRT互斥锁阻塞,因此代码变为串行)。

您不能在库代码中完全交换当前线程中的堆栈(分配自身,删除旧),因为在旧堆栈中 - 返回地址,可能是指向堆栈中变量的指针等。

并且您不能扩展堆栈(已为其分配(保留/提交)且不可扩展的虚拟内存。

但是,在调用期间,您可以分配临时堆栈并切换到此堆栈。 在这种情况下,您必须从NT_TIB中保存旧的StackBaseStackLimit(在winnt.h中查看此结构),设置新值(您需要为新堆栈分配内存),执行调用(对于切换堆栈,您需要一些汇编代码 - 您不能仅在C/C ++上执行此操作)并返回原始StackBaseStackLimit。 在内核模式下存在对这 -KeExpandKernelStackAndCallout

但是在用户模式下存在纤维 - 这很少使用,但看起来与任务完全匹配。 使用 Fiber,我们可以在当前线程中创建额外的堆栈/执行上下文。

所以一般来说,解决方案是下一个(对于库):

DLL_THREAD_ATTACH

  1. 将线转换为纤维 (ConvertThreadToFiber)(如果返回false也请检查GetLastErrorERROR_ALREADY_FIBER- 这也是可以的代码)
  2. 并通过调用CreateFiberEx创建自己的光纤

我们只这样做一次。 而不是每次调用您的过程时,这需要较大的堆栈空间:

  1. 通过呼叫GetCurrentFiber记住当前的光纤
  2. 光纤的设置任务
  3. 通过呼叫切换到光纤SwitchToFiber
  4. 光纤内部调用过程
  5. 返回到原始光纤(从呼叫GetCurrentFiber保存) 再次通过SwitchToFiber

最后,您需要DLL_THREAD_DETACH

  1. DeleteFiber删除光纤
  2. 通过调用ConvertFiberToThread将光纤转换为线程,但仅 如果初始ConvertThreadToFiber返回true(如果是ERROR_ALREADY_FIBER- 让谁先将线转换为光纤转换 它回来了 - 在这种情况下这不是你的任务)

您需要一些与您的纤维/线相关的(通常是小)数据。 这当然必须是每个线程变量。 因此,您需要使用__declspec(thread)来声明此数据。 或直接使用TLS(或为此存在哪些现代C++功能)

接下来是演示实现:

typedef ULONG (WINAPI * MY_EXPAND_STACK_CALLOUT) (PVOID Parameter);
class FIBER_DATA 
{
public:
PVOID _PrevFiber, _MyFiber;
MY_EXPAND_STACK_CALLOUT _pfn;
PVOID _Parameter;
ULONG _dwError;
BOOL _bConvertToThread;
static VOID CALLBACK _FiberProc( PVOID lpParameter)
{
reinterpret_cast<FIBER_DATA*>(lpParameter)->FiberProc();
}
VOID FiberProc()
{
for (;;)
{
_dwError = _pfn(_Parameter);
SwitchToFiber(_PrevFiber);
}
}
public:
~FIBER_DATA()
{
if (_MyFiber)
{
DeleteFiber(_MyFiber);
}
if (_bConvertToThread)
{
ConvertFiberToThread();
}
}
FIBER_DATA()
{
_bConvertToThread = FALSE, _MyFiber = 0;
}
ULONG Create(SIZE_T dwStackCommitSize, SIZE_T dwStackReserveSize);
ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
_PrevFiber = GetCurrentFiber();
_pfn = pfn;
_Parameter = Parameter;
SwitchToFiber(_MyFiber);
return _dwError;
}
};
__declspec(thread) FIBER_DATA* g_pData;
ULONG FIBER_DATA::Create(SIZE_T dwStackCommitSize, SIZE_T dwStackReserveSize)
{
if (ConvertThreadToFiber(this))
{
_bConvertToThread = TRUE;
}
else
{
ULONG dwError = GetLastError();
if (dwError != ERROR_ALREADY_FIBER)
{
return dwError;
}
}
return (_MyFiber = CreateFiberEx(dwStackCommitSize, dwStackReserveSize, 0, _FiberProc, this)) ? NOERROR : GetLastError();
}
void OnDetach()
{
if (FIBER_DATA* pData = g_pData)
{
delete pData;
}
}
ULONG OnAttach()
{
if (FIBER_DATA* pData = new FIBER_DATA)
{
if (ULONG dwError = pData->Create(2*PAGE_SIZE, 512 * PAGE_SIZE))
{
delete pData;
return dwError;
}
g_pData = pData;
return NOERROR;
}
return ERROR_NO_SYSTEM_RESOURCES;
}
ULONG WINAPI TestCallout(PVOID param)
{
DbgPrint("TestCallout(%s)n", param);
return NOERROR;
}
ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
if (FIBER_DATA* pData = g_pData)
{
return pData->DoCallout(pfn, Parameter);
}
return ERROR_GEN_FAILURE;
}
if (!OnAttach())//DLL_THREAD_ATTACH
{
DoCallout(TestCallout, "Demo Task #1");
DoCallout(TestCallout, "Demo Task #2");
OnDetach();//DLL_THREAD_DETACH
}

另请注意,在单线程上下文中执行的所有纤程 - 与线程关联的多个纤程不能同时执行 - 只能按顺序执行,并且您自己控制切换时间。 所以不需要任何额外的同步。 和SwitchToFiber- 这是完整的用户模式过程。 执行速度非常快,永远不会失败(因为从不分配任何资源)


更新


尽管使用更简单__declspec(thread) FIBER_DATA* g_pData;(更少的代码),但更适合实现直接使用TlsGetValue/TlsSetValue并在线程内的第一次调用时分配FIBER_DATA,但不是所有线程。__declspec(thread)XP中为 DLL 工作(根本没有工作)也不正确。 所以一些修改可以是

DLL_PROCESS_ATTACH分配您的 TLS插槽gTlsIndex = TlsAlloc();

并在DLL_PROCESS_DETACH上释放它

if (gTlsIndex != TLS_OUT_OF_INDEXES) TlsFree(gTlsIndex);

在每次DLL_THREAD_DETACH通知调用中

void OnThreadDetach()
{
if (FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex))
{
delete pData;
}
}

并且DoCallout需要以另一种方式进行修改

ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex);
if (!pData)
{
// this code executed only once on first call
if (!(pData = new FIBER_DATA))
{
return ERROR_NO_SYSTEM_RESOURCES;
}
if (ULONG dwError = pData->Create(512*PAGE_SIZE, 4*PAGE_SIZE))// or what stack size you need
{
delete pData;
return dwError;
}
TlsSetValue(gTlsIndex, pData);
}
return pData->DoCallout(pfn, Parameter);
}

因此,相反,通过OnAttach()DLL_THREAD_ATTACH上的每个新线程分配堆栈,以便仅在真正需要时才将其分配给线程(第一次调用时)

如果其他人也尝试使用光纤,则此代码可能会遇到光纤问题。 在 msdn 示例代码中说,如果返回 0ConvertThreadToFiber则不检查ERROR_ALREADY_FIBER。 因此,如果我们在决定创建光纤之前,并且它还尝试在我们之后使用光纤,我们可以等待主应用程序不正确地处理这种情况。ERROR_ALREADY_FIBERXP中也不起作用(从Vista开始)。

所以可能和另一种解决方案 - 自己创建线程堆栈,并临时切换到需要大量堆栈空间的调用。 主要不仅需要为堆栈和交换ESP(或RSP)分配空间,而且不要忘记在NT_TIB中正确建立StackBaseStackLimit- 这是必要且充分的条件(否则异常和保护页面扩展将不起作用)。

尽管这种替代解决方案需要更多的代码(手动创建线程堆栈和堆栈切换),但它也可以在XP上运行,并且在其他人也尝试在线程中使用光纤的情况下没有任何影响

typedef ULONG (WINAPI * MY_EXPAND_STACK_CALLOUT) (PVOID Parameter);
extern "C" PVOID __fastcall SwitchToStack(PVOID param, PVOID stack);
struct FIBER_DATA
{
PVOID _Stack, _StackLimit, _StackPtr, _StackBase;
MY_EXPAND_STACK_CALLOUT _pfn;
PVOID _Parameter;
ULONG _dwError;
static void __fastcall FiberProc(FIBER_DATA* pData, PVOID stack)
{
for (;;)
{
pData->_dwError = pData->_pfn(pData->_Parameter);
// StackLimit can changed during _pfn call
pData->_StackLimit = ((PNT_TIB)NtCurrentTeb())->StackLimit;
stack = SwitchToStack(0, stack);
}
}
ULONG Create(SIZE_T Reserve, SIZE_T Commit);
ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
_pfn = pfn;
_Parameter = Parameter;
PNT_TIB tib = (PNT_TIB)NtCurrentTeb();
PVOID StackBase = tib->StackBase, StackLimit = tib->StackLimit;
tib->StackBase = _StackBase, tib->StackLimit = _StackLimit;
_StackPtr = SwitchToStack(this, _StackPtr);
tib->StackBase = StackBase, tib->StackLimit = StackLimit;
return _dwError;
}
~FIBER_DATA()
{
if (_Stack)
{
VirtualFree(_Stack, 0, MEM_RELEASE);
}
}
FIBER_DATA()
{
_Stack = 0;
}
};
ULONG FIBER_DATA::Create(SIZE_T Reserve, SIZE_T Commit)
{
Reserve = (Reserve + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
Commit = (Commit + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1);
if (Reserve < Commit || !Reserve)
{
return ERROR_INVALID_PARAMETER;
}
if (PBYTE newStack = (PBYTE)VirtualAlloc(0, Reserve, MEM_RESERVE, PAGE_NOACCESS))
{
union {
PBYTE newStackBase;
void** ppvStack;
};
newStackBase = newStack + Reserve;
PBYTE newStackLimit = newStackBase - Commit;
if (newStackLimit = (PBYTE)VirtualAlloc(newStackLimit, Commit, MEM_COMMIT, PAGE_READWRITE))
{
if (Reserve == Commit || VirtualAlloc(newStackLimit - PAGE_SIZE, PAGE_SIZE, MEM_COMMIT, PAGE_READWRITE|PAGE_GUARD))
{
_StackBase = newStackBase, _StackLimit = newStackLimit, _Stack = newStack;
#if defined(_M_IX86) 
*--ppvStack = FiberProc;
ppvStack -= 4;// ebp,esi,edi,ebx
#elif defined(_M_AMD64)
ppvStack -= 5;// x64 space
*--ppvStack = FiberProc;
ppvStack -= 8;// r15,r14,r13,r12,rbp,rsi,rdi,rbx
#else
#error "not supported"
#endif
_StackPtr = ppvStack;
return NOERROR;
}
}
VirtualFree(newStack, 0, MEM_RELEASE);
}
return GetLastError();
}
ULONG gTlsIndex;
ULONG DoCallout(MY_EXPAND_STACK_CALLOUT pfn, PVOID Parameter)
{
FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex);
if (!pData)
{
// this code executed only once on first call
if (!(pData = new FIBER_DATA))
{
return ERROR_NO_SYSTEM_RESOURCES;
}
if (ULONG dwError = pData->Create(512*PAGE_SIZE, 4*PAGE_SIZE))
{
delete pData;
return dwError;
}
TlsSetValue(gTlsIndex, pData);
}
return pData->DoCallout(pfn, Parameter);
}
void OnThreadDetach()
{
if (FIBER_DATA* pData = (FIBER_DATA*)TlsGetValue(gTlsIndex))
{
delete pData;
}
}

SwitchToStack的汇编代码:在x86上

@SwitchToStack@8 proc
push    ebx
push    edi
push    esi
push    ebp
xchg    esp,edx
mov     eax,edx
pop     ebp
pop     esi
pop     edi
pop     ebx
ret
@SwitchToStack@8 endp

对于 x64:

SwitchToStack proc
push    rbx
push    rdi
push    rsi
push    rbp
push    r12
push    r13
push    r14
push    r15
xchg    rsp,rdx
mov     rax,rdx
pop     r15
pop     r14
pop     r13
pop     r12
pop     rbp
pop     rsi
pop     rdi
pop     rbx
ret
SwitchToStack endp

使用/测试可以是下一个:

gTlsIndex = TlsAlloc();//DLL_PROCESS_ATTACH
if (gTlsIndex != TLS_OUT_OF_INDEXES)
{
TestStackMemory();
DoCallout(TestCallout, "test #1");
//play with stack, excepions, guard pages
PSTR str = (PSTR)alloca(256);
DoCallout(zTestCallout, str);
DbgPrint("str=%sn", str);
DoCallout(TestCallout, "test #2");
OnThreadDetach();//DLL_THREAD_DETACH
TlsFree(gTlsIndex);//DLL_PROCESS_DETACH
}
void TestMemory(PVOID AllocationBase)
{
MEMORY_BASIC_INFORMATION mbi;
PVOID BaseAddress = AllocationBase;
while (VirtualQuery(BaseAddress, &mbi, sizeof(mbi)) >= sizeof(mbi) && mbi.AllocationBase == AllocationBase)
{
BaseAddress = (PBYTE)mbi.BaseAddress + mbi.RegionSize;
DbgPrint("[%p, %p) %p %08x %08xn", mbi.BaseAddress, BaseAddress, (PVOID)(mbi.RegionSize >> PAGE_SHIFT), mbi.State, mbi.Protect);
}
}
void TestStackMemory()
{
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery(_AddressOfReturnAddress(), &mbi, sizeof(mbi)) >= sizeof(mbi))
{
TestMemory(mbi.AllocationBase);
}
}
ULONG WINAPI zTestCallout(PVOID Parameter)
{
TestStackMemory();
alloca(5*PAGE_SIZE);
TestStackMemory();
__try
{
*(int*)0=0;
} 
__except(EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("exception %x handledn", GetExceptionCode());
}
strcpy((PSTR)Parameter, "zTestCallout demo");
return NOERROR;
}
ULONG WINAPI TestCallout(PVOID param)
{
TestStackMemory();
DbgPrint("TestCallout(%s)n", param);
return NOERROR;
}

最大堆栈大小是在创建线程时确定的。在此之后无法对其进行修改。