2015-05-19 4 views
1

В настоящее время я экспериментирую с возможностями переноса выполнения потока на другой вновь созданный поток из текущего потока (я надеюсь, что это правильное слово); Вот иллюстрация:Можно ли передать выполнение потока в другой поток?

  • Резьба1 работает
  • Резьба1 остановки в середине кода и создать thread2
  • Резьба2 продолжается с середины кода, где Резьба1 остановить

EDIT: Обновлен пример.

#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() (доступ запрещен). Я попробую вариант, если есть какое-либо предложение.

+6

Можете ли вы объяснить, почему вы думаете, что это может вам помочь? Таким образом, мы можем сказать вам правильное решение вашей проблемы. Это не так. –

+0

Дэвид, это полностью мой собственный эксперимент - изучение возможностей. В настоящее время нет проблем, связанных с этим, просто эксперимент. Мне может понадобиться какое-то техническое объяснение, почему оно или не будет работать. Благодаря! –

+0

Существует множество причин, по которым это не сработает. Я не могу найти мотивацию, чтобы попытаться убедить вас в этой бессмысленной задаче. Если вы хотите попытаться это сделать, продолжайте. –

ответ

0

В качестве FYI, я не пробовал следующее, но вполне возможно, что вы могли бы получить что-то, чтобы работать, как это с голой функцией (AFAIK только компиляторы Microsoft): https://msdn.microsoft.com/en-us/library/5ekezyy2.aspx

Есть Значительное количество ограничений: https://msdn.microsoft.com/en-us/library/4d12973a.aspx, но запуск нити с открытой функцией не указан в качестве ограничения. Голая функция удалит пролог/эпилог и позволит вам попытаться перенести контекст из предыдущего потока.

Вы также можете сделать это через интерпретатор: в основном сохранить интерпретируемое состояние программы и начать с отдельного потока.

Как я могу думать о фактическом использовании, я не уверен, почему вы когда-нибудь захотите это сделать.

2

Это будет невозможно. (EDIT: Это может быть возможным, чтобы успешно перейти с API, ОС, как GetThreadContext, как упоминалось СП1, но другие ограничения все еще применяются)

Дело в том, новый поток должен предыдущий стек нити для запуска. Вы можете сделать это, используя непосредственно старый стек или скопировав старый стек в новый стек. Ни одно из них не возможно: вы не можете копировать стек из-за указателей, зависящих от стека (например, указатели на кадры), и вы не можете использовать старый стек, потому что ОС обнаружит, что поток вышел из его стека , и выбросить переполнение стека или переполнение стека.

Возможно, если ОС не обнаружит неправильное смещение стека. Если это так, то вы можете загрузить старый ESP и EBP, чтобы использовать старый стек (как и вы). У вас есть проблемы с вашим текущим кодом (при условии, что он вообще может работать), потому что вы нажимаете некоторые регистры ПОСЛЕ того, как вы сохранили указатель стека (ESP). Когда вы перезагружаете ESP, это похоже на то, что вы никогда ничего не толкали. Указатель ESP действительно является особым случаем, который необходимо тщательно обрабатывать. Обратите внимание, что в этом случае вам даже не нужно заботиться о новом стеке, это просто будет проигнорировано. Это означает, что вам не нужна специальная обнаженная декларация.

Другое примечание. Если вы в состоянии это сделать, ни поток не сможет завершить работу, если вы не восстановите потоки предыдущего потока кода. Старый поток не должен использовать стек во время работы нового, поэтому он не может завершаться, а новый не может завершаться в старом стеке. Каждый стек содержит нитевидный код очистки внизу (или сверху, для стека сверху вниз).

+0

Даже если ОС не заметила, что вы переместились через стек, что будет делать старый поток, когда вы украдете его стек? Вы должны держать его как нить зомби, чтобы сохранить свой стек в новом потоке. – MSalters

+0

ElderBug, esp фактически правильно сохранен и восстановлен, и поток может быть закончен правильно. Я обновлю пример с упомянутой проблемой и попробую упомянуть Set/GetThreadContext. Благодаря! –