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_countersPROCESS_MEMORY_COUNTERS结构体的可变引用用于接收从 API 返回的内存统计信息。mem_counters_size传递结构体的大小以确保 API 函数能够正确读取并填充结构体数据。作用调用K32GetProcessMemoryInfo函数将当前进程的内存状态如页面故障数量、工作集大小、页面文件使用量等信息填充到mem_counters结构体中。将内存信息包装成我们自定义的ProcessInfolet 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提供了简单的APIprocess_vm_readv()、process-vm_writev()对应Windows中的ReadProcessMemory()、WriteProcessMemory()