Windows程式設計 - 程序、模組、執行緒
Window程式設計 - 程序、模組、執行緒
程序、模組、執行緒
一 概念:
程序,簡單來說,就是一個執行中的程式,包含了:
a. 一個虛擬的記憶體空間 所有程式的程式碼和資料都在這片記憶體空間中。
b. 記憶體空間中排布了很多的模組
c. 至少有一個執行緒
在程序的虛擬記憶體中,一般會載入一個exe,很多的dll。他們都稱之為模組。
程序本身是一個綜合了各種資源的東西,是不能執行程式碼,能夠執行程式碼的是歸屬於程序的執行緒。
每一個執行緒都是一個獨立的執行單元:
1 每一個執行緒有自己的一塊堆疊。
2 每一個執行緒有自己的執行環境。
CPU在執行程式碼的時候,主要是依賴於一套暫存器:
通用暫存器:eax ebx ecx edx esi edi esp ebp
指令指標暫存器:eip 儲存著下一條要執行的指令
段暫存器:cs ss ds es fs gs
所有的執行緒都是作業系統統一去管理排程的,每一個執行緒都有自己的優先順序。根據優先順序決定先呼叫誰後呼叫誰。
執行緒發生切換,實際就是切換執行緒的執行環境,比如現在有A,B,C三個執行緒,此時執行緒A在執行,執行緒A的時間片用完了,就儲存A的執行環境,看B和C的優先順序誰高,假如是C高,那麼就把C的執行緒環境載入到CPU中。
二:基本操作:
程序 模組 執行緒的遍歷(非常重要的操作)
方法有很多 我們這裡使用的是建立快照的方式:CreateToolHelp32Snapshot
需要知道的幾點:
a. 程序是作業系統管理的,遍歷的時候,能夠遍歷出系統中的所有程序的資訊:程序名,路徑,程序ID
遍歷程序的用處:通常來說我們都是知道程序名,然後去找到ID(ID每一次程式執行的時候都是不一樣的),我們如果要操作程序,就需要使用OpenProcess函式得到它的控制代碼,OpenProcess這個函式,就是根據程序ID得到控制代碼的。
b. 模組是屬於某一個程序的,所以我們遍歷模組的時候,需要指定遍歷的是哪一個程序的模組。
能夠遍歷出模組的資訊為:模組名,模組的起始虛擬地址(載入基址)
遍歷模組的用處:a 可以知道一個程式都載入了哪些DLL,監測DLL注入 b 分析DLL中的PE檔案資訊,可以為我們分析一個程式提供依據。
c. 執行緒雖然也是屬於某一個程序的,但是其管理是作業系統統一管理的,所以我們遍歷的時候,也是遍歷出操作系統中的所有執行緒。需要自己去過濾然後,得到某一個程序的執行緒。
執行緒遍歷,能夠得到的資訊有:執行緒ID,所屬程序的ID。
遍歷執行緒的用處:可以得到程序中每一個執行緒的資訊。可以操作這些執行緒,比如掛起,終止等等。
HANDLE hProcessSnap; // 程序快照控制代碼
HANDLE hProcess; // 程序控制代碼
PROCESSENTRY32 stcPe32 = { 0 }; // 程序快照資訊
stcPe32.dwSize = sizeof(PROCESSENTRY32);
// 1. 建立一個程序相關的快照控制代碼
hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
return false;
// 2. 通過程序快照控制代碼獲取第一個程序資訊
if (!Process32First(hProcessSnap, &stcPe32))
{
CloseHandle(hProcessSnap);
return false;
}
// 3. 迴圈遍歷程序資訊
do {
// 3.2 獲取優先順序資訊
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE,
stcPe32.th32ProcessID);
if (hProcess)
{
GetPriorityClass(hProcess);//獲取程序優先順序
CloseHandle(hProcess); //關閉控制代碼
}
// 3.3 獲取程序的其他相關資訊
stcPe32.th32ProcessID;
stcPe32.cntThreads;
stcPe32.th32ParentProcessID;
} while (Process32Next(hProcessSnap, &stcPe32));
// 4. 關閉控制代碼退出函式
CloseHandle(hProcessSnap);
程序基本操作
建立:CreateProcess
開啟已經存在的程序:OpenProcess
終止:ExitProcess
強制結束:TerminateProcess
程序操作函式演示
#include <windows.h>
int main()
{
// 傳入引數: 必須初始化,使用者設定程序的啟動資訊
STARTUPINFOA StartupInfo{ sizeof(STARTUPINFOA) };
// 傳出引數: 用於接收建立的程序和執行緒的控制代碼及id
PROCESS_INFORMATION ProcessInfomation{};
// 可以使用函式CreateWindow 通過傳入的路徑建立一個程序
BOOL Result = CreateProcessA(
"C:\\Windows\\System32\\notepad.exe", // 指定用於建立程序的可執行檔案
NULL, // 命令列引數, 如果不寫可以寫NULL
NULL, // 表示為程序使用預設的安全屬性
NULL, // 表示為程序中主執行緒使用的安全屬性
FALSE, // 是否需要整合父程序的可繼承物件
NULL, // 建立程序的標誌位; 使用控制檯\除錯
NULL, // 使用到的環境變數, NULL 表示使用預設的
NULL, // 設定當前的工作路徑, 供相對路徑參考
&StartupInfo, // 提供程序的啟動資訊
&ProcessInfomation // 接受程序和執行緒的控制代碼和相應的id
);
// 如果程序建立成功,會返回心得程序和執行緒的控制代碼,為了防止控制代碼洩露
// 應該使用 CloseHandle 來關閉控制代碼
if (Result != FALSE)
{
CloseHandle(ProcessInfomation.hThread);// 關閉控制代碼
CloseHandle(ProcessInfomation.hProcess);// 關閉控制代碼
}
return 0;
}
#include <windows.h>
int main()
{
// 1. 可以使用FindWindow函式來查詢到視窗控制代碼
HWND hWnd = FindWindow(NULL, L"無標題 - 記事本");
// 2, 通過函式 GetWindowThreadProcessId 可以找到對應視窗的 PID
DWORD Pid = 0;
GetWindowThreadProcessId(hWnd, &Pid);
// 3. 使用獲取到的程序ID 以指定的群賢開啟目標程序
// 3.1 表示當前需要一個擁有什麼許可權的核心物件控制代碼
// 3.2 表示當前的控制代碼是否是一個可繼承的控制代碼, 需要配合繼承
// 3.3 一個Pid表示需要開啟的是哪一個核心物件
HANDLE Process = OpenProcess(PROCESS_TERMINATE, FALSE, Pid);
// 4. 傳入目標程序控制代碼, 執行相應的操作, 例如關閉它
TerminateProcess(Process, -1);
// 5. 操作完成之後為了避免控制代碼洩露, 需要關閉核心物件控制代碼
CloseHandle(Process);
// 6. 有的時候, 也可以使用這個函式結束當前的程序
ExitProcess(-1);
return 0;
}
程序間的通訊:
1 COPY_DATA訊息
2 郵槽
執行緒基本操作:
建立:CreateThread
開啟:OpenThread
掛起:SuspendThread
恢復:ResumeThread
終止:ExitThread
強制結束:TerminateThread
#include <iostream>
#include <process.h>
#include <windows.h>
/**
1. 什麼是執行緒?
執行緒是一個核心物件(儲存在核心中的結構體), 是CPU進行排程的基本單位,可以將
執行緒看作一段執行的程式碼. 一個執行緒最少由一個描述自身核心物件和對應的執行緒上下文
(一組CPU提供的暫存器 堆疊[引數\返回地址])構成
2. 執行緒的特點
一個程序中最少有一個執行緒, 可以呼叫函式建立多個執行緒. 執行緒之間實際上並不存在從屬關係.
但是通常情況下, 我們將執行主函式的執行緒成為主執行緒, 主執行緒的特點: 一旦主執行緒特推出,
當前程序下所有被建立的其他執行緒都會被退出
3. 執行緒的重點
執行緒的建立: CreateThread(win32) \ _beginthread
執行緒的結束: TerminateThread \ ExitThread
執行緒的傳參: 是在用指標進行引數的傳遞
*/
typedef struct _THREAD_PARAMTER
{
char cValue;
int nValue;
double fValue;
}THREAD_PARAMTER, *PTHREAD_PARAMTER;
// 提供一個被作為執行緒起始位置的回撥函式, 函式原型必須符合要求
DWORD WINAPI WorkerThread(LPVOID lpThreadParameter)
{
// 由於傳進來的是一個指標, 為了獲取指向內容,通常需要強轉
auto Paramer = (PTHREAD_PARAMTER)lpThreadParameter;
while (true)
{
printf("我是 WorkerThread [%c]\n", Paramer->cValue);
}
return 0;
}
int main()
{
// 建立一個結構體變數, 將結構體的地址作為引數進行傳遞, [更多的時候我們會申請一塊
// 堆空間, 用於儲存傳遞的引數, 防止當前執行緒結束, 區域性變數無法訪問]
THREAD_PARAMTER Paramter{'a', 100, 3.14};
// 建立一個以指定位置開始的執行緒, 如果建立成功返回執行緒控制代碼
HANDLE Thread = CreateThread(
NULL, // 使用預設的安全屬性
0, // 使用預設的棧大小, 通常是1M
WorkerThread, //執行緒的起始位置, 是一回調函式
&Paramter, // 提供給執行緒函式的引數
0, // 執行緒標誌, 例如可以是建立時掛起
NULL // 如果建立成功,則返回TID
);
while (true)
{
// 多個執行緒之間執行是沒有規律的, 執行緒有 CPU排程, 每個執行緒在執行大概
// 10 ms左右, 會切換到另一個執行緒, 所以兩個執行緒的輸出是隔開的
printf("我是 MainThread\n");
}
return 0;
}
#include <iostream>
#include <windows.h>
// 執行緒是一個可等待物件, 任何一個可等待物件都存在兩種狀態, 在特定的時候會觸發
// 相應的訊號, 例如執行緒在結束的時候, 會將核心物件置為有訊號狀態
// 通過等待函式, 通常是 WatiForSingleObject 可以等待一個可等待物件, 如果物件處於
// 有訊號狀態, 這個函式會直接返回, 否則會阻塞程式的執行
// 執行緒的回撥函式, 建立執行緒的時候被用作執行緒的起始位置
DWORD CALLBACK WorkerThread(LPVOID lpParamter)
{
// 作為一個被建立的執行緒, 當主執行緒[main所在的執行緒]退出的時候, 這些執行緒無論有沒有執行
// 完畢, 都會跟著退出
for (int i = 0; i < 10000; i++)
{
printf("WorkerThread: %d\n", i);
}
return 0;
}
int main()
{
// 在主執行緒中使用函式 CreateThread 建立一個其他執行緒, 並等待執行緒執行完畢後退出
HANDLE Thread = CreateThread(NULL, 0, WorkerThread, NULL, 0, NULL);
// 如果執行緒建立成功, 就等等待執行緒執行完畢, 否則直接退出, 函式的第一個引數是要等待的物件,
// 第二個引數是等待的時常, 只有兩種情況下函式會返回, 第一個是物件等待成功, 第二個是等待超時,
// 操過了等待的毫秒數, 物件還沒有訊號
if (Thread)
WaitForSingleObject(Thread, INFINITE);
return 0;
}
遍歷執行緒
#include <windows.h>
#include <iostream>
#include <TlHelp32.h>
int main()
{
// 1. 使用函式建立執行緒快照, 此時引數2沒有意義, 無論替那些什麼, 遍歷到的都是所有執行緒
HANDLE SnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
// 2. 建立一個結構體用於儲存遍歷到的所有執行緒資訊
THREADENTRY32 ThreadInfo{ sizeof(THREADENTRY32)};
// 3. 嘗試從快照中獲取到儲存的第一個執行緒的資訊
if (Thread32First(SnapShot, &ThreadInfo))
{
do {
// 4. 由於遍歷到的是所有的執行緒, 所以我們要判斷
if (ThreadInfo.th32OwnerProcessID == 7184)
{
// 開啟目標程序的每一個執行緒, 結束這個執行緒
HANDLE Thread = OpenThread(THREAD_TERMINATE, FALSE, ThreadInfo.th32ThreadID);
if (Thread)
{
TerminateThread(Thread, -1); // 結束執行緒
CloseHandle(Thread); // 關閉控制代碼
}
}
} while (Thread32Next(SnapShot, &ThreadInfo));
}
return 0;
}
掛起和恢復
#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
// 線上程核心物件對應的結構體中, 有一個掛起計數, 只有當掛起計數為 0 的時候, 執行緒才會被排程(執行)
// 可以使用 ResumeThread 讓掛起計數 -1, 使用 Suspend 函式讓掛起計數+1
// 查詢指定視窗對應的程序id
DWORD FindProcessId(LPCWSTR WindowsName)
{
HWND hWnd = FindWindow(NULL, WindowsName);
if (hWnd != NULL)
{
DWORD ProcessID = 0;
GetWindowThreadProcessId(hWnd, &ProcessID);
return ProcessID;
}
return -1;
}
int main()
{
// 1. 使用函式建立執行緒快照
HANDLE Snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
// 2. 建立一個結構體用於儲存遍歷到的所有執行緒資訊
THREADENTRY32 ThreadInfo{sizeof(THREADENTRY32)};
// 3. 嘗試從快照中獲取儲存的第一個執行緒資訊
if (Thread32First(Snapshot, &ThreadInfo))
{
DWORD Pid = 0;
Pid = FindProcessId(L"無標題.txt - 記事本");
do {
// 4.
if (ThreadInfo.th32OwnerProcessID == Pid)// 判斷PID是否是需要的PID
{
// 開啟目標程序的每一個執行緒, 嘗試將這個執行緒掛起
HANDLE Thread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, ThreadInfo.th32ThreadID);
if (Thread)
{
// SuspendThread(Thread);
SuspendThread(Thread); // 暫停執行緒
ResumeThread(Thread); // 掛起執行緒
CloseHandle(Thread); // 關閉控制代碼
}
}
} while (Thread32Next(Snapshot, &ThreadInfo));
}
return 0;
}
使用偽控制代碼
#include <iostream>
#include <windows.h>
//編寫函式列印指定執行緒的建立時間, 要求接受一個執行緒控制代碼
VOID GetThreadCtreateTime(HANDLE Thread)
{
// 建立結構體用於儲存獲取到的和執行緒相關的時間戳
FILETIME CreateTime{}, ExitTime{};
FILETIME UserTime{}, KernelTime{};
// 使用函式獲取到執行緒的建立時間
GetThreadTimes(Thread, &CreateTime, &ExitTime, &UserTime, &KernelTime);
// 將時間戳轉換為當前時區表示的時間
FILETIME LocalTime{};
FileTimeToLocalFileTime(&CreateTime, &LocalTime);
// 將時間戳轉換為時間結構體
SYSTEMTIME SystemTime{};
FileTimeToSystemTime(&LocalTime, &SystemTime);
// 輸出指定執行緒的建立時間
printf("%d 時 %d 分 %d 秒\n", SystemTime.wHour, SystemTime.wMinute, SystemTime.wSecond);
}
// 一個執行緒, 輸出引數指定的執行緒的建立時間
DWORD CALLBACK WorkerThread(LPVOID Paramter)
{
// 通過強制型別轉換獲取到傳入的執行緒控制代碼
HANDLE Thread = (HANDLE)Paramter;
// 在當前執行緒中得到的實際上是主執行緒的偽控制代碼, 也就是-2, -2永遠代表的都是當前的
// 執行緒,所以下面的函式, 輸出的就是當前執行緒的建立時間, 而不是主執行緒的建立時間
GetThreadCtreateTime(Thread);
return 0;
}
int main()
{
// 獲取當前執行緒的偽控制代碼, 計算當前執行緒的建立時間. 執行緒的偽控制代碼始終是-2, 程序是 -1
HANDLE MainThread = GetCurrentThread();
// 在主執行緒中首先列印主執行緒的建立時間
GetThreadCtreateTime(MainThread);
// 等待兩秒鐘, 建立一個心得執行緒, 將主執行緒的控制代碼傳入, 計算 執行緒的控制代碼
Sleep(2000);
HANDLE Thread = CreateThread(NULL, 0, WorkerThread, (LPVOID)MainThread, 0,NULL);
// 為確保執行緒執行完畢,應等待執行緒
WaitForSingleObject(Thread, INFINITE);
return 0;
}
偽控制代碼轉換
#include <iostream>
#include <windows.h>
// 編寫函式列印指定執行緒的建立時間,要求接受一個執行緒控制代碼
VOID GetThreadCreateTime(HANDLE Thread)
{
// 建立結構體用於儲存獲取到的和執行緒相關的時間戳
FILETIME CreateTime{}, ExitTime{};
FILETIME UserTime{}, KernelTime{};
// 使用函式獲取到執行緒的建立時間
GetThreadTimes(Thread, &CreateTime, &ExitTime, &UserTime, &KernelTime);
// 將時間戳轉換為當前時區表示的時間
FILETIME LocalTime{};
FileTimeToLocalFileTime(&CreateTime, &LocalTime);
// 將時間戳轉換為時間結構體
SYSTEMTIME SystemTime{};
FileTimeToSystemTime(&LocalTime, &SystemTime);
// 輸出指定執行緒的建立時間
printf("%d 時 %d 分 %d 秒\n", SystemTime.wHour, SystemTime.wMinute, SystemTime.wSecond);
}
// 一個執行緒,輸出引數指定的執行緒的建立時間
DWORD CALLBACK WorkerThread(LPVOID Paramater)
{
// 通過強制型別轉換獲取到傳入的執行緒控制代碼
HANDLE Thread = (HANDLE)Paramater;
// 在當前執行緒中得到的實際上是主執行緒的偽控制代碼,也就是 -2,-2永遠代表的都是當前的
// 執行緒,所以下面的函式,輸出的就是當前執行緒的建立時間,而不是主執行緒的建立時間
GetThreadCreateTime(Thread);
return 0;
}
int main()
{
// 獲取當前執行緒的偽控制代碼, 計算當前執行緒的建立時間. 執行緒偽控制代碼始終是-2, 程序是-1;
HANDLE MainThread = GetCurrentThread();
//在主執行緒中首先列印主執行緒的建立時間
GetThreadCreateTime(MainThread);
// 如果想要其他執行緒計算出主執行緒的建立時間, 絕對不能用偽控制代碼, 轉換方式如下
HANDLE TrueHandle{};
DuplicateHandle(
GetCurrentProcess(), // 需要轉換的控制代碼在哪兒
MainThread, // 要轉換的控制代碼是誰
GetCurrentProcess(), // 將控制代碼拷貝到哪個程序的控制代碼表
&TrueHandle, // 轉換後的控制代碼儲存到哪裡
0,FALSE,DUPLICATE_SAME_ACCESS);
// 等待兩秒鐘, 建立一個新的執行緒, 將主執行緒的控制代碼傳入, 計算執行緒的建立時間
Sleep(2000);
HANDLE Thread = CreateThread(NULL, 0, WorkerThread, (LPVOID)TrueHandle, 0, NULL);
// 為了確保執行緒執行完畢, 應該等待執行緒
WaitForSingleObject(Thread, INFINITE);
return 0;
}