1.句柄

句柄就是个数字,⼀般和当前系统下的整数的位数⼀样,⽐如32bit系统下就是4个字节。

这个数字是⼀个对象的唯⼀标示,和对象⼀⼀对应

这个对象可以是⼀个块内存,⼀个资源,或者⼀个服务的context(如 socket,thread)等等

指针其实也是⼀种”句柄”,只是由于指针同时拥有更特殊的含义——实实在在地对应内存⾥地⼀个地址

在进程的地址空间中设⼀张表,表⾥头专⻔保存⼀些编号和由这个编号对应⼀个地址,⽽由那个地址去引⽤实际的对象,这个编号跟那个地址在数值上没有任何规律性的联系,纯粹是个映射⽽已。

在windows这个编号就是“句柄”

为什么不用首地址呢?

  • 暴露了内核对象本身,使得程序(⽽不是操作系统内核)也可以任意地修改对象地内部状态,这显然是操作系统内核所不允许的
  • 操作系统有定期整理内存的责任,如果⼀些内存整理过⼀次后,对象被搬⾛了怎么办?

看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <Windows.h> 
#include <stdio.h>
#include <string.h>
#include <iostream>
#include<tchar.h>
using namespace std;
int main(int argc, char* argv[])
{

TCHAR szPath[MAX_PATH] = { 0 };
//根据窗⼝名获取记事本窗⼝句柄
HWND hw = FindWindow(L"notepad", NULL);
printf("%x", hw);//可以打印窗⼝句柄看看是个什么值
RECT rect;
GetWindowRect(hw, &rect);//有了窗⼝句柄我们⾃然能对他进⾏⼀些操作了,⽐如获取窗⼝宽与⾼
int w = rect.right - rect.left, h = rect.bottom - rect.top;
cout << "宽:" << w << " " << "高:" << h << endl;
GetModuleFileName(NULL, szPath, MAX_PATH);//同理可以通过模块句柄获取当前⽬录下的路径,在这个API中填NULL,他即可获取当前进程中的模块句柄
_tprintf(TEXT("%s\n"), szPath);
return 0;
}
image-20230511202557027

所以说,有了某个进程的句柄,就能对之进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <Windows.h> 
#include <stdio.h>
#include <string.h>
#include <iostream>
#include<tchar.h>
using namespace std;
int main(int argc, char* argv[])
{

DWORD dwThreadId;
//根据窗⼝名获取记事本窗⼝句柄
HWND hw = FindWindow(L"notepad",NULL);
GetWindowThreadProcessId(hw, &dwThreadId);//根据窗⼝句柄发现进程的pid
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false,
dwThreadId);//这⾥是进程的句柄!(重要)
printf("%x\n", hProcess);
system("pause");
return 0;
}

image-20230511202919646

句柄表存放在ring0空间,⽽句柄的存在⽬的就是为了避免应⽤层直接修改内核都对象。

2.和线程相关的API函数

创建进程

1
2
3
4
5
6
7
8
9
10
11
12
13
//CreateProcess定义:
BOOL CreateProcess(
LPCWSTR pszImageName, //可执⾏⽂件的名称
LPCWSTR pszCmdLine, //传递给模块的参数
LPSECURITY_ATTRIBUTES psaProcess, //进程安全性,NULL为默认
LPSECURITY_ATTRIBUTES psaThread, //线程安全性,NULL为默认
BOOL fInheritHandles, //指定了当前进程中的可继承句柄是否可被新进程继承
DWORD fdwCreate, //指定了新进程的优先级及其他创建标志
LPVOID pvEnvironment, //指定了新进程使⽤的环境变量
LPWSTR pszCurDir, //新进程使⽤的当前⽬录
LPSTARTUPINFOW psiStartInfo, //该结构体指定了新进程中主窗⼝位置⼤⼩和标准句柄等
LPPROCESS_INFORMATION pProcInfo //[out]返回进程的标志信息,如ID号,句柄等
);

CreateProcess函数的最后两个参数 结构体指针 LPSTARTUPINFOW 指向的结构体和 LPPROCESS_INFORMATION结构体。LPPROCESS_INFORMATION指向的结构体用来接收新创建的进程的相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//STARTUPINFO定义
typedef struct _STARTUPINFO {
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
1
2
3
4
5
6
7
//PROCESS_INFORMATION定义
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;

使⽤API函数GetStartipInfo来获取⽗进程创建⾃⼰时使⽤的STARTUPINFO结构。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <Windows.h>
int main()
{
LPCWSTR pszImageName = L"C:\\Windows\\System32\\notepad.exe";
wchar_t cmdLine[] = L" C:\\Users\ASUS\\Desktop\\123\\ttt.txt";
//注意前两个参数的⽤法,第⼀个参数LPCWSTR类型,表示进程的可执⾏⽂件名字
//第⼆个参数表示命令⾏参数,引号中*有空格*(注意)将两个参数隔开
//可以不适⽤第⼀个参数,只使⽤第⼆个参数,则第⼆个参数表示为(中间空格隔开):
//wchar_t cmdLine[]=L"C:\\Windows\\System32\\notepad.exe
C:\\Users\\z\\Desktop\\ttt.txt"
LPWSTR pszCmdLine = cmdLine;
STARTUPINFOW si = { sizeof(si) };
si.wShowWindow = TRUE;
PROCESS_INFORMATION pi;
BOOL bCr = CreateProcess(
pszImageName,
pszCmdLine,
NULL,
NULL,
false,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi);
if (bCr)
{
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE,
pi.dwThreadId);
ResumeThread(hThread);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
std::cout << "新进程的ID号是" << pi.dwProcessId << std::endl;
}
}

image-20230511211437296

遍历进程

CreateToolhelp32Snapshot:

CreateToolhelp32Snapshot给当前系统内执⾏的进程拍快照,Process32First函数和Process32Next函数遍历快照表中记录的列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
//相关API定义
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
BOOL WINAPI Process32First(
HANDLE hSnapshot,
LPPROCESSENTRY32 lppe //[out]获取信息的结构体
);
BOOL WINAPI Process32Next(
HANDLE hSnapshot,
LPPROCESSENTRY32 lppe //[out]获取信息的结构体
);

需要重点关注的是Process32First和Process32Next的第⼆个参数,⽤来获取相关信息的结构体,PROCESSENTRY32结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct tagPROCESSENTRY32 {
DWORD dwSize; //该结构体的⼤⼩
DWORD cntUsage;
DWORD th32ProcessID;
DWORD th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
TCHAR szExeFile[MAX_PATH];
DWORD th32MemoryBase;
DWORD th32AccessKey;
} PROCESSENTRY32;
typedef PROCESSENTRY32* PPROCESSENTRY32;
typedef PROCESSENTRY32* LPPROCESSENTRY32;

使用该结构体前第一个参数要先初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <Windows.h>
#include <tlhelp32.h> //快照头⽂件
int main()
{
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
//给系统内的所有进程拍⼀个快照
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hProcessSnap == INVALID_HANDLE_VALUE)
{
std::cout << "快照失败!" << std::endl;
return -1;
}
//遍历进程快照,显示每个进程的信息
BOOL bMore = ::Process32First(hProcessSnap, &pe32);
while (bMore)
{
std::cout << "进程名称:" << pe32.szExeFile << std::endl;
std::cout << "进程ID号:" << pe32.th32ProcessID << std::endl;
std::cout << std::endl;
bMore = ::Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
return 0;
}
image-20230511211801782

当然快照不仅仅可以用来获取系统进程信息,还可以获取指定进程中的堆,模块等.

详细还得看文档

终止进程

除了进程中所有线程结束或主线程的⼊⼝函数返回之外,我们还可以通过调⽤函数强⾏终⽌进程,ExitProcess****终⽌当前进程,TerminateProcess**可以终⽌其他进程。

1
2
VOID ExitProcess( UINT uExitCode);
BOOL TerminateProcess( HANDLE hProcess, DWORD uExitCode);

可以很明显的看到两个函数都有⼀个参数uExitCode,该参数⽤来重置退出代码的值,进程中所有线程在没有结束时Exit Code的值都是STILL_ACTIVE,进程结束,进程中的所有线程的Exit Code的值替换成该参数。该值可以通过GetExitCodeThread获取。

不建议使⽤这两个函数终⽌进程,这两个函数结束进程的时候可能导致内存泄露。

3.线程操作

Win32线程控制只有是围绕线程这⼀内核对象的创建、挂起、恢复、终结以及通信等操作,这些操作都依赖于Win32操作系统提供的⼀组API和具体编译器的C运⾏时库函数。

_beginthread

  • _beginthread(函数名,栈大小,参数指针)
  • Win32 函数中提供了多线程的函数,包括创建线程,管理线程,终止线程,线程同步等接口

线程函数=》线程开始执行的函数

DWORD WINAPI ThreadFunc (LPVOID lpvThreadParm );【 PVIOD 一个普通指针类型】

线程创建

1
2
3
4
5
6
7
HANDLE CreateThread (
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId );

第⼀个参数lpThreadAtt,是⼀个指向SECURITY- ATTRIBUTES结构的指针,该结构制定了线程的安全属性,缺省为 NULL。

第⼆个参数dwStackSize,是栈的⼤⼩,⼀般设置为0。

第三个参数lpFun是新线程开始执⾏时,线程函数的⼊⼝地址。它必须是将要被新线程执行的函数地址,不能为NULL。

第四个参数lpParameter,是线程函数定义的参数。可以通过这个参数传送值,包括指针或者NULL

第五个参数dwCreationFlags,控制线程创建的附加标志,可以设置两种值。

  • 0表示线程在被创建后就会⽴即开始执⾏;
  • 如果该参数为CREATE_SUSPENDED,则系统产⽣线程后,该线程处于挂起状态,并不⻢上执⾏,直⾄函数ResumeThread被调⽤;

第六个参数lpThreadID,为指向32位变量的指针,该参数接受所创建线程的ID号。如果创建成功则返回线程的ID,否则返回NULL。

CreateThread不会执⾏C运⾏时数据块, 因此在C运⾏时库的应⽤程序中,不能使⽤CreateThread创建线程,微软提供了另外的创建线程的⽅法:创建线程⽤process.h头⽂件中声明的C执⾏时期链接库函数_beginthread。

1
2
3
4
5
hThread = _beginthread (
void( __cdecl *start_address )( void * ),
unsigned stack_size, void *arglist) ;
线程函数的语法:
void __cdecl ThreadProc (void * pParam) ;

在windows平台下可以通过windows的线程库来实现多线程编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "stdio.h"
#include <windows.h>
#include <process.h>
#include <iostream>
#include <fstream>
using namespace std;
//使⽤_beginthread函数创建线程的例⼦。
void ThreadFunc1(PVOID param)
{
while (1)
{
Sleep(1000);
cout << " This is ThreadFunc1 " << endl;
}
}
void ThreadFunc2(PVOID param)
{
while (1)
{
Sleep(1000);
cout << " This is ThreadFunc2 " << endl;
}
}
//可以多次执⾏本程序,查看两个线程函数执⾏的顺序。
int main()
{
int i = 0;
_beginthread(ThreadFunc1, 0, NULL);
_beginthread(ThreadFunc2, 0, NULL);
Sleep(5000);
cout << "end" << endl;
return 0;
}
image-20230511215417072 image-20230511215502138

这里可以看到我们开了2个线程,两个进程同时在我们程序中运行,所以一般要优化程序运行速度就会开多个线程去跑这个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//创建线程
//返回值:线程句柄
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
//挂起线程:
DWORD WINAPI SuspendThread(
_In_ HANDLE hThread // 线程句柄
);
//恢复线程执⾏
DWORD WINAPI ResumeThread(
_In_ HANDLE hThread // 线程句柄
);
//内核对象: 如线程,当线程处于运⾏时,是未通知状态,当线程处于停⽌状态时,是已通知状态
//满⾜两个参数任何⼀个都可以继续往下执⾏ timeOut:-1 永远等待
//等待线程执⾏完成 返回值 : WAIT_OBJECT_0(0) :线程已通知 WAIT_TIMEOUT(0x102)
:线程超时
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
DWORD WINAPI WaitForMulipleObjects(_In_ DWORD nCount,//对象个数
_In_ const HANDLE *lpHandles,//存放多个内核对象的句柄的数组 的指针;
_In_ BOOL bWaitAll,//TRUE 为等待全部对象为已通知,FLASE 为只要有⼀个
变为已通知
_In_ DWORD dwMilliseconds//等待时间
);
//退出线程
DWORD WINAPI ExitThread(DWORD exitCode); //线程内部使⽤
//return a //正常返回 a为退出码
DWORD WINAPI TerminateThread(_In_ HANDLE hThread, _In_ DWORD
exitCode);//线程外部使⽤,直接终⽌线程,并保留线程堆栈不销毁
//获取线程退出码
BOOL GetExitCodeThread(_In_ HANDLE hThread, _Out_ DWORD exitCode)//配合
WaitForSingleObject 使⽤/
// 获取挂起线程内部信息
DWORD WINAPI SuspendThread(_In_ HANDLE hhandle);
CONTEXT context;//设置要获取的类型
context.ContextFlage = CONTEXT_CONTROL;//有多种
//获取
BOOL GetThreadContext(_In_ HANDLE hThread, _In_ CONTEXT* context);
//设置
CONTEXTSetThreadContext(_In_ HANDLE hThread, _In_ CONTEXT* context);
//临界区
//创建
CRITICAL_SECTION cs;
//初始化
VOID WINAPI InitializeCriticalSection(&cs);
//在线程中使⽤
//获取
VOID WINAPI EnterCriticalScetion(&cs);
//释放
VOID WINAPI LeaveCriticalSection(&cs);
//删除
VOID WINAPI DeleteCriticalSection(cs);
//使⽤时可以为每⼀个全局变量单独创建⼀个锁,避免多个线程使⽤不同的有交叉的多个全局变量
时造成死锁
DWORD a; DWORD b; DWORD c;
CRITICAL_SECTION ka; CRITICAL_SECTION kb; CRITICAL_SECTION kc;
//互斥体:可以跨进程进⾏线程控制 互斥体是内核对象
//创建 在同⼀个进程内使⽤可以不提供Nmae
HANDLE gMutex = CreateMutex(nullptr, FALSE, "name");
//使⽤
HANDLE gMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, "name");
WaitForSingleObject(HANDLE g_Mutex, DWORD timeOut);//在互斥体没有被释放之前,
只能等待,不能接着往下
//释放
ReleaseMutex(HANDLE dMutex);

以上是大致的线程操作。

互斥体

互斥体一般用于防止程序双开,我们这边看一个堵塞例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "stdio.h"
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
//创建⼀个互斥体
HANDLE g_hMutex = CreateMutex(0, FALSE, TEXT("Z")); //TRUE 创建出来⽆信
号 FALSE 创建出来有信号
//获取互斥体,等待令牌
WaitForSingleObject(g_hMutex, INFINITE);
for (int i = 0; i < 10; i++)
{
Sleep(1000);
printf("A进程的X线程:%d \n", i);
}
//释放令牌
ReleaseMutex(g_hMutex);
return 0;
}

可以使用 互斥对象 来保护共享资源,防止多个线程或进程同时访问。 每个线程必须等待互斥体的所有权,然后才能执行访问共享资源的代码。 例如,如果多个线程共享对数据库的访问,则线程可以使用互斥对象一次只允许一个线程写入数据库.

这⾥两个例⼦是不可以同时进⾏打印的,因为他们有互斥体,运⾏会发现,如果没有得到令牌,程序是会堵塞住的,直到获取令牌。