Каков точный механизм, отвечающий за увеличение стека собственных приложений в Windows?
Я знаю основы, как защитные страницы используются в качестве однократного сигнала тревоги доступа для последующего выделения следующей страницы до тех пор, пока пространство не будет исчерпано, что включает в себя запрос TEB на предмет ограничений стека. Однако мне нужно понять точный процесс того, что происходит при доступе к такой защитной странице - где обрабатывается нарушение доступа, что вызывается...
Причина в том, что у меня есть очень странная, чрезвычайно редкая ошибка, связанная с проблемой роста стека. Реальная проблема слишком сложна, чтобы получить реальную помощь, но, по сути, у меня есть специально скомпилированный код, использующий сопрограммы со стеком, и каждую голубую луну я получаю следующее SEH-исключение при доступе к первой странице после первоначально выделенной:
Код: Выделить всё
0x80000001: Not implemented (Parameter: 0x0000000000000001, 0x00000011805BEFE8)
Код: Выделить всё
__try
{
// internally calls _chstck, but nothing
_alloca(160000);
}
__except(1)
{
int i = 0;
}
Код: Выделить всё
auto stack = event::YieldStack::Create();
// produces the exact SEH that I get when this error occurs
*(char**)(stack.GetStack() - 4097) = nullptr;
Код: Выделить всё
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 };
}
Код: Выделить всё
inline void setStackLimits(ExecutionStateJIT& state, char* pStackBase)
{
// set windows-specific stack variables
auto* pTIB = (NT_TIB*)NtCurrentTeb();
state.pOldStackBase = pTIB->StackBase;
state.pOldStackLimit = pTIB->StackLimit;
pTIB->StackBase = pStackBase;
pTIB->StackLimit = pStackBase - YieldStack::STACK_SIZE;
}
// somewhere later
mov rsp,pStackBase;
Код: Выделить всё
mov qword ptr [rsp],0; // at that point, RSP points to the next page
РЕДАКТИРОВАТЬ:< /p>
Мне удалось свести мою проблему к воспроизводимому примеру:
Код: Выделить всё
#include
#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)
{
// set windows-specific stack variables
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;
}
// reset stack-limits and ret back to old stack
[[noreturn]] __attribute__((noinline)) inline void switchBack(void)
{
restoreStackLimits();
asm volatile("mov rsp,%0" : : "m"(g_oldRSP) : );
}
// forces a read of the stacks next pages, beyond my initial reserve
inline void growStack(void)
{
//! must be at least two pages in size for error to trigger reliably
auto* pData = (char*)_alloca(8192);
//! prevent compiler from optimizing out
asm volatile("" : : "r"(pData) : );
}
inline void executeSwitched(void)
{
growStack();
switchBack();
}
// prepares stack for execution and switches to target-method
__attribute__((noinline)) inline void switchStack(char* pStack, char* pLimit)
{
auto* pStackBase = pStack;
//! place nullptr at bottom to indicate end of stack
pStack -= sizeof(void*);
new (pStack) const void* (nullptr);
//! place target-function at top, so that "ret" jumps into it at end of function
pStack -= sizeof(void*);
new (pStack) const void*(&executeSwitched);
asm volatile("mov %0,rsp" : "=m"(g_oldRSP) : :);
setStackLimits(pStackBase, pLimit);
//! RSP not part of clobber-list, as it does not need to be preserved
asm volatile("mov rsp,%0" : : "r"(pStack) : );
}
inline void executeFiber(LPVOID param)
{
growStack();
SwitchToFiber(param);
}
// enable to run on fiber instead
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 = CreateFiber(STACK_SIZE, &executeFiber, mainFiber);
if (!fiber)
return -2;
SwitchToFiber(fiber);
g_stackCount++;
}
}
else
{
while (true)
{
INITIAL_TEB teb;
//! change first param to STACK_SIZE to fix crashes
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. итераций успешного переключения между стеками - может быть < 1000, может быть > 3000, но это произойдет быстро. Хитрость заключается в том, чтобы просто продолжать распределять стеки, но не освобождать их. Если вы освободите стек после его завершения, это произойдет редко. Я убедился, что это не связано с нехваткой ресурсов или адресного пространства — на обоих концах еще много свободного места.
Я сравнил с оптоволокном, и они этого не сделали. Кажется, у меня такие проблемы, хотя я создаю их, не освобождая, что приводит к созданию нового стека на каждой итерации. Однако, пройдя через источник волокна, я смог увидеть ключевое отличие: волокно всегда фиксирует весь диапазон стека, а не то, что делаю я (фиксируя только первую страницу). И да, если сделать это для моего кода таким образом, это также решает проблему - очевидно, что если нет защитных страниц, не может быть и исключения для защитных страниц.
Однако для моего реального использования -Кстати, это не идеальный вариант. Мне нужно много таких стеков, и по соображениям производительности я объединяю и повторно использую их (если бы я этого не сделал, приложение замедлилось бы в 3 раза). Таким образом, фиксация всего стека приведет к потере большого количества памяти. Это оставляет мне обновленный вопрос:
Что заставляет необработанный 0x80000001 полуслучайно вызываться внутри моего работоспособного примера? Имейте в виду, что спам-резервирование адресов — это всего лишь способ заставить ошибку проявиться быстро — это может произойти в любое время, когда защитная страница стека обращается в первый раз, даже если этот стек был настроен и использовался ранее. Просто частота резко возрастает, когда вы рассылаете спам-запросы на выделение.
Что-то не так в моей настройке? Или это просто то, что может случиться, учитывая обстоятельства, и, может быть, именно поэтому Fiber фиксирует весь стек сразу? У меня нет возможности справиться с этой ошибкой, когда она возникает - мне нужен векторный обработчик исключений, чтобы перехватить ее, и возврат EXCEPTION_CONTINUE_EXECUTION для этого типа ошибки (помимо рискованности) просто приведет к следующему нарушению доступа к стекам. страница (так как она не выросла). Я мог бы попробовать исправить страницы стеков вручную, но мне бы вообще хотелось знать, почему это происходит.
Подробнее здесь: https://stackoverflow.com/questions/783 ... stom-stack
Мобильная версия