南京大学 操作系统 (JYY) 学习笔记:进程地址空间与内存“外挂”魔法 写在前面这是本系列的第六篇。在上一讲中我们学习了如何用fork和execve创造和洗脑一个进程。但是当一个新进程苏醒时它的脑子内存里到底有什么今天我们将深入探索进程的地址空间。不仅要学习正规军的mmap内存映射魔法还要化身黑客看看如何利用状态机的弱点“合法入侵”并篡改其他进程的内存比如做个游戏外挂。课前思考与回顾关于进程管理的延续思考深度强化学习 (Reinforcement learning) 与大模型的崛起正在改变编程但操作系统依然很重要。编程语言Markdown, JS, Python 等是“可信可验证”的桥梁。操作系统是运行支撑提供虚拟环境、文件系统快照、网络等。程序必须要到一个真实的环境中运行大语言模型终究会产生幻觉而操作系统底层的隔离机制如 Docker能确保我们在试错时不会把系统搞崩溃。状态机的生命周期管理 APIfork,execve和_exit操作系统视角的本质状态机的复制、重置和删除。intpidfork();if(pid-1){// 错误perror(fork);gotofail;}elseif(pid0){// 子进程execve(...);perror(execve);exit(EXIT_FAILURE);}else{// 父进程...intstatus;waitpid(pid,status,0);// 等待状态机结束}进程的初始状态当我们调用execve创建重置一个进程后它的初始状态是怎样的更深层次的理解需要我们去窥探它的寄存器和内存。对于内存有点难办因为我们可以把任何整数强转成指针去访问但进程空间里的绝大部分区域是没有访问权限的Segmentation Fault。那么到底哪些可以访问计算机系统里没有黑魔法所有一切都是有答案的。结论进程 execve 后的初始状态ABI 中规定的 initial state:在 System V ABI 手册的 Section 3.4 “Process Initialization” 中只规定了部分寄存器和栈的初始状态比如argv和envp中的字符串保存在栈中。Binary 中指定的 PT_LOAD 段:可执行文件ELF里声明了内存是分成“一段一段”的每一段都有严格的访问权限 (r-读, w-写, x-执行)。观测进程的地址空间配合 gdb 调试器我们甚至可以“随时查看程序的地址空间”。这些代码也是 AI 生成的AI 是帮助我们理解复杂细节的利器。rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec6/address-space# ./allocmmap: 7fd2c4e88000 Read get:2Read get:0Read get:3rootLAPTOP-GT06V0GS:/mnt/d/CSLab/osCourse/lec6/address-space# ./mmap-demoThis is allocated memory at 0x7fe150f8a000 Mapped executable at 0x7fe150ec0000, size:823704bytes First16bytes of executable: 7f454c4602 01 01 03 00 00 00 00 00 00 00 00 rootLAPTOP-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,intprot,intflags,intfd,off_t offset);// 解除映射intmunmap(void*addr,size_t length);// 修改映射权限intmprotect(void*addr,size_t length,intprot);MAP_ANONYMOUS (匿名映射):不需要文件直接向操作系统伸手要一块纯粹的物理内存这就是大块malloc的底层原理。文件映射 (fd):把文件直接“搬到”进程地址空间中不需要read和write直接用指针像读写数组一样读写文件加载器底层就是这么干的。我们可以用pmap命令查看进程的地址空间。优雅使用 mmap 的两个例子Example 1: 申请大量内存空间瞬间完成内存分配。mmap/munmap为malloc/free提供了底层机制。libc 的malloc在申请大块内存时会直接调用一次mmap实现。底层原理mmap申请内存时操作系统只是在页表里做个记录并不会立即分配真实的物理内存。只有当你的代码真的一刀劈下去发生缺页中断访问它时物理内存才会被分配因此申请极快。Example 2: Everything is a file (万物皆文件)映射几个 G 的大文件但只访问其中的一小部分。# Python 中的 mmap 演示importmmapimporthexdump# 打开物理磁盘设备文件withopen(/dev/sda,rb)asfp:# 映射文件 (128 GB 虚拟空间)mmmmap.mmap(fp.fileno(),protmmap.PROT_READ,length12830)# 打印前 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)查找 Filter (Cheat Engine 原理):进入游戏时经验值exp 4950在几十 GB 内存中搜索所有值为 4950 的地址可能有几十万个。去打个怪此时exp 5100。在刚才找到的那些地址中再次筛选出当前值变为5100的地址。符合4950 - 5100连续变化的内存地址通常只剩下个位数。锁定地址把里面的值改成99999999好了出门就是满级了极客精神的启示只要你认为一件事情符合逻辑法则这件事情就可以干下去。哪怕网上没有先例、没有教程AI 会辅佐你完成这一切降维打击物理级“外挂”反作弊系统越来越强读写别人内存的行为会被检测封号怎么办用魔法打败魔法跳出操作系统采集视频信号用视频采集卡 (如 MS2130) 树莓派直接从物理线缆上截取屏幕画面。用 AI 跑图像识别如 YOLO分析出敌人的头部坐标。用单片机模拟一个物理的 USB 鼠标向电脑发送移动指令自动瞄准。进阶甚至可以用 FPGA 设计一个电路直接在硬件底层把 bounding-box瞄准框绘制到视频流上输出到显示器连主机操作系统都毫无察觉。创造变得前所未有的容易。发挥你的想象力一旦思路打开也许就会改变人类的命运