是否可以将线程执行转移到另一个线程?

问题描述 投票:0回答:2

我目前正在尝试将线程执行从当前线程转移到另一个新创建的线程的可能性(我希望这是一个正确的词);插图如下:

  • 线程1正在运行
  • Thread1 在代码中间停止并创建 Thread2
  • Thread2 从 Thread1 停止的代码中间继续执行

编辑:更新了示例。

#include "stdafx.h"
#include <memory>
#include <windows.h>
#include <cassert>

int _eax, _ebx, _ecx, _edx;
int _ebp, _esp, _esi, _edi;
int _eip;
int _flags;
int _jmp_addr;
bool thread_setup = false;
CONTEXT PrevThreadCtx;
HANDLE thread_handle;

int _newt_esp;
int _newt_ret;

DWORD WINAPI RunTheThread(LPVOID lpParam)
{
    // 1000 is more than enough, call to CreateThread() should already return by now.
    Sleep(1000);

    ResumeThread(thread_handle);
    return 0;
}

DWORD WINAPI DummyPrologueEpilogue(LPVOID lpParam)
{
    return 123;
}

__declspec(naked) void TransferThread(LPVOID lpParam)
{
    //longjmp(jmpbuf, 0);=
    __asm
    {
        call get_eip;
        cmp[_newt_esp], 0;
        mov[_newt_ret], eax;
        jz setup_new_thread;
        jmp DummyPrologueEpilogue;

get_eip:
        mov eax, [esp];
        ret;

setup_new_thread:
        pushad;
        mov[_newt_esp], esp;

        mov eax, [_flags];
        push eax;
        popfd;

        mov eax, [_eax];
        mov ebx, [_ebx];
        mov ecx, [_ecx];
        mov edx, [_edx];

        mov ebp, [_ebp];
        mov esp, [_esp];
        mov esi, [_esi];
        mov edi, [_edi];

        jmp [_eip];
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    int x = 100;
    char szTest[256];

    sprintf_s(szTest, "x = %d", x);

    //HideThread();

    //setjmp(jmpbuf);

    __asm
    {
        // Save all the register
        mov[_eax], eax;
        mov[_ebx], ebx;
        mov[_ecx], ecx;
        mov[_edx], edx;

        mov[_ebp], ebp;
        mov[_esp], esp;
        mov[_esi], esi;
        mov[_edi], edi;

        push eax;

        // Save the flags
        pushfd;
        pop eax;
        mov[_flags], eax;

        // If we on *new thread* jmp to end_asm, otherwise continue...
        call get_eip;
        mov[_eip], eax;
        mov al, byte ptr[thread_setup];
        test al, al;
        jnz end_asm;

        mov eax, [jmp_self];
        mov[_jmp_addr], eax;

        pop eax;

        mov[_newt_esp], 0;
        mov byte ptr[thread_setup], 1;
        push 0;
        push CREATE_SUSPENDED;
        push 0;
        push TransferThread;
        push 0;
        push 0;
        call CreateThread;
        mov [thread_handle], eax;

        // Create another thread just to resume 'TransferThread()'/*new thread* to give time to
        // __stdcall below to return properly, thus restoring the stack.
        // So the *new thread* does not accidentally pop the value from stacks or the __stdcall cleanup
        // code doesn't accidentally overwrites new pushed value from *new thread*.
        push 0;
        push 0;
        push 0;
        push RunTheThread;
        push 0;
        push 0;
        call CreateThread;

        // Jump to self, consumes CPU
jmp_self:
        jmp jmp_self;
        nop;
        nop;
        jmp end_asm;

get_eip:
        mov eax, [esp];
        ret;
end_asm:
    }

    // Test stack-based variable
    MessageBoxA(0, szTest, "Hello World!", MB_OK);
    assert(x = 100);

    x += GetCurrentThreadId();
    sprintf_s(szTest, "x = %d", x);

    HMODULE hMod = LoadLibrary(TEXT("comctl32"));
    FreeLibrary(hMod);

    try
    {
        std::unique_ptr<char[]> pTest(new char[256]);

        sprintf_s(pTest.get(), 256, "WinApi call test. Previous loadLibrary() call return %X", hMod);
        MessageBoxA(0, pTest.get(), "Hello World!", MB_OK);
    } catch (...) {}

    char *pszTest = (char*) malloc(256);
    if (pszTest)
    {
        float f = 1.0;
        f *= (float) GetCurrentThreadId();

        sprintf_s(pszTest, 256, "Current Thread ID = %X, Thread handle = %X, FP Test = %f", GetCurrentThreadId(), GetCurrentThread(), f);
        MessageBoxA(0, pszTest, "Hello World!", MB_OK);

        free( pszTest );
    }

    // printf() from *new thread* will fail on stkchk()
    //printf("Simple test\n");

    // Let's terminate this *new* thread and continue the old thread
    if (thread_setup)
    {
        DWORD OldProtect;
        thread_setup = false;

        VirtualProtect((PVOID)_jmp_addr, 2, PAGE_EXECUTE_READWRITE, &OldProtect);
        *(int*)(_jmp_addr) = 0x90909090; // Prev thread not suspended. Just hope this op is atomic.

        // Operation below will change the stack pointer
        //VirtualProtect((PVOID)_jmp_addr, 2, OldProtect, &OldProtect);
        //FlushInstructionCache(GetCurrentProcess(), (PVOID)_jmp_addr, 2);

        __asm {
            push eax;
            mov eax, jmp_self2;
            mov[_jmp_addr], eax;
            pop eax;
jmp_self2:
            jmp jmp_self2;
            nop;
            nop;
            mov esp, [_newt_esp];
            popad;
            jmp _newt_ret;
        }
    }
    else
    {
        DWORD OldProtect;
        VirtualProtect((PVOID)_jmp_addr, 2, PAGE_EXECUTE_READWRITE, &OldProtect);
        *(int*)(_jmp_addr) = 0x90909090; // Prev thread not suspended. Just hope this op is atomic.
    }

    // Show both thread can be exited cleanly... with some hacks.
    DWORD dwStatus;
    while (GetExitCodeThread(thread_handle, &dwStatus) && dwStatus == STILL_ACTIVE) Sleep(10);
    printf("*New Thread* exited with status %d (Expected 123), Error=%X\n", dwStatus, GetLastError());
    assert(dwStatus == 123);

    printf("Test printf from original thread!\n");
    printf("printf again!\n");
    printf("and again!\n");
    Sleep( 1000 );

    return 0;
}

代码读起来可能很困难,因为它主要由 asm 组成。所以我添加了一些评论来提供帮助。现在我测试了一下,很有可能,但是有一些问题。调用几个 win api 似乎没问题,但调用 printf 肯定会在 stkchk() 函数上崩溃(访问被拒绝)。如果有任何建议,我会尝试替代方案。

c++ c multithreading winapi x86
2个回答
3
投票

编辑:可能可以使用 JS1 提到的

GetThreadContext
等操作系统 API 和/或一些特定于操作系统的摆弄来成功切换,但其他限制仍然适用。

问题是,新线程需要先前的线程堆栈才能运行。您可以通过直接使用旧堆栈或将旧堆栈复制到新堆栈来完成此操作。这些都不容易:由于依赖于堆栈的指针(例如帧指针),您无法复制堆栈,并且无法使用旧堆栈,因为操作系统会检测到线程已脱离其堆栈,并引发堆栈溢出或下溢。

复制并使用新的堆栈是不现实的,因为您无法可靠地识别指向堆栈的指针。您可以尝试合理的启发式,但您永远无法 100% 确定您是否正确。

要重用旧堆栈,您需要更新操作系统的堆栈检测机制,以便操作系统不会抛出堆栈错误。在 Windows 上,您需要更新线程信息块中的堆栈指针。可能有也可能没有一些其他信息可能需要更新,我没有进行详尽的研究(也许是异常/堆栈展开数据?)。

您当前的代码存在一些问题,因为您在保存堆栈指针(ESP)后推送了一些寄存器。当你重新加载 ESP 时,就像你从未推送过任何东西一样。 ESP 指针确实是一种特殊情况,需要小心处理。请注意,在这种情况下,您甚至不需要关心新堆栈,它只会被忽略。这意味着您不需要任何特殊的裸声明。

另一个注意事项,如果您能够执行此操作,则终止时可能会出现其他问题。清理和释放堆栈时会发生什么?如果旧线程终止,它的堆栈不会被释放吗?如果堆栈被更改,我也不确定新堆栈是否会被正确释放。


0
投票

仅供参考,我还没有尝试过以下操作,但您可能能够使用裸函数获得像这样的功能(据我所知仅限 Microsoft 编译器): https://msdn.microsoft.com/en-us/library/5ekezyy2.aspx

存在大量限制:https://msdn.microsoft.com/en-us/library/4d12973a.aspx,但使用裸函数启动线程并未被列为限制。裸函数将删除序言/结尾,并允许您尝试从前一个线程传输上下文。

您也可以通过解释器来完成此操作:基本上保存程序的解释状态并在单独的线程上启动。

由于我想不出实际的用例,我不确定你为什么要这样做。

© www.soinside.com 2019 - 2024. All rights reserved.