在库代码中,您不能在当前线程中完全交换堆栈(分配自己,删除旧的),因为在旧堆栈中有返回地址,可能是指向堆栈中变量的指针等。
并且您无法扩展堆栈(为其分配了虚拟内存(保留/提交)并且不可扩展)。
但是,在调用期间可以分配临时堆栈并切换到此堆栈。在这种情况下,您必须从NT_TIB
(请参阅winnt.h
中的此结构)保存旧的StackBase
和StackLimit
,设置新值(您需要为新堆栈分配内存),进行调用(要切换堆栈,您需要一些汇编代码-您不能仅使用),然后返回原始的StackBase
和StackLimit
。在内核模式下存在对此的支持 - KeExpandKernelStackAndCallout
。
但是,在用户模式下存在Fibers - 这很少使用,但看起来非常适合任务。使用Fiber,我们可以在当前线程内部创建附加堆栈/执行上下文。
因此,通常解决方案如下(针对库):
在DLL_THREAD_ATTACH
上:
ConvertThreadToFiber
)(如果它返回false
,还要检查GetLastError
是否为ERROR_ALREADY_FIBER
- 这也是可以的代码)CreateFiberEx
创建自己的纤程我们只需要执行一次。然后,每次调用需要大量堆栈空间的过程时:
GetCurrentFiber
来记住当前的纤程SwitchToFiber
切换到您的纤程SwitchToFiber
调用从调用GetCurrentFiber
保存的原始纤程再次返回最后,在DLL_THREAD_DETACH
上,您需要:
DeleteFiber
删除您的纤程ConvertFiberToThread
将纤程转换为线程,但仅在初始 ConvertThreadToFiber
返回 true
的情况下执行(如果是 ERROR_ALREADY_FIBER
,则让最先将线程转换为纤程的人将其转换回去 - 在这种情况下,这不是您的任务)您需要一些(通常很小的)与您的纤程/线程相关联的数据。这当然必须是每个线程变量。因此,您需要使用 __declspec(thread)
声明此数据。或者直接使用 TLS
(或存在哪些现代功能)
演示实现如下:
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);
}
因此,最好只在需要时为线程分配堆栈(即第一次调用时),而不是在每个新线程的DLL_THREAD_ATTACH
上通过OnAttach()
分配堆栈。
如果其他人也尝试使用纤程,这段代码可能会出现问题。例如,MSDN中的example代码没有检查ERROR_ALREADY_FIBER
,以防ConvertThreadToFiber
返回0。因此,如果我们在它之前决定创建纤程并且它在我们之后也尝试使用纤程,则可以等待主应用程序不正确处理此情况。此外,在xp中,ERROR_ALREADY_FIBER
无效(从vista开始)。
因此,可能还有另一种解决方案-自己创建线程堆栈,并在需要大量堆栈空间的调用期间暂时切换到该堆栈。主要需分配堆栈空间并交换esp(或rsp),但不要忘记在NT_TIB
中正确建立StackBase
和StackLimit
- 这是必要且充分的条件(否则异常和警戒页面扩展将无法工作)。
尽管这种替代解决方案需要更多的代码(手动创建线程堆栈和堆栈切换),但它也适用于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;
}
}
以及 x86 平台上用于 SwitchToStack
的汇编代码
@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=%s\n", 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 %08x\n", 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 handled\n", GetExceptionCode());
}
strcpy((PSTR)Parameter, "zTestCallout demo");
return NOERROR;
}
ULONG WINAPI TestCallout(PVOID param)
{
TestStackMemory();
DbgPrint("TestCallout(%s)\n", param);
return NOERROR;
}
_alloca
:https://msdn.microsoft.com/en-us/library/wb1s57t5.aspx。这也是为了提高性能,因为堆分配很慢,特别是如果多个线程并行从堆中分配(它们被相同的libc
/CRT
互斥锁阻塞,所以代码变成串行)。 - Serge Rogatch_alloca()
。我也会把这个加到问题里。 - Serge Rogatch