0x06 Windows编程提高(内核、进程线程)
1. 内核对象
- 什么是内核对象
- 操作系统中的三种对象:GDI对象(GDI32) USER对象(User32) 内核对象(kernel32)
- 其中,GDI对象和USER对象在所有应用程序中的值都是固定的。(句柄时全局的)
- 内核对象本质上是一个结构体,但是这个结构体处于内核层,在用户层,只能通过操作系统提供的接口进行间接的访问。
- 常见的内核对象有:进程,线程,文件,邮槽,IOCP,事件 等
- 操作系统中的三种对象:GDI对象(GDI32) USER对象(User32) 内核对象(kernel32)
- 内核对象的特点:
- 全局性:内核对象是跨进程的,可以在不同进程中访问同一个对象,通常使用字符串或ID标识一个对象。
- 引用计数:每一个内核对象都有一个引用计数,当创建或者打开一个内核对象的时候,引用计数会+1,当关闭一个内核对象的时候,引用计数会-1,当引用计数为0时,内核对象会被销毁。
- 安全性:除IOCP对象,所有的内核对象都需要传入安全描述符用于控制访问权限。
- 句柄表:每一个进程都有一个句柄表,相同的内核对象在不同的进程中,可能句柄值是不一样的。
2. 文件操作
- 文件操作相关函数
- 不需要传入句柄
- 删除文件:DeleteFile
- 拷贝文件:CopyFileA
- 移动文件:MoveFileA
- 获取文件属性:GetFileAttributesExA
- 需要传入句柄
- 打开\创建文件: CreateFile,失败返回 INVALID_HANDLE_VALID(-1)
- 获取文件指针:SetFilePointer
- 读取\写入数据: Read\WriteFile
- 获取文件大小:低位 = GetFileSize(文件句柄,大小的高位)
- 设置文件结尾: SetEndOfFile
- 遍历文件API
- FindFirstFile 、 FindNextFile、 注意 “ * “,遍历文件需要排除 . ..
- 不需要传入句柄
3. 进程和模块
- 什么是进程
- 进程可以看作是一个运行中的程序,应该由一个可执行程序(.exe)产生。一个进程最少包含了
- 一个进程内核对象、一个线程内核对象、一块虚拟地址空间(4GB)、需要用到的模块。不执行代码
- 什么是模块
- windows下的可执行文件通常被称作模块,用于提供必须的数据以及代码。
- 进程的类型(windows子系统)
- GUI程序:窗口程序,入口为 (w)WinMain
- CUI程序:控制台程序,入口为 (w)main
- 进程的相关函数
- 创建进程:CreateProcess、WinExec、system、ShellExecute 等
- 结束进程:TerminateProcess、 ExitProcess
- 打开进程:OpenProcess,不同的操作需要传入不同的权限
- 获取当前的句柄:GetCurrentProcess() 返回的是伪句柄,使用 DumplicateHandle进行转换
- 获取当前PID:GetCurrentProcessId(),pid和tid使用的是同一个序列
- 遍历进程:CreateToolhelp32Snapshot \ Process32First \ Process32Next
- 进程通信的方式:
- 邮槽、套接字、WM_COPYDATA、管道、文件映射
4. 线程和线程同步
- 线程的基本概念
- 线程其实可以理解为一段正在执行中的代码,它最少由一个线程内核对象和一个栈(4M)组成。
- 线程之间是没有从属关系的,同一进程下的所有线程都可以访问进程内的所有内容。
- 主线程其实是创建进程时创建的线程,主线程一旦退出,所有子线程也会退出。
- 线程的相关函数
- 创建线程:CreateThread、_beginthreadex()
- 结束线程:TerminateThread、_endthreadex()、ExitThread
- 等待线程:WaitForSingleObject,线程结束是有信号的
- 挂起线程:SuspendThread
- 线程有挂起次数,挂起多少次就要恢复多少次
- 恢复线程:ResumeThread
- 切换线程:SwitchToThread Sleep
- 遍历线程:CreateToolhelp32Snapshot、Thread32First、Thread32Next
- 无论PID传入的是什么,遍历到的都是所有线程
- 线程的退出方式
- 主线程函数(main\WinMain)返回,最为友好,会调用析构函数、会清理栈
- 使用ExitThread:不会调用析构函数
- 使用TerminateThread:不会调用析构函数,不会清理栈
- 结束进程:可能来不及保存工作结果
- 线程的优先级
- 线程只有相对于进程的优先级,随着进程优先级的改变,线程的相对优先级并不会改变
- 通常情况下手动修改优先级并不会对程序的执行产生变化
- 线程环境
- 线程环境指的是一个线程当前的执行状态,包括了当前执行到了那里,局部变量和参数有哪些等。
- windows是一个分时系统,当一个线程时间片用完时,会保存当前线程的执行状态,并且切
- 换到新的线程,同时将新线程上次的执行状态恢复。这就是线程切换的基本原理
- 使用 GetThreadContext 和 SetThreadContext 可以获取和设置线程环境
- 记得给结构体的第一个字段传参。
- 调试器
- 线程同步
- 同步和互斥
- 同步:就是按照一定的规则执行不同的线程
- 互斥:当多个线程要访问相同的数据时
- 用户层的线程同步
- 原子操作(Interlocked…)
- INLINE HOOK的时候用于填充OPCODE
- 特点:将一条语句转换成了具有同等功能的单条汇编指令 lock(指令前缀)
- 缺点:只能给单个整数类型(4/8)进行保护,不能给使一段代码变成原子操作
- 函数:
- InterlockedExchange 交换两个数【关键】inline hook 中会用到
- InterlockedIncrement 自增
- 临界区(CriticalSection)
- 特点:拥有线程拥有者的概念,同一个线程可以不断的重新进入临界区,但是进入了多
- 少次,就要退出多少。
- 缺点:一旦拥有临界区的线程崩溃,那么所有等待临界区的线程就会产生死锁。
- 函数:
- 初始化: InitializeCriticalSection
- 保护:EnterCriticalSection 这两个函数之间的内容就是被保护的代码
- 结束保护 :LeaveCriticalSection
- 删除:DeleteCriticalSection
- 原子操作(Interlocked…)
- 内核层的线程同步
- 等待函数__(WaitForObject)
- 等待函数的形式
- 单个:WaitForSingleObject
- 多个:WaitForMultipleObjects
- 等待函数的形式
- 一个可以被等待的对象通常由两种状态,分别是:
- 可等待(激发态)(有信号):等待函数【不会阻塞】
- 不可等待(非激发态)(无信号):等待函数需要等待一定时长并【阻塞】
- 等待函数的副作用:
- 改变被等待对象的信号状态(激发态变为 非激发态)
- 互斥体(Mutex)
- 特点:拥有临界区的线程拥有者概念,但是线程崩溃不会死锁,执行较慢
- 函数
- 创建:CreateMutex,如果第二个参数是FALSE,那么就是可等待
- 打开:OpenMutex
- 保护:WaitForSingleObject: 把互斥体置为不可等待
- 离开保护:ReleaseMutex把互斥体置为可等待
- 关闭:CloseHandle
- 事件(Event)
- 特点:可以手动选择是否产生副作用,如果设置为手动状态,那么就不产生副作用
- 函数:
- 创建:CreateEvent()
- 打开:OpenEvent()
- 保护:WaitForSingleObject()
- 设置为非激发态:ResetEvent()
- 退出(设置为激发态):SetEvent()
- 关闭:CloseHandle
- 信号量(Semaphore)
- 特点:有多把锁,可以控制活动线程的数量
- 函数:
- 创建:CreateSemaphore
- 打开:OpenSemaphore
- 上锁:WaitForSingleObject
- 解锁:ReleaseSemaphore
- 关闭:CloseHandle
- 用途:一般用做消费者-工厂模式
- 等待函数__(WaitForObject)
- 同步和互斥
5. 同步IO和异步IO
- 同步和异步
- 同步:以普通的方式读写一个文件就是同步IO,当读写的数量较大的时候,所在线程会阻塞
- ReadFile() \ WriteFile
- 异步:以重叠IO的方式读写一个文件的操作,它会在调用IO函数的时候提交一个IO请求给操作系统,然后函数不等IO 操作执行完毕继续往下执行,当IO操作结束时,操作系统会以某种方式返回一个信号。
- 在打开文件的时候必须指定 FILE_FLAG_OVERLAPPED 标记
- 必须在读写函数内传入 OVERLAPPEND 结构体
- 当使用重叠IO的时候,Read|WriteFile 的第四个字段可以传入 NULL
- 在重叠IO中文件指针是没有用的,需要使用 OVERLAPPEND 结构体中的Offset(High)字段进行设置
- 同步:以普通的方式读写一个文件就是同步IO,当读写的数量较大的时候,所在线程会阻塞
- 异步操作 - 等待句柄
```cpp
// 缺点:当IO请求过多的时候,不知道等待完成的是哪一个IO操作
include
include
using namespace std; int main() { // 1. 打开一个文件 HANDLE FileHandle = CreateFileA( “F:\test.txt”, GENERIC_ALL, NULL, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); // 2. 进行读写操作 CHAR Buffer[0x100] = { 0 }; OVERLAPPED OverLappend = { 0 }; OverLappend.Offset = 4; // 使用重叠IO必须的结构体
ReadFile(FileHandle, Buffer, 0x100, NULL, &OverLappend); // 3. 通过等待文件句柄可以知道一个IO操作是否完成 WaitForSingleObject(FileHandle, INFINITE); return 0; }
- **异步操作 - 等待对象**
```cpp
// 相对于等待句柄来说,可以识别是哪一个IO操作完成了
#include <iostream>
#include <windows.h>
using namespace std;
int main()
{
// 1. 打开一个文件
HANDLE FileHandle = CreateFileA(
"F:\\test.txt", GENERIC_ALL, NULL,
NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
// 2. 进行读写操作(读取0~5)
CHAR BufferA[0x100] = { 0 };
OVERLAPPED OverLappendA = { 0 };
OverLappendA.Offset = 0;
// 使用重叠IO必须的结构体
OverLappendA.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
ReadFile(FileHandle, BufferA, 0x5, NULL, &OverLappendA);
// 3. 进行读写操作(读取6~10)
CHAR BufferB[0x100] = { 0 };
OVERLAPPED OverLappendB = { 0 };
OverLappendB.Offset = 6;
// 使用重叠IO必须的结构体
OverLappendB.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
ReadFile(FileHandle, BufferB, 0x5, NULL, &OverLappendB);
// 3. 通过等待文件句柄可以知道一个IO操作是否完成
WaitForSingleObject(OverLappendA.hEvent, INFINITE);
WaitForSingleObject(OverLappendB.hEvent, INFINITE);
return 0;
}
异步操作 - APC函数
- APC队列: 异步过程调用队列,保存的是一组函数的地址以及需要的参数
- APC队列内的函数在什么时候调用?当线程属于闲暇状态(可警醒)的时候会查看当前的APC队列是否存在函数,如果存在函数就会一一调用,如果不存在就会休眠或者继续执行。
- 投递的请求并不一定按照投递顺序完成
- APC队列中的函数是根据完成的顺序放入的
- APC队列时线程相关的,每一个线程都有一个APC队列
```cpp
include
include
using namespace std; CHAR BufferA[0x100] = { 0 }; CHAR BufferB[0x100] = { 0 }; // IO操作完成后操作系统会向APC队列添加一个这样的函数 VOID WINAPI OverLappedRoutine( DWORD dwErrorCode, // 错误码 DWORD dwNumberOfBytesTransfered,// 实际操作的字节数 LPOVERLAPPED lpOverlapped // 重叠IO结构体 ) { // 判断当前时哪一个消息 if (lpOverlapped->hEvent == (HANDLE)0x01) { printf(“%s\n”, BufferA); } else { printf(“%s\n”, BufferB); } }
int main() { // 1. 打开一个文件 HANDLE FileHandle = CreateFileA( “F:\test.txt”, GENERIC_ALL, NULL, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); // 2. 进行读写操作(读取0~5) OVERLAPPED OverLappendA = { 0 }; OverLappendA.Offset = 0; // 使用重叠IO必须的结构体
OverLappendA.hEvent = (HANDLE)0x01; ReadFileEx(FileHandle, BufferA, 0x5, &OverLappendA, OverLappedRoutine); // 3. 进行读写操作(读取6~10) OVERLAPPED OverLappendB = { 0 }; OverLappendB.Offset = 6; // 使用重叠IO必须的结构体
- APC队列: 异步过程调用队列,保存的是一组函数的地址以及需要的参数
OverLappendB.hEvent = (HANDLE)0x02; ReadFileEx(FileHandle, BufferB, 0x5, &OverLappendB, OverLappedRoutine); // 将线程设置为可警醒状态 SleepEx(0, TRUE); return 0; } // 最常使用的是完成那个端口模型 ```