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来调用诸如GetCurrentProcessK32GetProcessMemoryInfoVirtualQueryEx等 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-sys 0.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);
  1. let mut mem_counters: PROCESS_MEMORY_COUNTERS = mem::zeroed();
    - 使用 mem::zeroed() 函数创建并初始化一个 PROCESS_MEMORY_COUNTERS 结构体,使其所有字段值都为零。
    - PROCESS_MEMORY_COUNTERS 是一个 Windows 系统预定义的结构体,用于存储进程的内存统计信息。

  2. let mem_counters_size = mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
    - 通过 mem::size_of 函数计算 PROCESS_MEMORY_COUNTERS 结构体的大小(单位为字节)。
    - 该大小会被转换为 u32 类型,用以作为后续 API 函数调用的参数。

  3. K32GetProcessMemoryInfo(this_proc, &mut mem_counters, mem_counters_size);
    - 参数解释:

    • this_proc:当前进程的句柄,表示你需要查询哪个进程的内存数据。
    • &mut mem_countersPROCESS_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();
  1. let MEMINFO_SIZE = mem::size_of::<MEMORY_BASIC_INFORMATION>();
    - 作用:计算 MEMORY_BASIC_INFORMATION 结构体的大小(单位为字节)并存储在 MEMINFO_SIZE 变量中。
    - 原因VirtualQueryEx 函数要求提供这个结构体缓冲区的大小,以便正确写入查询结果。

  2. let mut base_addr: PVOID = min_addr;
    - 作用:初始化虚拟内存扫描的起始地址,将第一个扫描地址设置为 min_addr
    - base_addr:表示当前查询的内存起始地址,会在后续循环中逐步增加,遍历整个虚拟地址空间。

  3. 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;:获取当前进程的id
  • OpenProcess(pid);:打开这个进程

Linux提供了简单的API:process_vm_readv()process-vm_writev(),对应Windows中的ReadProcessMemory()WriteProcessMemory()

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐