1. 程式人生 > 其它 >Windows程式設計 - 程序、模組、執行緒

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;
}