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

Posted

技术标签:

【中文标题】是否可以将线程执行转移到另一个线程?【英文标题】:Is it possible to transfer thread execution to another thread? 【发布时间】:2015-05-19 06:07:20 【问题描述】:

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

线程 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() 函数上崩溃(访问被拒绝)。如果有任何建议,我会尝试替代方案。

【问题讨论】:

您能解释一下为什么您认为这可以帮助您吗?这样,我们也许可以告诉您解决问题的正确方法。不是这个。 大卫,这完全是我自己的实验——探索可能性。目前没有与此相关的问题,只是一个实验。我可能需要一些技术解释为什么它会或不会起作用。谢谢! 这不起作用的原因有很多。我找不到试图说服你反对这项毫无意义的任务的动力。如果您想尝试这样做,请继续。 整个想法毫无意义。线程实际上只是线程 ID 和线程状态的组合。创建一个新线程会创建一个新状态和一个新 ID。是的,您也许可以交换线程状态,因此旧线程 ID 属于新线程状态,反之亦然。这并没有取得任何成果。线程 ID 用于记账。在您的簿记中交换线程 ID 会简单得多。 看看at this webpage。它使用 Windows 函数 GetThreadContext() 和 SetThreadContext() 执行您想要的操作。 【参考方案1】:

这是不可能的。 (编辑:使用像 JS1 提到的 GetThreadContext 这样的 OS API 可能会成功切换,但其他限制仍然适用)

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

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

另外请注意,如果您能够做到这一点,那么如果您不恢复线程之前的代码流,则两个线程都将无法终止。旧线程在新线程运行时不应该使用堆栈,所以它不能终止,新线程也不能在旧堆栈上终止。每个堆栈在底部(或顶部,对于自上而下堆栈)包含与线程相关的清理代码。

【讨论】:

即使操作系统没有注意到你移动了堆栈,一旦你窃取了旧线程的堆栈,它会做什么?您必须将其保留为僵尸线程,以使其堆栈为新线程保持活动状态。 ElderBug,esp实际上正确保存和恢复,线程可以正常终止。我将用提到的问题更新我的示例,并尝试提到的 Set/GetThreadContext。谢谢!【参考方案2】:

作为一个仅供参考,我没有尝试过以下操作,但您可能能够使用裸函数(仅适用于 AFAIK 的 Microsoft 编译器)获得类似这样的工作: https://msdn.microsoft.com/en-us/library/5ekezyy2.aspx

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

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

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

【讨论】:

以上是关于是否可以将线程执行转移到另一个线程?的主要内容,如果未能解决你的问题,请参考以下文章

插槽在哪个线程中执行,我可以将其重定向到另一个线程吗?

提升线程:是不是可以在移动到另一个线程之前限制线程的运行时间

将异常从一个线程重新抛出到另一个线程

将代码执行移至后台线程的阈值是多少? [关闭]

文本编辑器可以将每个按键从一个线程传递到另一个线程吗?

从不同线程将值添加到全局集中并等待所有线程完成