簡體   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