线程与内存管理
线程相关
CreateThread / OpenThread
通过这两个函数获取线程句柄.
- 前者可以用C++11的 thread 库代替.
- 后者传入对应的线程tid, 返回对应的线程句柄.
线程句柄的作用
- 使用
TerminateThread
直接终止线程.
- 使用
WaitForSingleObject
等待线程结束.
- 使用
SuspendThread/ResumeThread
随时挂起和唤醒线程, 一般用来配合分析检测.
- 使用
GetThreadTimes
获取时间, 例如创建时间, CPU占用时间.
SetThreadAffinityMask
将某个线程任务绑定到某些CPU核上, 使得这些CPU优先运行这个任务.
其作用在于 :
- 避免频繁的线程切换.
- 可以有效缓解缓存失效, 每个CPU核都有自己的缓存, 会缓存过去运行线程的变量/指令, 随意改变线程执行的核会让这些缓存失效.
使用方法就是传入线程句柄, 传入掩码, 掩码中每有一个1, 任务就会被绑定到其对应的核上.
1 2
| DWORD_PTR mask = 0x01; SetThreadAffinityMask(hThread, mask);
|
适用于计算密集型/低延迟要求的任务, 比如音视频解码, 金融交易.
SwitchToThread
调用该函数, 会使当前线程主动让出时间片, 供其他线程使用.
- 一些任务如果认为优先级低(就是可以延后执行), 那么可以设置一些节点触发
SwitchToThread
, 让一些高优先级任务(比如UI界面更新)更有可能拿到时间片.
自旋锁是让线程自旋一会等待解锁, 可以有效减少线程切换带来的消耗, 用SwitchToThread
可以优化其逻辑 :
1 2 3 4 5 6 7 8 9 10 11 12 13
| volatile bool g_Lock = false;
void SpinLockWithYield() { int spinCount = 0; while (InterlockedCompareExchange(&g_Lock, true, false) != false) { if (++spinCount > 100) { SwitchToThread(); spinCount = 0; } _mm_pause(); } }
|
原本自选锁会一直在这里自旋, 将持续占用一个CPU核, 我们预计会很快解锁, 但是假如有特殊情况一直拿不到锁, 累计到一定自选次数后, 就可以调用SwitchToThread
让出时间片来减少空转.
内存管理相关
Windows内存管理也和Linux的那一套非常相似, 进程地址空间 + 分页.
VirtualAlloc
这是Windows操作系统底层的内存申请函数, malloc在windows平台底层就会间接调用到该函数. 可以认为是Windows版的mmap.
1 2 3 4 5 6
| LPVOID VirtualAlloc( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect );
|
lpAddress
: 你可以指定分配的地址, 但是该指针必须页对齐, 不然会失败.
dwSize
: 指定分配大小, 最终结果会按页对齐.
- 对齐 : 默认页大小为4KB(4096), 因此在每个页开始处进行访问只需要一次指令, 但是其他地方就要多条指令, 因此我们希望分配地址是4KB的整数倍, 也就是所谓的页对齐.
flAllocationType
: 指定分配的类型.
MEM_RESERVE
: 保留一段虚拟地址空间, 但先不申请内存, 需要时再COMMIT.
MEM_COMMIT
: 提交对于物理地址空间的申请, 会做虚拟到物理的映射.
- RESERVE出来的空间不可被使用, 只是占用使用权, 要使用时必须COMMIT.
- 最常见的做法是填入
MEM_RESERVE | MEM_COMMIT
, 本质就是申请一块物理空间可以随时使用.
flProtect
: 设置内存保护属性, 防止自己对某些重要内存进行修改.
平常大多数情况使用malloc申请内存即可, 只是在申请大块内存, 有延迟映射需求, 有内存对齐需求, 需要控制内存保护权限的情况下可以选择使用VirtualAlloc
.
VirtualFree : 需要配套使用.
1 2 3 4 5
| BOOL VirtualFree( LPVOID lpAddress, SIZE_T dwSize, DWORD dwFreeType );
|
VirtualProtect
修改已分配内存的保护属性.
1 2 3 4 5 6
| BOOL VirtualProtect( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect );
|
- 建议只使用
VirtureAlloc
出来的句柄设置, 因为其是按页修改保护属性的, malloc
出来的并非页对齐.
- 默认是
PAGE_READWRITE
可读写, 可改为PAGE_READONLY
只读和PAGE_NOACCESS
禁止访问.
- 保护熟悉设置一视同仁, 对本进程和其他进程同样有效, 一般是先读写完, 如果想保护数据在后期通过该函数设置只读或禁读.
VirtualQuery / VirtualQueryEx
查询进程虚拟内存状态/属性/归属的重要函数(可跨进程).
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| SIZE_T VirtualQuery( LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer, SIZE_T dwLength );
SIZE_T VirtualQueryEx( HANDLE hProcess, LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer, SIZE_T dwLength );
|
主要用来查询指定内存的状态, 在读写自己或其他内存前使用, 可以有效避免崩溃.
以下是缓存区的结构 :
1 2 3 4 5 6 7 8 9
| typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; DWORD AllocationProtect; SIZE_T RegionSize; DWORD State; DWORD Protect; DWORD Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
|
- 一般是读取
State
和Protect
, 地址可以用来读取内存, 但是仅限本进程, 跨进程需要ReadProcessMemory
.
ReadProcessMemory / WriteProcessMemory
跨进程读写内存的核心函数.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesRead );
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesWritten );
|
关键就是目标进程句柄+虚拟地址+缓冲区, 但前提是读写的内存必须已提交, 读必须有可读权限, 写必须有可写权限, 这点靠VirtualQueryEx
查询状态保证.
跨进程内存读写实操
这里打算做一个测试 :
- ProcessA构造一个类对象, 内部有一个val, 显示自己的pid和对象地址后进入循环等待.
- ProcessB用进程pid和对象地址访问该对象并进行修改.
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 <iostream> using namespace std;
class MyClass { public: int val; MyClass(int v) : val(v) {} };
int main() { MyClass obj(12345); cout << "Process ID: " << GetCurrentProcessId() << endl; cout << "Object address: " << &obj << endl;
while (true) { Sleep(1000); } return 0; }
|
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
| #include <windows.h> #include <iostream> using namespace std;
int main() { DWORD pid; uintptr_t objAddr;
cout << "Enter target PID: "; cin >> pid; cout << "Enter target object address (hex, e.g., 0x12345678): "; cin >> std::hex >> objAddr;
HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, FALSE, pid); if (!hProcess) { cout << "Failed to open process, error: " << GetLastError() << endl; return -1; }
MEMORY_BASIC_INFORMATION mbi; if (VirtualQueryEx(hProcess, (LPCVOID)objAddr, &mbi, sizeof(mbi)) == 0) { cout << "VirtualQueryEx failed, error: " << GetLastError() << endl; CloseHandle(hProcess); return -1; }
if (mbi.State != MEM_COMMIT || (mbi.Protect & PAGE_NOACCESS)) { cout << "Memory not accessible" << endl; CloseHandle(hProcess); return -1; }
int val = 0; SIZE_T bytesRead; if (ReadProcessMemory(hProcess, (LPCVOID)objAddr, &val, sizeof(val), &bytesRead) && bytesRead == sizeof(val)) { cout << "Before write, val = " << val << endl; } else { cout << "ReadProcessMemory failed, error: " << GetLastError() << endl; CloseHandle(hProcess); return -1; }
int newVal = 999; SIZE_T bytesWritten; if (WriteProcessMemory(hProcess, (LPVOID)objAddr, &newVal, sizeof(newVal), &bytesWritten) && bytesWritten == sizeof(newVal)) { cout << "Successfully wrote val = " << newVal << endl; } else { cout << "WriteProcessMemory failed, error: " << GetLastError() << endl; CloseHandle(hProcess); return -1; }
if (ReadProcessMemory(hProcess, (LPCVOID)objAddr, &val, sizeof(val), &bytesRead) && bytesRead == sizeof(val)) { cout << "After write, val = " << val << endl; } else { cout << "ReadProcessMemory failed after write, error: " << GetLastError() << endl; }
CloseHandle(hProcess); return 0; }
|
实际效果如下 :

需要注意的是, 打开进程句柄时需要传入对应的权限标识, 不然无法使用对应的函数, 我这里就加入了读写和查询的权限.
跨进程内存读写可行性分析
实际上在进程运行时所有用户区堆内存/数据段几乎都是可读写的, 本质是因为页保护属性对所有进程一视同仁, 如果设置为不可读写, 那么设置方也无法读写, 这是操作系统内存设计的底层逻辑, 无法改变. 因此跨进程读写还是有可行性的.
不过许多应用也会有很多反制措施, 比如禁止未授权的进程获取本进程的句柄等等, 但似乎依旧有漏洞可钻, 这点貌似很深就不深入学习了.