目录
1、字符
(1)字符编码
1、原始的ASCII表: 0—7F(0—127)
2、扩展的ASCII表(ANSI编码)
0—FF(0—255)
00-7F:原始的ASCII表
80-FF:一些不常用的符号
GB2312或GB2312-80
利用80-FF这个范围,每2个字节编码一个汉字
中国 D6 D0 B9 FA
1. 这些使用多个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码
2. 在简体中文Windows操作系统中,ANSI 编码代表 GB2312编码
3. 在繁体中文Windows操作系统中,ANSI编码代表Big5
4. 在日文Windows操作系统中,ANSI 编码代表 JIS 编码
3、Unicode编码
编码范围:0—0x10FFFF
存在问题:只规定了符号的二进制代码,没有规定代码如何存储(有的字符占2个字节,有的占3个字节)
4、如何存储Unicode
UTF-16 / UTF-8 是Unicode的实现方式(存储方式)
UTF-16
以16位无符号整数为单位 (以2个字节对齐)
注意:
1. 是16位为一个单位,不是一个字符就只有16位
2. 要看字符的Unicode编码处于什么范围,字符可能占2字节,也可能占4字节
3. Unicode 编码一般指的就是 UTF-16
缺点:
1. 有些字符一个字节就能表示,比如:字母,使用 UTF-16却要使用2个字节
2. 影响网络传输效率
UTF-8: 变长编码,以1/2/3/4字节为单位
UTF-8 的 xxxx 部分对应的是 UTF-16 的编码
Unicode编码 / UTF-16(16进制) | UTF-8字节流(二进制) |
000000-00007F | 0xxxxxxx |
000080-0007FF | 110xxxxx 0xxxxxxx |
000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
BOM(Byte Order Mark):表明文件的存储格式,位于文件开头
UTF-8 | EF BB BF |
UTF-16LE(小端) | FF FE |
UTF-16BE(大端) | FE FF |
字符:A中
// 用 UE打开 --> 使用16进制编辑
UTF-16: 41 00 2D 4E (小端存储)
01000001 00000000 00101101 01001110
'A' 的编码范围: 00-7F
'中' 的编码范围: 0800-FFFF
转换成 UTF-8编码
UTF-8: 41 E4 B8 AD
01000001 1110(0100) 10(111000) 10(101101)
(0100) (111000) (101101) = 01001110 00101101 = 4E 2D
(2)C语言的宽字符
1、字符串在内存中的值
char a[] = "中国";
D6 D0 B9 FA 00 // 拓展的 ASCII,占 5字节(包含 '/0')
wchar_t b[] = L"中国"; // 以 UTF-16存储,不写 L采用项目默认编码
2D 4E FD 56 00 00 // Unicode编码,占 6字节(包含 '/0')
2、在控制台打印宽字符
#include<locale.h>
setlocale(LC_ALL,""); // 使用控制台默认的编码
wchar_t a[] = L"中国";
wprintf(L"%s\n",a);
3、字符串长度
char a[] = "abcde"; // 占6字节内存
wchar_t b[] = L"abcde"; // 占12字节
strlen(a); // 获取字符长度,不包含00,返回5
wcslen(b); // 获取字符长度,不包含00 00,返回5
// 分别用于 ASCII字符串和 Unicode的字符串,不同的字符串要使用对应的函数
// 如果强制转型,使用错误的函数成功编译,不能得到正确的结果
4、常用函数
char wchar_t // 多字节字符类型 宽字符类型
printf wprintf // 打印到控制台函数
strlen wcslen // 获取长度
strcpy wcscpy // 字符串复制
strcat wcscat // 字符串拼接
strcmp wcscmp // 字符串比较
strstr wcsstr // 字符串查找
(3)API中的宽字符
1、Win32概念
主要存放在 C:\Windows\system32 ( 存放64位的 dll ) 和 C:\Windows\SysWow64 (32位)下面的所有的 dll
Kernel32.dll | 最核心的功能模块,如:内存管理、进程/线程相关的函数 |
User32.dll | Windows用户界面相关应用程序接口,如:创建窗口、发送消息 |
GDI32.dll | 全称Graphical Device Interface(图形设备接口),包含:用于画图和显示文本的函数 |
2、Win32的数据类型
int MessageBox(
HWND hWnd, // handle to owner window
LPCTSTR IpText, // text in message box
LPCTSTR IpCaption, // message box title
UINT tType // message box style
);
// LPCTSTR --> LPCSTR --> CONST CHAR * --> const char *
// UINT --> unsigned int
常用数据类型
汇编:
byte BYTE PBYTE
word WORD PWORD
dword DWORD PDWORD
C语言:
char CHAR PCHAR
unsigned char UCHAR PUCHAR
short SHORT PSHORT
unsigned short USHORT PUSHORT
int INT PINT
unsigned int UINT PUINT
C++语言:
bool BOOL
3、在Win32中使用字符串
字符类型:
CHAR szStr[] = "中国"; // ASCII
WCHAR swzStr[] = L"中国"; // Unicode
TCHAR stzStr[] = TEXT("中国"); // TCHAR是一个宏,根据项目的字符编码 自动转换字符的编码
字符串指针:
PSTR pszStr = "中国"; // char*
PWSTR pwszStr = L"中国"; // wchat_t*
PTSTR ptszStr = TEXT("中国"); // PTSTR 是一个宏,有利于跨平台
// 建议使用 TCHAR和 PTSTR
4、第一个Win32 API的使用
int main(){
CHAR szTitle[] = "标题";
CHAR szContent[] = "欢迎大家来的Win32 API世界";
MessageBoxA(0, szContent, szTitle, MB_OK); // ASCII 版本
WCHAR swzTitle[] = L"标题";
WCHAR swzContent[] = L"欢迎大家来的Win32 API世界";
MessageBoxW(0, swzContent, swzTitle, MB_OK); // Unicode 版本
TCHAR stzTitle[] = TEXT("标题");
TCHAR stzContent[] = TEXT("欢迎大家来的Win32 API世界");
MessageBox(0, stzContent, stzTitle, MB_YESNO); // 自动识别项目编码,但是 windows内核是 Unicode编码
// MessageBox 也是一个宏,能做到自动识别
return 0;
}
2、进程
(1)进程的创建过程
1、概念:进程为程序提供所需的资源(代码 / 数据),线程使用资源
2、进程内存空间的地址划分
- 每个字节的内存空间,都进行了编址
- 进程可以理解为:内存上的空间,线程负责使用空间内的资源
- 所有进程共用高2GB 内核的内存空间
分区 | x86 32位Windows |
空指针赋值区(64KB) | 0x00000000 - 0x0000FFFF(从未使用) |
用户模式区 | 0x00010000 - 0x7FFEFFFF |
64KB禁入区 | 0x7FFF0000 - 0x7FFFFFFF |
内核 | 0x80000000 - 0xFFFFFFFF(所有进程共用内核高2GB的内存空间) |
- 进程空间内有许多模块文件(dll),每个模块都是一个可执行文件,遵守相同的格式,即 PE结构
3、进程的创建过程:任何进程都是由别的进程创建的,进程创建时总会创建一个线程,CreateProcess( )
1. 映射EXE文件 // 将 A.exe载入进程空间
2. 创建进程内核对象EPROCESS,每个进程都有自己的 EPROCESS对象
3. 映射系统 DLL(ntdll.dll)
4. 创建线程内核对象 ETHREAD
5. 系统启动线程:
// 将A进程要执行的 DLL换成B进程的,最终执行的是B
映射DLL(ntdll.LdrlnitializeThunk) // 将所有需要使用的 dll,载入进程用户空间
线程开始执行
// 创建进程,会创建一个 EPROCESS 结构体
// 创建线程,会创建一个 ETHREAD 结构体
(2)创建进程
BOOL CreateProcess (
LPCTSTR lpApplicationName, // 对象名称,完整的路径名
LPTSTR lpCommandLine, // 命令行参数,可以不使用为NULL
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 创建的进程是否允许被继承
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 同时创建的线程是否允许被继承
BOOL bInheritHandles, // 是否要继承父进程的句柄表
DWORD dwCreationFlags, // CREATE_SUSPENDED
LPVOID lpEnvironment, // 环境块
LPCTSTR lpCurrentDirectory, // 当前进程的工作目录
LPSTARTUPINFO lpStartupInfo, // 新窗口如何显示,STARTUPINFO结构存储启动状态,IN参数
LPPROCESS_INFORMATION lpProcessInformation // PROCESS_INFORMATION结构,OUT参数
);
1、命令行
- www.4399.com 就是命令行的参数
![使用cd命令打开浏览器](https://i-blog.csdnimg.cn/blog_migrate/56ad0946126cd7a1123b0a98c24f810e.png)
2、父进程为子进程填充STARTUPINFO结构(存储启动状态信息)
- 如果进程的 STARTUPINFO结构信息出现异常,说明进程处于被调试状态(由调试器启动),不是从桌面双击启动(反调试)
3、代码
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
using namespace std;
/*
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess; // 进程句柄
HANDLE hThread; // 线程句柄 (创建进程一定会同时创建一个线程,所有该结构体包含线程的信息)
DWORD dwProcessId; // 进程id
DWORD dwThreadsId; // 线程id
} PROCESS_INFORMATION, *PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
*/
/*
typedef struct _STARTUPINFOA {
DWORD cb; // 指定该结构的大小
LPSTR IpReserved; // 保留,曾为 NULL
LPSTR IpDesktop; // 指定一个字符串,包括该进程的桌面名或窗口位置名
LPSTR IpTitle; // 指定控制台进程创建的新控制台窗口标题
DWORD dwX; // 指定新窗口左上角 x和 y偏移量(以像素为单位)
DWORD dwY;
DWORD dwXSize; // 指定新窗口的宽度和高度
DWORD dwYSize;
DWORD dwXCountChars; // 指定新窗口的屏幕缓冲区的宽度和高度
DWORD dwYCountChars;
DWORD dwFillAttribute; // 指定新窗口的初始文字和背景颜色
DWORD dwFlags; // 创建窗口标志
WORD wShowWindow; // 新窗口的显示状态
WORD cbReserved2; // 保留,必须置为0
LPBYTE IpReserved2; // 保留,必须置为 NULL
HANDLE hStdInput; // 指定一个句柄,该句柄用作进程的标准输入句柄
HANDLE hStdOutput; // 指定一个句柄,该句柄用作进程的标准输出句柄
HANDLE hStdError; // 指定一个句柄,该句柄用作进程的标准错误句柄
} STARTUPINFOA, *LPSTARTUPINFOA;
*/
// 输出 STARTUPINFO结构体信息
void AntiDebug() {
STARTUPINFO si;
GetStartupInfo(&si); // 将父进程给子进程填写的启动信息存入结构体 si中(子进程就是要创建的进程)
// 打印获取到的信息
printf("%x %x %x %x %x %x %x %x\n", si.dwX, si.dwY,
si.dwXCountChars, si.dwYCountChars, si.dwFillAttribute,
si.dwXSize, si.dwYSize, si.dwFlags);
}
// 创建子进程
BOOL CreatChildProcess(PTCHAR szChildProcessName, PTCHAR szComandLine) {
STARTUPINFO si; // 在堆栈中创建这两个结构体
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi)); // 结构体内存置 0
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si); // cb 成员,记录结构体大小
// 创建子进程,返回成功或失败
if (!CreateProcess(
szChildProcessName, // 对象名称,要创建的进程的名字
szComandLine, // 命令行
NULL, // 不继承进程句柄
NULL, // 不继承线程句柄
FALSE, // 不继承句柄,父进程句柄表与子进程无关
0, // 没有创建标志
NULL, // 使用父进程环境变量
NULL, // 使用父进程目录作为当前目录,可以自己设置目录
&si, // STARTUPINFOA结构体详细信息,IN参数,需要传入的参数
&pi // Process_INFORMATION结构体进程信息,OUT参数,传出函数执行结果
)) {
printf("错误代码:%d \n", GetLastError());
return FALSE;
}
// 释放句柄 (无效关闭,无法关闭创建的进程)
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return true;
}
int main() {
TCHAR szApplicationName[] = TEXT("C:\\Edge\\Application\\msedge.exe");
TCHAR szCmdLine[] = TEXT("\"C:\\Edge\\Application\\msedge.exe\" \"https://www.huya.com/11342396\"");
CreatChildProcess(szApplicationName, NULL);
// AntiDebug();
getchar();
return 0;
}
(3)句柄表
1、内核对象
- 进程、线程、文件、互斥体、事件等在内核有一个对应的结构体,这些结构体由内核负责管理,称为内核对象;
- CloseHandle 函数可以关闭的对象,全是内核对象
- 如:Access token、Communications device、Console input、Console screen buffer、Event、File、File mapping
- Job、Mailslot、Mutex、Named pipe、Process、Semaphore、Socket、Thread
2、管理内核对象
- 创建内核对象,会在内核层生成对应的结构体,同时会返回结构体在内核层的地址( 返回给句柄表,不会返回给用户层 )
- 访问内核时,如果访问了错误的内存地址,不会返回内存不可访问错误,而是导致蓝屏
- 为了避免访问错误的内存地址,应用层无法直接看到内核结构体的地址,也不能直接访问内核结构体
- 只能通过句柄表访问
3、每个进程有一个句柄表
- 不是所有内核对象都拥有句柄表,只有进程的内核对象才拥有
- 句柄表存放了,内核结构体的内存地址
- 句柄表隔离了用户层和内核层,让用户无法直接操作内核
4、多进程共享一个内核对象(方式一)
- 所有进程共用内核层,所有内核对象可以跨进程共享
- 句柄表是每个进程私有的一张表,句柄值在本进程内才有意义
- 内核对象A使用计数为2,CloseHandle 只是将内核对象的使用计数减1,并不是关闭;当使用计数为0,内核对象会被关闭
- 结束一个线程句柄不会结束相关联的线程,要关闭线程内核对象,既要结束线程,也要让使用计数为0
- 只要线程不结束,进程就不会结束;
5、句柄是否可以被继承(共享内核方式二)
- 创建的内核对象中一定包含安全描述符(LPSECURITY_ATTRIBUTES 结构体)
- 如:CreatEvent 函数的第一个参数就是安全描述符,所以 Event 是一个内核对象
- 句柄表表项(3列):当前内核对象继承标志(是否可被继承)、索引、内核结构体地址
- 下图,父进程中2、4项可被子进程继承,子进程获取父进程句柄表,其中子进程可直接使用父进程的句柄值;
// 函数原型
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全描述符
BOOL bManualReset,
BOOL bInitialState,
LPCWSTR lpName
);
// 安全描述符
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; // 结构体大小
LPVOID lpSecurityDescriptor; // 指针
BOOL bInheritHandle; // 当前内核对象,是否允许继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
int main(){
SECURITY_ATTRIBUTES sa;
ZeroMemory(&sa,sizeof(SECURITY_ATTRIBUTES));
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
CreateEvent(NULL, FALSE, FALSE); // 该内核对象不能被继承,句柄表的标志为 0
CreateEvent(sa, FALSE, FALSE); // 允许被继承,句柄表的标志位 1
}
6、句柄是否允许被继承
- CreateProcess 函数的第 5个参数,为 Flase 时,子进程不继承父进程的句柄表;
- 为 True 时,子进程将继承父进程句柄表中能被继承的表项
BOOL CreateProcess (
LPCTSTR lpApplicationName, // 对象名称,完整的路径名
LPTSTR lpCommandLine, // 命令行参数,可以不使用为NULL
// 安全描述符
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 创建的进程是否允许被继承
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 同时创建的线程是否允许被继承
BOOL bInheritHandles, // 是否要继承父进程的句柄表
DWORD dwCreationFlags, // CREATE_SUSPENDED
LPVOID lpEnvironment, // 环境块
LPCTSTR lpCurrentDirectory, // 当前进程的工作目录
LPSTARTUPINFO lpStartupInfo, // 新窗口如何显示,STARTUPINFO结构存储启动状态,IN参数
LPPROCESS_INFORMATION lpProcessInformation // PROCESS_INFORMATION结构,OUT参数
);
(4)进程相关API
1、ID与句柄
- 句柄:当前进程句柄表的索引,称为句柄
- 全局句柄表 (操作系统所有):包含所有正在运行中的进程和线程;
- 进程ID / 线程ID:全局句柄表的一个索引;
BOOL TerminateProcess(
HANDLE hProcess, // 进程句柄
UINT uExitCode // 进程结束的原因
);
// 打开一个已经存在的进程 (相当于共享了 EPROCESS内核结构)
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 被打开的进程,拥有的访问权限
BOOL bInheritHandle, // 是否能被继承
DWORD dwProcessId // 进程的 PID
);
// 使用句柄关闭进程,失败
int main() {
HANDLE hProcess;
hProcess = (HANDLE)0xf8;
if (!TerminateProcess(hProcess, 1)) {
printf("终止进程失败:%d\n", GetLastError()); // 失败,错误代码 6(句柄无效)
}
// 进程句柄是私有的,不能用 A进程的句柄,来结束 A进程
// 因为在 main进程中, A进程的句柄值是无效的
return 0;
}
// 使用 PID关闭进程,成功
int main() {
HANDLE hProcess;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 0x14d8); // 使用 PID
// 使用 OpenProcess函数后,main进程和 A进程共享了内核对象
// main 进程能通过 hProcess关闭 A进程
if (!TerminateProcess(hProcess, 1)) {
printf("终止进程失败:%d\n", GetLastError());
}
return 0;
}
2、以挂起的形式创建进程
// CreateProcess 函数的 dwCreationFlags参数
值:CREATE_NEW_CONSOLE
含义:新的进程将使用一个新的控制台,而不是继承父进程的控制台。这个标志不能与DETACHED_PROCESS标志一起使用
值:CREATE_SUSPENDED
含义:新进程的主线程会以暂停的状态被创建,直到调用 ResumeThread函数被调用时才运行
// 进程创建过程发生改变
1.映射EXE文件 (A.exe)
2.创建内核对象EPROCESS,每个进程都有自己的EPROCESS对象
3.映射系统DLL(ntdll.dll)
4.创建系统内核对象ETHREAD
5.进程以挂起的方式创建:
。。。进程进入等待状态 //可在此注入自己的代码
6.恢复以后再继续执行:
映射DLL(ntdll.LdrlnitializeThunk)
线程开始执行
BOOL CreatChildProcess(PTCHAR szChildProcessName, PTCHAR szComandLine) {
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi)); // 结构体置 0
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si); // cb 成员,记录结构体大小
// 创建子进程,返回成功或失败
if (!CreateProcess(
szChildProcessName, // 对象名称,要创建的进程的名字
szComandLine, // 命令行
NULL, // 不继承进程句柄
NULL, // 不继承线程句柄
FALSE, // 不继承句柄
CREATE_SUSPENDED, // 挂起方式创建
NULL, // 使用父进程环境变量
NULL, // 使用父进程目录作为当前目录,可以自己设置目录
&si, // STARTUPINFOA结构体详细信息,IN参数,需要传入的参数
&pi // Process_INFORMATION结构体进程信息,OUT参数,传出函数执行结果
)) {
printf("错误代码:%d \n", GetLastError());
return FALSE;
}
// 获取进程信息
printf("%x %x", pi.dwProcessId, pi.hProcess);
Sleep(10000);
// 线程执行前,插入我们想执行的代码
ResumeThread(pi.hThread);
// 释放句柄
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return true;
}
int main() {
TCHAR szApplicationName[] = TEXT("C:\\b.exe");
CreatChildProcess(szApplicationName, NULL);
return 0;
}
3、模块目录、工作目录
int main() {
TCHAR strModule[256];
GetModuleFileName(NULL, strModule, 256); // 获取当前模块(当前运行的exe),所在的路径
TCHAR strWork[256]; // 获取工作路径(工作路径由父进程设置,默认是父进程的工作路径)
GetCurrentDirectory(256, strWork);
printf("模块路径:%s\n", strModule);
printf("工作路径:%s\n", strWork);
return 0;
}
4、其他进程相关API
GetCurrentProcessId 获取进程 PID
GetCurrentProcess 获取进程句柄
GetCommandLine 获取命令行
GetStartupInfo 获取启动信息
EnumProcess 遍历进程 PID(系统中所有进程)
CreateToolhelp32Snapshot // 进程快照:获取系统当前全部进程,进程拥有的模块
#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h>
/*
typedef struct tagMODULEENTRY32W{
DWORD dwSize;
DWORD th32ModuleID; // This module
DWORD th32ProcessID; // owning process
DWORD GlblcntUsage; // Global usage count on the module
DWORD ProccntUsage; // Module usage count in th32ProcessID's context
BYTE * modBaseAddr; // Base address of module in th32ProcessID's context
DWORD modBaseSize; // Size in bytes of module starting at modBaseAddr
HMODULE hModule; // The hModule of this module in th32ProcessID's context
WCHAR szModule[MAX_MODULE_NAME32 + 1];
WCHAR szExePath[MAX_PATH];
} MODULEENTRY32W;
*/
// 其他进程相关 API 作业
int main() {
MODULEENTRY32 ModulInfo;
ZeroMemory(&ModulInfo, sizeof(MODULEENTRY32));
ModulInfo.dwSize = sizeof(MODULEENTRY32);
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnapshot == NULL){
printf("Error:%d\n", GetLastError());
return FALSE;
}
if (!Module32First(hSnapshot, &ModulInfo)){
printf("Error:%d\n", GetLastError());
CloseHandle(hSnapshot);
return FALSE;
}
// 打印模块信息
do
{
wprintf(L"---------------------------------------------------\n");
wprintf(L"ModulName:%s\n", ModulInfo.szModule);
wprintf(L"ModulPath:%s\n", ModulInfo.szExePath);
wprintf(L"ModulBaseAddr:%p\n", ModulInfo.modBaseAddr);
wprintf(L"ModulHandle:%d\n", ModulInfo.hModule);
wprintf(L"ModulSize:%d\n", ModulInfo.dwSize);
wprintf(L"ProcessID:%d\n", ModulInfo.th32ProcessID);
wprintf(L"---------------------------------------------------\n");
} while (Module32Next(hSnapshot, &ModulInfo));
CloseHandle(hSnapshot);
// 打印进程信息
PROCESSENTRY32 pe32;
ZeroMemory(&pe32, sizeof(PROCESSENTRY32));
pe32.dwSize = sizeof(PROCESSENTRY32);
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE){
printf("Error:%d\n", GetLastError());
return FALSE;
}
if (!Process32First(hSnapshot, &pe32)){
printf("Error:%d\n", GetLastError());
CloseHandle(hSnapshot);
return FALSE;
}
do
{
wprintf(L"---------------------------------------------------\n");
wprintf(L"Proc ID:%d\n", pe32.th32ProcessID);
wprintf(L"Proc Name:%s\n", pe32.szExeFile);
wprintf(L"Proc Parent ID:%d\n", pe32.th32ParentProcessID);
wprintf(L"Threads count:%d\n", pe32.cntThreads);
wprintf(L"---------------------------------------------------\n");
} while (Process32Next(hSnapshot, &pe32));
return 0;
}
3、线程
(1)创建线程
1、线程
- 线程是附属在进程上的执行实体,是代码的执行流程
- 一个进程可以包含多个线程,但一个进程至少要包含一个线程
- main 函数就是一个线程
2、创建线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD,安全描述符
SIZE_T dwStackSize, // initial stack size,线程需要的初始堆栈大小
LPTHREAD_START_ROUTINE lpStartAddress, // thread function,线程要执行的代码(有指定的格式)
LPVOID lpParameter, // thread argument,参数
DWORD dwCreationFlags, // creation option,创建标志
// 0:创建后立即运行; CREATE_SUSPENDED:挂起的方式创建
LPDWORD lpThreadId // thread identifier,返回线程 id
);
DWORD WINAPI ThreadProc(
LPVOID lpParameter // thread data
);
3、向线程函数传递变量
// 标准格式,参数可以使用或不使用
// 不使用参数 CreateThread(NULL,0, ThreadProc,NULL,0,NULL);
DWORD WINAPI ThreadProc(LPVOID lpThreadParameter) {
int* p = (int*)lpThreadParameter;
for (int i = 0; i < *p; i++) {
Sleep(500);
printf("Thread --------------%d\n", i);
}
return 0;
}
// 需要强制转型
void WINAPI TestProc() {
for (int i = 0; i < 20; i++) {
Sleep(500);
printf("Thread --------------%d\n", i);
}
}
int main() {
int n = 20;
// 局部变量 n在 main函数的堆栈中定义 (建议使用全局变量)
// 向线程函数传递参数时,要保证参数的生命周期比线程的生命更长
// 如果 main函数执行完毕后,线程还没执行完毕,main函数会清栈,参数 n会丢失
HANDLE hThread = CreateThread(NULL,0, ThreadProc,(LPVOID)&n,0,NULL);
//HANDLE hThread = CreateThread(NULL,0, (LPTHREAD_START_ROUTINE)TestProc,NULL,0,NULL);
CloseHandle(hThread);
// 线程被清理的2个条件:直接 CloseHandle无法清理线程
// 线程内核对象的使用计数器清0,线程执行的代码已经执行完毕,线程对应的内核对象才会被清理
for (int i = 0; i < 20; i++) {
Sleep(500);
printf("Main --------------%d\n", i);
}
return 0;
}
(2)线程控制
1、让线程停下来
Sleep() // 让自己停下来
SuspendThread() // 让别人停下来
ResumeThread() // 线程恢复
DWORD WINAPI ThreadProc(LPVOID lpThreadParameter) {
for (int i = 0; i < 20; i++) {
Sleep(50);
printf("Thread --------------%d\n", i);
}
return 0;
}
int main() {
HANDLE hThread = CreateThread(NULL,0, ThreadProc,NULL,0,NULL);
Sleep(5000); // 让 main线程,休息5秒
SuspendThread(hThread); // 使线程处于阻塞状态
// SuspendThread(hThread); // 挂起同一线程 2次,也必须恢复2次
Sleep(5000);
ResumeThread(hThread);
CloseHandle(hThread);
return 0;
}
2、等待线程结束
- WaitForSingleObject 函数、WaitForMultipleObjects 函数
DWORD WaitForSingleObject(
HANDLE hHandle, // 进程或线程句柄
DWORD dwMilliseconds // 等待时间
);
int main() {
HANDLE hThread = CreateThread(NULL,0, ThreadProc,NULL,0,NULL);
WaitForSingleObject(hThread,INFINITE); // 无限等待,直到目标对象状态发生改变,main函数才会继续执行
printf("线程执行完毕\n"); // 线程执行完毕,打印这句话
CloseHandle(hThread);
return 0;
}
DWORD WaitForMultipleObjects(
DWORD nCount, // 等待内核对象的个数
CONST HANDLE* lpHandles, // 内核对象的句柄数组
BOOL bWaitAll, // 等待方式 True:所有对象状态改变,才继续执行
DWORD dwMilliseconds // 等待时间
);
int main() {
HANDLE hThread[2]; // 创建 2个线程
hThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
WaitForMultipleObjects(2,hThread,TRUE,INFINITE); // 直到 2个目标对象状态发生改变,main函数才会继续执行
printf("线程执行完毕\n");
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
- GetExitCodeThread 函数
BOOL GetExitCodeThread(
_In_ HANDLE hThread, // 线程句柄
_Out_ LPDWORD lpExitCode // 返回状态
);
DWORD WINAPI ThreadProc(LPVOID lpThreadParameter) {
// ... 线程函数执行完毕,返回 0
return 0;
}
DWORD WINAPI ThreadProc1(LPVOID lpThreadParameter) {
// ... 线程函数执行完毕,返回 1
return 1;
}
int main() {
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
DWORD dwResult1, dwResult2; // 2个线程的返回结果
WaitForMultipleObjects(2,hThread,TRUE,INFINITE);
GetExitCodeThread(hThread[0], &dwResult1); // 获取线程执行结果,存入 dwResult1
GetExitCodeThread(hThread[1], &dwResult2);
printf("线程执行完毕\n");
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
3、设置、获取线程上下文
- 单核处理器只能同时运行一个线程,在进行线程切换时,线程会把当前 CPU寄存器的内容保存在 结构体 context 中
typedef struct DECLSPEC_NOINITALL _CONTEXT {
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
DWORD ContextFlags;
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
FLOATING_SAVE_AREA FloatSave;
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
- 获取、设置寄存器的值
// 函数原型
BOOL GetThreadContext(
HANDLE hThread, // handle to thread with context
LPCONTEXT lpContext // context structure
);
BOOL SetThreadContext(
HANDLE hThread, // handle to thread
CONST CONTEXT *lpContext // context structure
);
int main() {
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
SuspendThread(hThread[0]); // 线程挂起后,才能获取上下文
CONTEXT ct1;
ct1.ContextFlags = CONTEXT_INTEGER; // 需要获取哪写寄存器的值,就给 ContextFlags赋对应的值
GetThreadContext(hThread[0], &ct1);
printf("%x %x %x %x\n", ct1.Eax, ct1.Ebx, ct1.Ecx, ct1.Edx);
// 设置寄存器的值,用 SetThreadContext 函数传入
ResumeThread(hThread[0]);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
(3)临界区的构建
1、线程安全问题
- 每个线程都有自己的栈,而局部变量是存储在栈中的,这就意味着每个线程都有一份自己的 "局部变量"
- 如果线程仅仅使用 "局部变量" 那么就不存在线程安全问题
- 如果多个线程共用一个全局变量,且对全局变量进行写操作
2、解决方法
- 令牌:就是一个全局变量(是应用层的令牌)
3、临界区实现之线程锁
1. 创建全局变量
CRITICAL_SECTION cs; // 结构体
2. 初始化全局变量
InitializeCriticalSection(&cs);
3. 实现临界区 (线程锁)
EnterCriticalSection(&cs);
// 使用临界资源的代码(判断和读写全局变量的代码,放在临界区内部)
LeaveCriticalSection(&cs);
int tickets = 10;
CRITICAL_SECTION cs; // 临界区结构体
DWORD WINAPI Thread1(LPVOID lpThreadParameter) {
EnterCriticalSection(&cs);
while (tickets > 0) { // 判断全局变量的代码,不能放在临界区之外
// 会导致卖出后,还剩 -1张票
printf("还有 %d张,", tickets);
tickets--;
printf("卖出一张,还剩 %d张\n", tickets);
}
LeaveCriticalSection(&cs);
return 0;
}
int main() {
InitializeCriticalSection(&cs); // 初始化
DWORD dwResult1, dwResult2;
HANDLE hThread[2];
// 用 2个窗口来卖票
hThread[0] = CreateThread(NULL, 0, Thread1, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, Thread1, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
GetExitCodeThread(hThread[0], &dwResult1);
GetExitCodeThread(hThread[1], &dwResult2);
printf("%d %d\n", dwResult1, dwResult2);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
return 0;
}
(4)互斥体
1、内核级临界资源
- 线程锁:只能控制同一个进程中的两个或多个线程,共享一个临界资源,只有一个进程能访问令牌
- 互斥体:内核级别的令牌,多个进程能同时访问互斥体
2、互斥体的使用
HANDLE CreateMutexW(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner, // False:创建后,能直接使用(有信号)
// True: 创建后,不能直接使用(没信号),表示当前互斥体属于当前的进程
LPCWSTR lpName // 给内核互斥体命名
);
获取互斥体令牌的方式:
1. 有信号
2. 线程的拥有者 (即使参数为 True,A进程仍能获取互斥体,但 B进程无法获取)
// 互斥体.cpp (A进程)
int main() {
// 创建一个互斥体
HANDLE hMutex = CreateMutex(NULL, FALSE, L"XYZ");
// 获取互斥体
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 10; i++) {
Sleep(1000);
printf("A进程的X线程 %d\n", i);
}
// 释放互斥体
ReleaseMutex(hMutex);
return 0;
}
// Test.cpp (B进程)
int main() {
HANDLE hMutex = CreateMutex(NULL, FALSE, L"XYZ"); // 这个互斥体创建失败,因为 A进程已经创建了一个同名互斥体
// 但仍会返回同名互斥体的句柄
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 10; i++) {
Sleep(1000);
printf("B进程的Y线程 %d\n", i);
}
ReleaseMutex(hMutex);
return 0;
}
3、互斥体与线程锁的区别
- 线程锁只能用于单个进程间的线程控制
- 互斥体可以设定等待超时,但线程锁不能
- 进程A的线程意外终结时(会自动释放令牌),Mutex可以避免进程B无限等待
- Mutex效率没有线程锁高
int main() {
HANDLE hMutex = CreateMutex(NULL, TRUE, L"XYZ");
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < 10; i++) {
Sleep(1000);
if (i == 5) ExitProcess(0); // A进程意外终结,会自动释放互斥体
printf("A进程的X线程 %d\n", i);
}
ReleaseMutex(hMutex);
return 0;
}
4、互斥体应用
- 防止游戏(程序)多开,保证一个进程只能运行一份
int main() {
// 创建一个互斥体
HANDLE hMutex = CreateMutex(NULL, FALSE, L"防止多开");
DWORD dwRet = GetLastError();
/*
先判断句柄是否有值,
有值:
1. 第一次创建,返回句柄值,GetLastError函数返回 0
2. 再次创建,返回句柄值,GetLastError函数返回 ERROR_ALREADY_EXISTS
无值:
程序创建失败
*/
if(hMutex){
if(ERROR_ALREADY_EXISTS == dwRet){
CloseHandle(hMutex);
return 0;
}
}
else{
printf("创建失败,程序退出\n");
CloseHandle(hMutex);
return 0;
}
while(1){
printf("执行程序\n");
}
return 0;
}
(5)事件
1、事件可作为通知类型来使用
- 同时通知(唤醒)许多其他线程
HANDLE CreateEventW(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset, // True:当前 Event是通知类型 False:Event是互斥体
BOOL bInitialState, // False:创建后,无信号 True:创建后,有信号
// 无信号,使用 WaitForSingleObject无法获取目标对象
LPCWSTR lpName // 给 Event命名,不命名只能在当前进程使用该 Event,其他进程无法使用
);
HANDLE hEvent;
DWORD WINAPI Thread1(LPVOID lpThreadParameter) {
TCHAR szBuffer[10] = { 0 };
// 当事件变成通知时
WaitForSingleObject(hEvent, INFINITE);
// 线程执行
printf("Thread1执行了\n");
getchar();
return 0;
}
DWORD WINAPI Thread2(LPVOID lpThreadParameter) {
TCHAR szBuffer[10] = { 0 };
// 当事件变成通知时
WaitForSingleObject(hEvent, INFINITE);
printf("Thread2执行了\n");
getchar();
return 0;
}
int main() {
// 创建事件
// 默认安全属性,True通知/False互斥,初始没信号、没名字
hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, Thread1, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, Thread2, NULL, 0, NULL);
// 设置事件为已通知(设置 Event为有信号)
SetEvent(hEvent);
// 等待事件结束,销毁内核对象
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hEvent);
return 0;
}
2、线程同步
- 线程互斥:
- 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。
- 当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用
- 其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
- 线程同步:
- 线程同步是指线程之间所具有的一种制约关系
- 一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
3、同步的前提是互斥
- 同步 = 互斥 + 有序
- 使用 互斥体实现线程同步,缺点:浪费 cpu时间片
HANDLE hMutex; // 互斥体
int max = 10; // 生产 10个产品
int number = 0; // 产品容器(缓冲区)
// 生产者线程
DWORD WINAPI ThreadProduct(LPVOID lpThreadParameter) {
for (int i = 0; i < max; i++) {
// 互斥的访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
if (number == 0){
number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区\n", id, number);
}else {
i--; // 浪费了 cpu时间片
}
ReleaseMutex(hMutex);
}
return 0;
}
// 消费者线程
DWORD WINAPI ThreadConsumer(LPVOID lpThreadParameter) {
for (int i = 0; i < max; i++) {
// 互斥的访问缓冲区
WaitForSingleObject(hMutex, INFINITE);
if (number != 0) {
number = 0;
DWORD id = GetCurrentThreadId();
printf("消费者%d将数据%d放入缓冲区\n", id, number);
}else {
i--; // 白白消耗 cpu时间
}
ReleaseMutex(hMutex);
}
return 0;
}
int main() {
hMutex = CreateMutex(NULL, FALSE, NULL);
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, ThreadProduct, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hMutex);
return 0;
}
- 使用事件实现线程同步,解决了 cpu 时间片浪费的问题
HANDLE hSet,hClear; // 生产者事件、消费者事件
int max = 10; // 生产 10个产品
int number = 0; // 产品容器(缓冲区)
// 生产者线程
DWORD WINAPI ThreadProduct(LPVOID lpThreadParameter) {
for (int i = 0; i < max; i++) {
// 互斥的访问缓冲区
WaitForSingleObject(hSet, INFINITE);
number = 1;
DWORD id = GetCurrentThreadId();
printf("生产者%d将数据%d放入缓冲区\n", id, number);
SetEvent(hClear);
}
return 0;
}
// 消费者线程
DWORD WINAPI ThreadConsumer(LPVOID lpThreadParameter) {
for (int i = 0; i < max; i++) {
// 互斥的访问缓冲区
WaitForSingleObject(hClear, INFINITE); // 获取对象后,把 hClear设为无信号
number = 0;
DWORD id = GetCurrentThreadId();
printf("消费者%d将数据%d放入缓冲区\n", id, number);
SetEvent(hSet);
// 将 hSet设为有信号(唤醒生产者线程),并将消费者线程挂起(即使还有多余的cpu时间片,仍会挂起)
}
return 0;
}
int main() {
// 只在 main进程使用,第4个参数为 NULL,不命名
hSet = CreateEvent(NULL, FALSE, TRUE, NULL); // 初始有信号,让生产者先运行
hClear = CreateEvent(NULL, FALSE, FALSE, NULL); // 初始无信号
HANDLE hThread[2];
hThread[0] = CreateThread(NULL, 0, ThreadProduct, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, 0, ThreadConsumer, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hSet);
CloseHandle(hClear);
return 0;
}