1.8 内存 Pt.6:通过操作系统扫描地址空间
1.8.1. 通过操作系统扫描地址空间(例子)
操作系统提供了接口可让程序发出请求——系统调用(system call)。在Windows里,KERNEL.DLL提供了用于检查和操纵运行进程内存的功能。
这个例子我们在Windows上进行。为什么以Windows为例呢?
- 函数命名易于理解
- 无需POSIX API的知识
1.8.2. 依赖项
本例使用了 windows-sys 这个 crate,它提供了对 Windows API 的低级绑定。在Cargo.toml中添加如下依赖:
[dependencies] windows-sys = { version = "0.59.0", features = [ "Win32_Foundation", "Win32_System_Memory", "Win32_System_ProcessStatus", "Win32_System_Threading", ] }- 这里我们使用
windows-sys来调用诸如GetCurrentProcess、K32GetProcessMemoryInfo和VirtualQueryEx等 Windows API。这些模块受 feature 控制,因此必须启用上面的 features。
1.8.3. 主程序
然后在main.rs把需要的东西引入作用域:
use std::ffi::c_void; use std::mem; use windows_sys::Win32::System::Memory::{VirtualQueryEx, MEMORY_BASIC_INFORMATION}; use windows_sys::Win32::System::ProcessStatus::{PROCESS_MEMORY_COUNTERS, K32GetProcessMemoryInfo}; use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetCurrentProcessId}; /// Windows API 中的 `PVOID` / `SIZE_T`。 /// 在 `windows-sys` 0.59+ 中,它们不再作为 `Win32::Foundation` 下的具名别名导出, /// 而是直接用原始 Rust 类型表示。 type PVOID = *mut c_void; type SIZE_T = usize;•PVOID:代表void*指针,用于表示不透明内存地址。这里是本地别名,对应*mut c_void。
•SIZE_T:对应无符号整型,用来表示内存区域大小。这里是本地别名,对应usize(与windows-sys0.59+ 的函数签名一致)。
•MEMORY_BASIC_INFORMATION:系统内定义的结构,用于描述一段内存区域的基本信息。
•PROCESS_MEMORY_COUNTERS:用于记录进程内存使用情况的结构体。
•K32GetProcessMemoryInfo:通过该函数获取当前进程的内存信息。
•GetCurrentProcess与GetCurrentProcessId:分别获取当前进程的句柄与进程 ID。
为了方便Debug格式的输出,我们将Windows API返回的PROCESS_MEMORY_COUNTERS包装在一个自定义的ProcessInfo结构体中:
#[derive(Debug)] struct ProcessInfo { cb: u32, page_fault_count: u32, peak_working_set_size: usize, working_set_size: usize, quota_peak_paged_pool_usage: usize, quota_paged_pool_usage: usize, quota_peak_non_paged_pool_usage: usize, quota_non_paged_pool_usage: usize, pagefile_usage: usize, peak_pagefile_usage: usize, }cb(u32):- 描述:结构体的大小(以字节为单位)。
用途:标识该结构体的大小,用于兼容性。
page_fault_count(u32):- 描述:进程自启动以来的页面错误总数。
用途:页面错误代表内存访问未命中物理内存而触发的处理过程。它包括软故障(从文件缓存中获取数据)和硬故障(需要从磁盘加载)。
peak_working_set_size(usize):- 描述:进程使用的工作集(实际驻留在物理内存中的内存)的峰值大小。
用途:用于监控进程的内存使用高峰。
working_set_size(usize):- 描述:进程当前使用的工作集大小。
用途:展示进程当前占用的物理内存量。
quota_peak_paged_pool_usage(usize):- 描述:进程使用的分页池(Paged Pool)配额的峰值大小。
用途:分页池是指可以分页到磁盘的内核模式内存。
quota_paged_pool_usage(usize)- 描述:进程当前使用的分页池配额大小。
用途:用于监控当前使用的可分页内核内存量。
quota_peak_non_paged_pool_usage(usize):- 描述:进程使用的非分页池(Non-paged Pool)配额的峰值大小。
用途:非分页池是指永久驻留在物理内存中的内核模式内存。
quota_non_paged_pool_usage(usize):- 描述:进程当前使用的非分页池配额大小。
用途:用于监控当前使用的不可分页内核内存量。
pagefile_usage(usize):- 描述:进程当前在页面文件中使用的空间大小。
用途:表示进程将数据分页到磁盘上的数量。
peak_pagefile_usage(usize)- 描述:进程使用的页面文件空间的峰值大小。
- 用途:用于监控进程的页面文件使用高水位标记。
windows-sys中的MEMORY_BASIC_INFORMATION没有实现Debug,因此我们也把它的字段包装起来以便打印:
#[derive(Debug)] struct MemoryBasicInfo { base_address: *mut c_void, allocation_base: *mut c_void, allocation_protect: u32, region_size: usize, state: u32, protect: u32, type_: u32, }获取当前进程句柄与进程ID(要放在unsafe块中):
let this_proc = GetCurrentProcess(); let this_pid = GetCurrentProcessId();获取进程内存信息:
let mut mem_counters: PROCESS_MEMORY_COUNTERS = mem::zeroed(); let mem_counters_size = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32; K32GetProcessMemoryInfo(this_proc, &mut mem_counters, mem_counters_size);let mut mem_counters: PROCESS_MEMORY_COUNTERS = mem::zeroed();
- 使用mem::zeroed()函数创建并初始化一个PROCESS_MEMORY_COUNTERS结构体,使其所有字段值都为零。
-PROCESS_MEMORY_COUNTERS是一个 Windows 系统预定义的结构体,用于存储进程的内存统计信息。let mem_counters_size = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
- 通过mem::size_of函数计算PROCESS_MEMORY_COUNTERS结构体的大小(单位为字节)。
- 该大小会被转换为u32类型,用以作为后续 API 函数调用的参数。K32GetProcessMemoryInfo(this_proc, &mut mem_counters, mem_counters_size);
-参数解释:this_proc:当前进程的句柄,表示你需要查询哪个进程的内存数据。&mut mem_counters:PROCESS_MEMORY_COUNTERS结构体的可变引用,用于接收从 API 返回的内存统计信息。mem_counters_size:传递结构体的大小,以确保 API 函数能够正确读取并填充结构体数据。- 作用:
调用K32GetProcessMemoryInfo函数,将当前进程的内存状态(如页面故障数量、工作集大小、页面文件使用量等信息)填充到mem_counters结构体中。
将内存信息包装成我们自定义的ProcessInfo:
let proc_info = ProcessInfo { cb: mem_counters.cb, page_fault_count: mem_counters.PageFaultCount, peak_working_set_size: mem_counters.PeakWorkingSetSize, working_set_size: mem_counters.WorkingSetSize, quota_peak_paged_pool_usage: mem_counters.QuotaPeakPagedPoolUsage, quota_paged_pool_usage: mem_counters.QuotaPagedPoolUsage, quota_peak_non_paged_pool_usage: mem_counters.QuotaPeakNonPagedPoolUsage, quota_non_paged_pool_usage: mem_counters.QuotaNonPagedPoolUsage, pagefile_usage: mem_counters.PagefileUsage, peak_pagefile_usage: mem_counters.PeakPagefileUsage, };定义扫描地址的起始与结束地址:
let min_addr: PVOID = 0 as PVOID;设定一个典型的 64 位用户模式地址空间上限:
let max_addr: PVOID = 0x00007FFF_FFFF_FFFF as PVOID;输出进程信息和地址区间:
println!("{:p} @ {:p}", this_pid as *const (), this_proc as *const ()); println!("{:?}", proc_info); println!("min: {:p}, max: {:p}", min_addr, max_addr);初始化VirtualQueryEx所需参数:
let MEMINFO_SIZE = mem::size_of::<MEMORY_BASIC_INFORMATION>(); let mut base_addr: PVOID = min_addr; let mut mem_info: MEMORY_BASIC_INFORMATION = mem::zeroed();let MEMINFO_SIZE = mem::size_of::<MEMORY_BASIC_INFORMATION>();
-作用:计算MEMORY_BASIC_INFORMATION结构体的大小(单位为字节)并存储在MEMINFO_SIZE变量中。
-原因:VirtualQueryEx函数要求提供这个结构体缓冲区的大小,以便正确写入查询结果。let mut base_addr: PVOID = min_addr;
-作用:初始化虚拟内存扫描的起始地址,将第一个扫描地址设置为min_addr。
-base_addr:表示当前查询的内存起始地址,会在后续循环中逐步增加,遍历整个虚拟地址空间。let mut mem_info: MEMORY_BASIC_INFORMATION = mem::zeroed();
-作用:使用mem::zeroed()函数创建并初始化一个MEMORY_BASIC_INFORMATION结构体,并将所有字段值置为零。
-原因:该结构体将用于存储VirtualQueryEx的结果,即当前查询地址对应内存区域的详细信息。
循环调用VirtualQueryEx扫描整个地址空间:
loop { let rc: SIZE_T = VirtualQueryEx(this_proc, base_addr, &mut mem_info, MEMINFO_SIZE as SIZE_T); if rc == 0 { break; } // `windows-sys` 中的 `MEMORY_BASIC_INFORMATION` 没有实现 `Debug`, // 因此包装我们关心的字段再打印。 let printable = MemoryBasicInfo { base_address: mem_info.BaseAddress, allocation_base: mem_info.AllocationBase, allocation_protect: mem_info.AllocationProtect, region_size: mem_info.RegionSize, state: mem_info.State, protect: mem_info.Protect, type_: mem_info.Type, }; println!("{:#?}", printable); // 累加当前区域的大小,得到下一个查询地址 base_addr = ((base_addr as usize) + mem_info.RegionSize) as PVOID; if (base_addr as usize) >= (max_addr as usize) { break; } }1.8.4. 完整代码
main.rs:
use std::ffi::c_void; use std::mem; use windows_sys::Win32::System::Memory::{VirtualQueryEx, MEMORY_BASIC_INFORMATION}; use windows_sys::Win32::System::ProcessStatus::{PROCESS_MEMORY_COUNTERS, K32GetProcessMemoryInfo}; use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetCurrentProcessId}; /// Windows API 中的 `PVOID` / `SIZE_T`。 /// 在 `windows-sys` 0.59+ 中,它们不再作为 `Win32::Foundation` 下的具名别名导出, /// 而是直接用原始 Rust 类型表示。 type PVOID = *mut c_void; type SIZE_T = usize; /// 为了能够用 Debug 格式输出,我们自己包装了 PROCESS_MEMORY_COUNTERS。 #[derive(Debug)] struct ProcessInfo { cb: u32, page_fault_count: u32, peak_working_set_size: usize, working_set_size: usize, quota_peak_paged_pool_usage: usize, quota_paged_pool_usage: usize, quota_peak_non_paged_pool_usage: usize, quota_non_paged_pool_usage: usize, pagefile_usage: usize, peak_pagefile_usage: usize, } /// `windows-sys` 中的 `MEMORY_BASIC_INFORMATION` 没有实现 `Debug`。 #[derive(Debug)] struct MemoryBasicInfo { base_address: *mut c_void, allocation_base: *mut c_void, allocation_protect: u32, region_size: usize, state: u32, protect: u32, type_: u32, } fn main() { unsafe { // 获取当前进程句柄与进程ID let this_proc = GetCurrentProcess(); let this_pid = GetCurrentProcessId(); // 获取进程内存信息 let mut mem_counters: PROCESS_MEMORY_COUNTERS = mem::zeroed(); let mem_counters_size = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32; K32GetProcessMemoryInfo(this_proc, &mut mem_counters, mem_counters_size); // 将内存信息包装成我们自定义的 ProcessInfo let proc_info = ProcessInfo { cb: mem_counters.cb, page_fault_count: mem_counters.PageFaultCount, peak_working_set_size: mem_counters.PeakWorkingSetSize, working_set_size: mem_counters.WorkingSetSize, quota_peak_paged_pool_usage: mem_counters.QuotaPeakPagedPoolUsage, quota_paged_pool_usage: mem_counters.QuotaPagedPoolUsage, quota_peak_non_paged_pool_usage: mem_counters.QuotaPeakNonPagedPoolUsage, quota_non_paged_pool_usage: mem_counters.QuotaNonPagedPoolUsage, pagefile_usage: mem_counters.PagefileUsage, peak_pagefile_usage: mem_counters.PeakPagefileUsage, }; // 定义扫描地址的起始与结束地址 let min_addr: PVOID = 0 as PVOID; // 这里设定一个典型的 64 位用户模式地址空间上限 let max_addr: PVOID = 0x00007FFF_FFFF_FFFF as PVOID; // 输出 println!("{:p} @ {:p}", this_pid as *const (), this_proc as *const ()); println!("{:?}", proc_info); println!("min: {:p}, max: {:p}", min_addr, max_addr); // 初始化 VirtualQueryEx 所需参数 let MEMINFO_SIZE = mem::size_of::<MEMORY_BASIC_INFORMATION>(); let mut base_addr: PVOID = min_addr; let mut mem_info: MEMORY_BASIC_INFORMATION = mem::zeroed(); // 循环调用 VirtualQueryEx 扫描整个地址空间 loop { let rc: SIZE_T = VirtualQueryEx(this_proc, base_addr, &mut mem_info, MEMINFO_SIZE as SIZE_T); if rc == 0 { break; } let printable = MemoryBasicInfo { base_address: mem_info.BaseAddress, allocation_base: mem_info.AllocationBase, allocation_protect: mem_info.AllocationProtect, region_size: mem_info.RegionSize, state: mem_info.State, protect: mem_info.Protect, type_: mem_info.Type, }; println!("{:#?}", printable); // 累加当前区域的大小,得到下一个查询地址 base_addr = ((base_addr as usize) + mem_info.RegionSize) as PVOID; if (base_addr as usize) >= (max_addr as usize) { break; } } } }Cargo.toml:
[package] name = "RustStudy" version = "0.1.0" edition = "2021" [dependencies] windows-sys = { version = "0.59.0", features = [ "Win32_Foundation", "Win32_System_Memory", "Win32_System_ProcessStatus", "Win32_System_Threading", ] }1.8.5. 读取和写入进程内存的步骤
读取和写入进程内存的逻辑还算简单,伪代码如下:
let pid = some_process_id; OpenProcess(pid); loop 地址空间 { 调用VirtualQueryEx()来访问下个内存块 通过调用ReadProcessMemory()来访问内存块 寻找某种特定的模式 使用所需的值调用WriteProcessMemory }let pid = some_process_id;:获取当前进程的idOpenProcess(pid);:打开这个进程
Linux提供了简单的API:process_vm_readv()、process-vm_writev(),对应Windows中的ReadProcessMemory()、WriteProcessMemory()