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

时间:2015-05-19 06:07:20

标签: c++ c multithreading winapi x86

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

  • Thread1正在运行
  • 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()函数上崩溃(访问被拒绝)。如果有任何建议,我会尝试替代。

2 个答案:

答案 0 :(得分:2)

它不可能。 (编辑:有可能像JS1所提到的那样成功地使用GetThreadContext等操作系统API切换,但其他限制仍然适用)

问题是,新线程需要先前的线程堆栈才能运行。您可以通过直接使用旧堆栈或将旧堆栈复制到新堆栈来实现。这些都不可能:由于依赖于堆栈的指针(例如帧指针),您无法复制堆栈,并且您无法使用旧堆栈,因为操作系统将检测到该线程已经过去超出堆栈,并抛出堆栈溢出或下溢。

如果操作系统没有检测到堆叠错位,则可能。如果是这种情况,那么您可以加载旧的ES​​P和EBP以使用旧堆栈(就像您一样)。您的当前代码有问题(前提是它甚至可以工作),因为您在保存堆栈指针(ESP)后推送了一些寄存器。当你重新加载ESP时,它就像你从来没有推过任何东西。 ESP指针确实是一个需要仔细处理的特殊情况。请注意,在这种情况下,您甚至不需要关心新堆栈,它只会被忽略。这意味着你不需要任何特殊的裸体声明。

另一个注意事项,如果你能够这样做,如果你没有恢复线程以前的代码流,那么这两个线程都不会终止。旧的线程不应该在新的运行时使用堆栈,因此它不能终止,并且新的不能在旧堆栈上终止。每个堆栈包含底部(或顶部,自上而下堆栈)的线程相关清理代码。

答案 1 :(得分:0)

作为一个FYI,我没有尝试过以下内容,但您可能能够通过裸功能(仅AFAIK Microsoft编译器)获得这样的工作: https://msdn.microsoft.com/en-us/library/5ekezyy2.aspx

存在大量限制:https://msdn.microsoft.com/en-us/library/4d12973a.aspx但是启动具有裸功能的线程并未列为限制。裸函数会删除prolog / epilog并允许您尝试从前一个线程传输上下文。

您也可以通过解释器执行此操作:基本上保存程序的解释状态并从单独的线程开始。

由于我没有考虑实际用例,我不确定你为什么要这样做。