Установить размер стека программно в Windows

Можно ли в WinAPI установить размер стека для текущего потока во время выполнения, как setrlimit делает на Linux?
Я имею в виду увеличить зарезервированный размер стека для текущего потока, если он слишком мал для текущих требований.
Это находится в библиотеке, которая может вызываться потоками из других языков программирования, так что нельзя выбирать размер стека во время компиляции.

Если нет, есть ли какие-либо идеи по поводу решения, такого как сборочный батут, который изменяет указатель стека на динамически выделяемый блок памяти?

Часто задаваемые вопросы: поток прокси является верным решением (если только поток вызывающей стороны не имеет очень маленького стека). Однако переключение потоков кажется снижением производительности. Мне нужно значительное количество стека для рекурсии или для _alloca. Это также для производительности, потому что распределение кучи происходит медленно, особенно если несколько потоков выделяются из кучи параллельно (они блокируются одним и тем же libc/CRT мьютекс, поэтому код становится последовательным).

4

Решение

Вы не можете заполнить стек подкачки в текущем потоке (выделить себя, удалить старый) в коде библиотеки, потому что в старом стеке — адреса возврата, могут быть указатели на переменные в стеке и т. д.

и вы не можете расширить стек (виртуальная память для него уже выделена (зарезервирована / зафиксирована) и не расширяется.

однако возможно выделить временный стек и переключиться на этот стек во время вызова. Вы должны в этом случае сохранить старое StackBase а также StackLimit от NT_TIB (посмотрите эту структуру в winnt.h), установите новые значения (вам нужно выделить память для нового стека), сделайте вызов (для стека коммутаторов вам нужен код сборки — вы не можете сделать это только на C / C ++) и вернуть оригинал StackBase а также StackLimit, в kernelmode существует поддержка для этого — KeExpandKernelStackAndCallout

Однако в пользовательском режиме существуют Волокна — это очень редко используется, но выглядит как идеально подходит для задачи. с Fiber мы можем создать дополнительный контекст стека / исполнения внутри текущая тема.

В общем, решение следующее (для библиотеки):

на DLL_THREAD_ATTACH :

  1. преобразовать нить в волокно
    (ConvertThreadToFiber) (если он вернется false проверьте также
    GetLastError за ERROR_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_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() гораздо лучше выделять его только для потоков, когда это действительно необходимо (при первом вызове)

и этот код может потенциально иметь проблемы с волокнами, если кто-то еще также попытается использовать волокна. скажи в мсдн пример код не проверяется на ERROR_ALREADY_FIBER в случае ConvertThreadToFiber верните 0. поэтому мы можем подождать, что это дело будет неправильно обработано основным приложением, если мы перед тем, как решим создать волокно, а также попробуем использовать волокно после нас. также ERROR_ALREADY_FIBER не работал в хр (начать с перспективы).

Так что возможно и другое решение — самостоятельно создать стек потоков и временно переключиться на него с помощью вызовов, которые требуют большого стекового пространства. Главное нужно не только выделить место под стек и подкачку особ (или же RSP) но не забудьте исправить установить StackBase а также StackLimit в NT_TIB — это необходимое и достаточное условие (в противном случае исключения и расширение страницы защиты не будут работать).

несмотря на то, что для этого альтернативного решения требуется больше кода (вручную создайте стек потоков и переключите стек), он будет работать и на 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

и для х64:

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;
}
7

Другие решения

Максимальный размер стека определяется при создании потока. Это не может быть изменено после этого времени.

0

По вопросам рекламы [email protected]