写在前面:这是本系列的第六篇。

在上一讲中,我们学习了如何用 forkexecve 创造和洗脑一个进程。但是,当一个新进程苏醒时,它的脑子(内存)里到底有什么?

今天,我们将深入探索进程的地址空间。不仅要学习正规军的 mmap 内存映射魔法,还要化身黑客,看看如何利用状态机的弱点,“合法入侵”并篡改其他进程的内存(比如做个游戏外挂)。

在这里插入图片描述

课前思考与回顾

关于进程管理的延续思考:

  • 深度强化学习 (Reinforcement learning) 与大模型的崛起正在改变编程,但操作系统依然很重要
  • 编程语言(Markdown, JS, Python 等)是“可信可验证”的桥梁。
  • 操作系统是运行支撑(提供虚拟环境、文件系统快照、网络等)。
  • 程序必须要到一个真实的环境中运行,大语言模型终究会产生幻觉,而操作系统底层的隔离机制(如 Docker)能确保我们在试错时不会把系统搞崩溃。

状态机的生命周期管理 API:

  • fork, execve_exit
  • 操作系统视角的本质:状态机的复制、重置和删除。
int pid = fork();
if (pid == -1) { // 错误
    perror("fork"); goto fail;
} else if (pid == 0) { // 子进程
    execve(...);
    perror("execve"); exit(EXIT_FAILURE);
} else { // 父进程
    ...
    int status;
    waitpid(pid, &status, 0); // 等待状态机结束
}

进程的初始状态

当我们调用 execve 创建(重置)一个进程后,它的初始状态是怎样的?更深层次的理解需要我们去窥探它的寄存器和内存。

对于内存,有点难办,因为我们可以把任何整数强转成指针去访问,但进程空间里的绝大部分区域是没有访问权限的(Segmentation Fault)

那么,到底哪些可以访问???计算机系统里没有黑魔法,所有一切都是有答案的。

结论:进程 execve 后的初始状态

  • ABI 中规定的 initial state:
    在 System V ABI 手册的 Section 3.4 “Process Initialization” 中,只规定了部分寄存器和栈的初始状态(比如 argvenvp 中的字符串保存在栈中)。
  • Binary 中指定的 PT_LOAD 段:
    可执行文件(ELF)里声明了内存是分成“一段一段”的,每一段都有严格的访问权限 (r-读, w-写, x-执行)。

观测进程的地址空间

配合 gdb 调试器,我们甚至可以“随时查看程序的地址空间”。(这些代码也是 AI 生成的,AI 是帮助我们理解复杂细节的利器)。

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec6/address-space# ./alloc
mmap: 7fd2c4e88000
Read get: 2
Read get: 0
Read get: 3

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec6/address-space# ./mmap-demo
This is allocated memory at 0x7fe150f8a000
Mapped executable at 0x7fe150ec0000, size: 823704 bytes
First 16 bytes of executable: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00

root@LAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec6/address-space# ./simple


进程的地址空间管理

往后想一想

进程的初始状态,只有 ELF 文件里声明的内存和一些操作系统分配的栈内存。任何其他指针的访问都是非法的。

那么问题来了:如果我们从输入读了一个非常大的 size,然后调用 malloc(size),这些新内存是从哪里来的呢?

结论:一定有一个“系统调用”可以改变进程的地址空间!

Memory Map 系统调用 (mmap)

如果你来设计操作系统的 API,你会怎么设计?UNIX 给出的终极答案是 mmap。它可以在状态机上增加、删除或修改一段可访问的内存。

// 映射
void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset);

// 解除映射
int munmap(void *addr, size_t length);

// 修改映射权限
int mprotect(void *addr, size_t length, int prot);

  • MAP_ANONYMOUS (匿名映射): 不需要文件,直接向操作系统伸手要一块纯粹的物理内存(这就是大块 malloc 的底层原理)。
  • 文件映射 (fd): 把文件直接“搬到”进程地址空间中,不需要 readwrite,直接用指针像读写数组一样读写文件!(加载器底层就是这么干的)。

我们可以用 pmap 命令查看进程的地址空间。

优雅使用 mmap 的两个例子

Example 1: 申请大量内存空间

  • 瞬间完成内存分配。
  • mmap/munmapmalloc/free 提供了底层机制。libc 的 malloc 在申请大块内存时,会直接调用一次 mmap 实现。
  • 底层原理: mmap 申请内存时,操作系统只是在页表里做个记录,并不会立即分配真实的物理内存。只有当你的代码真的一刀劈下去(发生缺页中断访问它时),物理内存才会被分配,因此申请极快。

Example 2: Everything is a file (万物皆文件)

  • 映射几个 G 的大文件,但只访问其中的一小部分。
# Python 中的 mmap 演示
import mmap
import hexdump

# 打开物理磁盘设备文件
with open('/dev/sda', 'rb') as fp:
    # 映射文件 (128 GB 虚拟空间)
    mm = mmap.mmap(fp.fileno(),
                   prot=mmap.PROT_READ, length=128 << 30)
    # 打印前 512 字节 (MBR 扇区)
    hexdump.hexdump(mm[:512])


入侵进程的地址空间 (Hacking Address Space)

进程在“无情执行指令的机器”上执行,本来是一个封闭世界。

但如果允许一个进程对另一个进程的地址空间有访问权呢?
这意味着我们可以任意改变另一个程序的行为!听起来就很 cool。

  • 例子:我们可以改变 gdb 或 其他编译程序的内部代码。
  • 这不仅可以用来调试,还能让你获得“开挂”的权利。

物理入侵进程地址空间

金手指:直接物理劫持内存

听起来很离谱,但在古早的“卡带机”时代,这的确是可以做到的!

  • Game Genie (游戏金手指): 本质上是一个硬件级别的 Look-up Table (LUT)。
  • 它的逻辑简单且优雅:串联在卡带和主机之间,当 CPU 想要读地址 a,原本应该读出数值 x 时,它直接在物理引脚上拦截,替换为我们要修改的数值 y(比如无限命、无敌)。

在现代系统中,虽然不能轻易拔插引脚,但 CPU 提供了特定的硬件支持:

  • Debug Registers (调试寄存器): CPU 提供的一种机制,用于支持硬件断点。
  • Intel Processor Trace: 记录程序的执行路径和内存访问情况。这些机制帮助系统工具(如 gdb)“合法”入侵地址空间。

内存扫描:外挂的软件演进

随着游戏越来越大,地址空间动辄几十个 GB,你根本分不清哪些地址存的是玩家的生命值,哪些是怪物的属性,更何况包含大量动态分配 (malloc) 的内存,每次重启游戏地址都不一样!

解决思路:Everything is a state machine (一切皆状态机)

我们只需要观察状态机的变化轨迹 (Trace):

  1. 查找 + Filter (Cheat Engine 原理):
  2. 进入游戏时,经验值 exp = 4950,在几十 GB 内存中搜索所有值为 4950 的地址(可能有几十万个)。
  3. 去打个怪,此时 exp = 5100
  4. 在刚才找到的那些地址中,再次筛选出当前值变为 5100 的地址。
  5. 符合 4950 -> 5100 连续变化的内存地址,通常只剩下个位数。
  6. 锁定地址,把里面的值改成 99999999,好了,出门就是满级了!

极客精神的启示: 只要你认为一件事情符合逻辑法则,这件事情就可以干下去。哪怕网上没有先例、没有教程,AI 会辅佐你完成这一切!

降维打击:物理级“外挂”

反作弊系统越来越强,读写别人内存的行为会被检测封号怎么办?

用魔法打败魔法:跳出操作系统!

  • 采集视频信号: 用视频采集卡 (如 MS2130) + 树莓派,直接从物理线缆上截取屏幕画面。
  • 用 AI 跑图像识别(如 YOLO),分析出敌人的头部坐标。
  • 用单片机模拟一个物理的 USB 鼠标,向电脑发送移动指令(自动瞄准)。
  • 进阶: 甚至可以用 FPGA 设计一个电路,直接在硬件底层把 bounding-box(瞄准框)绘制到视频流上输出到显示器,连主机操作系统都毫无察觉。

创造变得前所未有的容易。发挥你的想象力,一旦思路打开,也许就会改变人类的命运!

Logo

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

更多推荐