繁体   English   中英

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

[英]Set stack size programmatically on Windows

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

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

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

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

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

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

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

所以一般解决方案是下一个(对于图书馆):

DLL_THREAD_ATTACH

  1. 将线程转换为光纤( ConvertThreadToFiber )(如果它返回false检查也GetLastError for ERROR_ALREADY_FIBER - 这也是好的代码)
  2. 并通过调用CreateFiberEx创建自己的 Fiber

我们只做一次。 比,每次调用您的过程时,这需要大量堆栈空间:

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

最后在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 示例代码中说,如果ConvertThreadToFiber返回 0,则不检查ERROR_ALREADY_FIBER 。因此,如果我们在决定创建光纤之前,我们可以等待主应用程序不正确处理这种情况,并且它也尝试在我们之后使用光纤。 ERROR_ALREADY_FIBERxp 中也不起作用(从 vista 开始)。

所以可能和另一种解决方案 - 自己创建线程堆栈,并临时切换到它做需要大堆栈空间的调用。 主不仅需要分配堆栈和交换空间ESP(RSP),但不要忘了正确建立StackBaseStackLimitNT_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

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

最大堆栈大小是在创建线程时确定的。 过了那个时间就不能修改了。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM