线程操作与进程挂起(Windows核心编程)
阅读原文时间:2023年07月09日阅读:1
  • 对于挂起进程,挂起线程则比较简单,利用 ResumeThread 与 SuspendThread 即可,当线挂起时线程用户状态下的一切操作都会暂停

    #include
    #include
    #include
    using namespace std;

    unsigned __stdcall ThreadFun1(void *pvParam);

    int main(int argc, char *argv[])
    {
    DWORD ThreadId1 = NULL;
    HANDLE MyThread1 = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun1, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId1);

    // 挂起进程
    SuspendThread(MyThread1);
    cout << "线程开始挂起" << endl;
    
    // 重新释放线程,也就是运行挂起的线程
    ResumeThread(MyThread1);
    ResumeThread(MyThread1);
    
    WaitForSingleObject(MyThread1, INFINITE);
    
    CloseHandle(MyThread1);
    return 0;

    }

    unsigned __stdcall ThreadFun1(void *pvParam)
    {
    cout << "ThreadFun1" << endl;
    return true;
    }

  • 为什么进行了两次 ResumeThread 操作呢,是因为 CREATE_SUSPENDED 也会将线程内核计数加 1 变为了 2

  • 使用 SwitchToThread 切换线程

    #include
    #include
    #include
    using namespace std;

    unsigned __stdcall ThreadFun1(void *pvParam);
    unsigned __stdcall ThreadFun2(void *pvParam);

    int main(int argc, char *argv[])
    {
    DWORD ThreadId1 = NULL; DWORD ThreadId2 = NULL;

    // 创建两个线程,并运行
    HANDLE MyThread1 = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun1, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId1); ResumeThread(MyThread1);
    HANDLE MyThread2 = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun2, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId2); ResumeThread(MyThread2);
    
    // 等待线程关闭
    WaitForSingleObject(MyThread1, INFINITE); WaitForSingleObject(MyThread2, INFINITE);
    
    // 关闭句柄
    CloseHandle(MyThread1); CloseHandle(MyThread2);
    return 0;

    }

    unsigned __stdcall ThreadFun1(void *pvParam)
    {
    cout << " 切换 ThreadFun2 " << endl;
    SwitchToThread();
    cout << " ThreadFun1 is start! " << endl;
    return true;
    }
    unsigned __stdcall ThreadFun2(void *pvParam)
    {
    cout << " ThreadFun2 is start! " << endl;
    cout << " 切换 ThreadFun1 " << endl;
    Sleep(200);
    SwitchToThread();
    return true;
    }

  • 其实 SwitchToThread 的根本作用就是 CPU 资源让给其他的线程

  • Windows 并没有给出相应的内核 API 函数来挂起某一个进程,想要挂起进程就必须遍历进程中的所有运行中的线程并且将它们挂起,和挂起线程类似,只不过需要操作多个线程,示例如下:

    #include
    #include
    #include
    #include
    #include
    #include
    using namespace std;

    unsigned __stdcall ThreadFun1(void *pvParam);
    unsigned __stdcall ThreadFun2(void *pvParam);
    unsigned __stdcall ThreadFun3(void *pvParam);
    DWORD WINAPI ControlProThreads(DWORD ProcessId, BOOL Suspend);
    VOID WINAPI ErrorCodeTransformation(DWORD ErrorCode);

    int main(int argc, char *argv[])
    {
    // 创建 3 个挂起线程 MyThread1、MyThread2、MyThread3,挂起计数为 1
    DWORD ThreadId1 = NULL, ThreadId2 = NULL, ThreadId3 = NULL;
    HANDLE MyThread1 = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun1, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId1);
    HANDLE MyThread2 = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun2, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId2);
    HANDLE MyThread3 = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun3, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId3);

    // 将挂起计数 -1 变为 0,表示运行线程
    ResumeThread(MyThread1); ResumeThread(MyThread2); ResumeThread(MyThread3);
    
    // 获取当前进程的 ID 保存在 ProcessId 中
    HANDLE DupProcessHandle = NULL; DWORD ProcessId;
    DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &DupProcessHandle, 0, FALSE, DUPLICATE_SAME_ACCESS);
    ProcessId = GetProcessId(DupProcessHandle);
    
    // 用于挂起进程的函数,参数一为需要挂起的进程 ID,参数而表示是否为挂起操作,反之则为释放操作
    DWORD res = ControlProThreads(ProcessId, TRUE);
    
    // 若调用失败,打印错误信息
    if (res != TRUE) ErrorCodeTransformation(res);
    
    // 等待所有线程退出
    HANDLE Threads[3]; Threads[0] = MyThread1; Threads[1] = MyThread2; Threads[2] = MyThread3;
    WaitForMultipleObjects(3, Threads, TRUE, INFINITE);
    
    // 关闭句柄
    CloseHandle(MyThread1); CloseHandle(MyThread2); CloseHandle(MyThread3);
    return 0;

    }

    DWORD WINAPI ControlProThreads(DWORD ProcessId, BOOL Suspend)
    {
    // 定义函数错误代码
    DWORD LastError = NULL;

    // 通过 CreateToolhelp32Snapshot 函数获取当前系统所有线程的快照
    HANDLE ProcessHandle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, ProcessId);
    if (ProcessHandle == INVALID_HANDLE_VALUE)
    {
        // 函数调用失败,返回错误代码,方便打印出错误信息
        LastError = GetLastError();
        return LastError;
    }
    
    // 判断进程是否为当前进程,SkipMainThread 的作用是是否跳过主线程
    INT SkipMainThread = 0; HANDLE DupProcessHandle = NULL;
    DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &DupProcessHandle, 0, FALSE, DUPLICATE_SAME_ACCESS);
    DWORD IsThisProcess = GetProcessId(DupProcessHandle);
    
    // 如果传入的进程 ID 不是当前进程,则将 SkipMainThread 变为 1,表示不跳过主线程
    if (ProcessId != IsThisProcess) SkipMainThread = 1;
    
    THREADENTRY32 ThreadInfo = { sizeof(ThreadInfo) };
    BOOL fOk = Thread32First(ProcessHandle, &ThreadInfo);
    
    while (fOk)
    {
        // 将线程 ID 转换为线程句柄
        HANDLE ThreadHandle = OpenThread(THREAD_SUSPEND_RESUME, FALSE, ThreadInfo.th32ThreadID);
    // 是否为传入进程的子线程,且判断是挂起操作还是释放操作
    if (ThreadInfo.th32OwnerProcessID == ProcessId &amp;&amp; Suspend == TRUE)
    {
        // 如果是当前进程,就跳过主线程,SkipMainThread == 0 表示为主线程
        if (SkipMainThread != 0)
        {
            // 挂起线程
            DWORD count = SuspendThread(ThreadHandle);
            cout &lt;&lt; "[*] 线程号: " &lt;&lt; ThreadInfo.th32ThreadID &lt;&lt; " 挂起系数 + 1 " &lt;&lt; " 上一次挂起系数为: " &lt;&lt; count &lt;&lt; endl;
        }
        SkipMainThread++;
    }
    else if(ThreadInfo.th32OwnerProcessID == ProcessId &amp;&amp; Suspend == FALSE)
    {
        if (SkipMainThread != 0)
        {
            // 释放线程
            DWORD count = ResumeThread(ThreadHandle);
            cout &lt;&lt; "[-] 线程号: " &lt;&lt; ThreadInfo.th32ThreadID &lt;&lt; " 挂起系数 - 1 " &lt;&lt; " 上一次挂起系数为: " &lt;&lt; count &lt;&lt; endl;
        }
        SkipMainThread++;
    }
    fOk = Thread32Next(ProcessHandle, &amp;ThreadInfo);
    } // 关闭句柄 CloseHandle(ProcessHandle);

    }

    unsigned __stdcall ThreadFun1(void *pvParam)
    {
    cout << "ThreadFun1" << endl;
    // 睡眠 2000 毫秒,模拟线程正在执行工作
    Sleep(2000);
    return true;
    }
    unsigned __stdcall ThreadFun2(void *pvParam)
    {
    cout << "ThreadFun2" << endl;
    // 睡眠 2000 毫秒,模拟线程正在执行的工作
    Sleep(2000);
    return true;
    }
    unsigned __stdcall ThreadFun3(void *pvParam)
    {
    cout << "ThreadFun3" << endl;
    // 睡眠 2000 毫秒,模拟线程正在执行的工作
    Sleep(2000);
    return true;
    }

    // 如果返回错误,可调用此函数打印详细错误信息
    VOID WINAPI ErrorCodeTransformation(DWORD ErrorCode)
    {
    LPVOID lpMsgBuf; LPVOID lpDisplayBuf; DWORD dw = ErrorCode;
    // 将错误代码转换为错误信息
    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
    NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL
    );
    lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, (lstrlen((LPCTSTR)lpMsgBuf) + 40) * sizeof(TCHAR));
    StringCchPrintf((LPTSTR)lpDisplayBuf, LocalSize(lpDisplayBuf), TEXT("错误代码 %d : %s"), dw, lpMsgBuf);
    // 弹窗显示错误信息
    MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);
    LocalFree(lpMsgBuf); LocalFree(lpDisplayBuf); ExitProcess(dw);
    }

  • 值得注意的是,ControlProThreads 函数会判断传入的进程 ID 是否为当前进程 ID,如果是当前进程 则跳过主线程,否则主线程被挂起,所有的操作都会暂停,相当于一种特殊的死锁

  • 而 ErrorCodeTransformation 函数则是将错误代码转换为错误信息,方便查找出错误,在使用内核 API 进行编程时尤其需要注意这一点

  • 当然 ControlProThreads 运行起来也会有一定的风险,因为获取的快照之后可能会有新线程创建,也会有旧进程销毁;如下图所示,挂起了除了主线程外的当前进程的所有线程

  • 如果需要释放挂起的进程,只需要将 FALSE 传递给 ControlProThreads 的第二个参数即可

    // 用于挂起进程的函数,参数一为需要挂起的进程 ID,参数而表示是否为挂起操作,反之则为释放操作
    ControlProThreads(ProcessId, TRUE);
    Sleep(2000);
    cout << "\n 睡眠 2000 毫秒 \n" << endl;
    ControlProThreads(ProcessId, FALSE);
  • 这样就相当于暂停线程再运行线程,相应的假如多次挂起,则需要对应多次释放操作,线程内核计数变为 0 的时候就可以被 CUP 调度了

  • 想要获取进程和线程的时间信息,可以使用微软提供的 GetThreadTime 和 GetProcessTime 这两个函数

    #include
    #include
    #include
    using namespace std;

    unsigned __stdcall ThreadFun1(void *pvParam);

    int main(int argc, char *argv[])
    {
    // 创建进程
    TCHAR CmdLine[8] = TEXT("CMD.EXE");
    STARTUPINFO StartInfo = { sizeof(StartInfo) }; PROCESS_INFORMATION ProcessInfo = { 0 };
    CreateProcess(NULL, CmdLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &StartInfo, &ProcessInfo);
    WaitForSingleObject(ProcessInfo.hProcess, INFINITE);

    // 获取进程执行的时间
    FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;
    GetProcessTimes(ProcessInfo.hProcess, &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);
    cout << "hProcess 所用的内核时间为: " << ftKernelTime.dwLowDateTime / 10000 << " - hProcess 所用的用户时间为: " << ftUserTime.dwLowDateTime / 10000 << endl;
    
    // 创建线程
    DWORD ThreadId1 = NULL;
    HANDLE MyThread1 = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun1, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId1); ResumeThread(MyThread1);
    WaitForSingleObject(MyThread1, INFINITE);
    
    // 获取线程的执行时间
    GetThreadTimes(MyThread1, &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);
    cout << "MyThread1 所用的内核时间为: " << ftKernelTime.dwLowDateTime / 10000  << " - MyThread1 所用的用户时间为: " << ftUserTime.dwLowDateTime / 10000 << endl;
    
    // 关闭句柄
    CloseHandle(MyThread1);
    return 0;

    }

    unsigned __stdcall ThreadFun1(void *pvParam)
    {
    Sleep(100);
    return true;
    }

  • 运行如下所示,还是有一点不准确的

  • 要想获得更准确的运行时间可以这么办,利用 QueryPerformanceFrequency 配合 QueryPerformanceCounter 就可以达到更为精确的运行时间

    #include
    #include
    #include
    using namespace std;

    int main(int argc, char *argv[])
    {
    LARGE_INTEGER nFreq; LARGE_INTEGER nBeginTime; LARGE_INTEGER nEndTime; double time;
    QueryPerformanceFrequency(&nFreq);

    // 开始计时
    QueryPerformanceCounter(&nBeginTime);
    
    // 创建进程
    TCHAR CmdLine[8] = TEXT("CMD.EXE");
    STARTUPINFO StartInfo = { sizeof(StartInfo) }; PROCESS_INFORMATION ProcessInfo = { 0 };
    CreateProcess(NULL, CmdLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &StartInfo, &ProcessInfo);
    WaitForSingleObject(ProcessInfo.hProcess, INFINITE);
    
    // 结束及时
    QueryPerformanceCounter(&nEndTime);
    time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) / (double)nFreq.QuadPart;
    cout << "进程运行时间为: " << time << " 秒 " << endl;
    
    return 0;

    }

  • 相比 GetThreadTime 和 GetProcessTime 这两个函数,确实精确了很多,原因是由于计时的线程是非抢占式的,执行完以后才会执行其他线程