周末趣谈:你的进程内存其实就是一个文件
文章背景与核心概要
本文探讨了 Unix 和 Linux 系统中关于“一切皆文件”哲学的实际应用与局限性,并重点介绍了通过 /proc/<pid>/mem 伪文件直接读写任意进程实时内存的精妙机制。作者通过一个展示“自我手术”的 C 语言示例,演示了如何利用标准的 pwrite 等文件操作无缝修改内存。此外,文章还重温并现代化改造了作者在 2002 年编写的经典工具 memfetch,使其能够在现代 64 位系统上将进程内存转储为原始二进制文件。
Weekend Trivia: Your Process' Memory is a File
Summary
While Unix systems don't strictly live up to the "everything is a file" philosophy, Linux offers a fascinating realization of this concept through the
/proc/<pid>/mempseudo-file. By using standard file operations likelseekandpwrite(orpread), a program can seamlessly read or modify the live memory of another process in real time. This article explores this elegant API, examines a C demonstration of self-surgery, and revisitsmemfetch—a retro utility updated for modern 64-bit systems to dump process memory into raw binary files.
Unix 的“一切皆文件”哲学
有人说,Unix 的设计哲学是“一切皆文件”。如果你熟悉类 Unix 平台,你可能知道它们并没有完全兑现这个宏大宣传。诚然,这些系统允许通过 /sys 或 /dev 等目录中的类文件对象来便捷地访问硬件。与此同时,许多操作系统功能并没有通过文件暴露出来;例如,如果不使用专用的系统调用,你就无法连接到远程 Web 服务器。
The Unix "Everything is a File" Philosophy
Some folks say that the design philosophy of Unix is that “everything is a file”. If you’re familiar with Unix-like platforms, you probably know that they don’t quite live up to the hype. It’s true that these systems allow convenient access to hardware through file-like objects in directories such as
/sysor/dev. At the same time, there’s plenty of OS functionality that isn’t exposed via files; for example, you can’t connect to a remote webserver without using a dedicated system call.
这大概是人们很希望做到的一点!一个名为 bash 的流行 Shell 带有变通方案:它对某些文件路径进行了特殊处理,从而让你能构造出下面这种 Shell 怪胎:
This is something people would probably have liked to do! A popular shell called bash comes with a workaround: it special-cases certain file paths, letting you construct the following shell monstrosity:
$ (echo -e 'GET / HTTP/1.0\nHost: coredump.cx\n' 1>&0; cat) </dev/tcp/coredump.cx/80
HTTP/1.1 301 Moved Permanently
Date: Mon, 01 Jun 2026 01:07:22 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
Location: https://coredump.cx/
...
话虽如此,/dev/tcp/<server>/<port> 这一招仅适用于由 Shell 自身打开的文件。如果你把这样的路径传递给任何其他程序,你将无法获得期望的结果:
That said, the
/dev/tcp/<server>/<port>trick works only for the files opened by the shell itself. If you pass such a path to any other program, you won’t get the expected result:
$ cat /dev/tcp/coredump.cx/80
cat: /dev/tcp/coredump.cx/80: No such file or directory
如果你对这种不一致性发牢骚,你可能会听到这样的解释:并非一切都是文件,而是“一切皆文件描述符(file descriptor)”。也就是说,你可能需要做一些特殊的事情来发起 TCP/IP 连接,但一旦完成,返回的连接标识符就具有类似文件的语义,并且可以传递给标准的文件 API,例如 read(…) 和 write(…)。
If you complain about this inconsistency, you might get told that not everything is a file; instead, “everything is a file descriptor”. That is, you might need to do something special to initiate a TCP/IP connection, but once this is done, the returned connection identifier has file-like semantics and can be passed to standard file APIs such as read(…) and write(…).
超越文件描述符:PID 与 /proc 文件系统
并非一切皆文件描述符!操作系统的某些部分使用独立的命名空间和 API;进程标识符(PID)就是一个很好的例子。你不能对一个进程标识符调用 read(…):PID 和文件描述符都只是整数,但它们使用相同的数字来引用不相干的事物。(最近的 Linux 内核引入了一个特殊的系统调用,允许你将 PID 转换为受限用途的文件描述符,但目前用它能做的事情还非常有限。)
Beyond File Descriptors: PIDs and the
/procFilesystemNot everything is a file descriptor! Some parts of the OS use separate namespaces and APIs; a good example are process identifiers (PIDs). You can’t call read(…) on a process identifier: both PIDs and file descriptors are just integers, but they use the same numbers to reference unrelated things. (Recent Linux kernels have a special system call that lets you convert a PID into a limited-use file descriptor, but there’s little you can currently do with that.)
乍一看,通过文件与 PID 交互的最接近方式是通过一个名为 /proc 的伪文件系统来窥探进程的元数据。可惜的是,这些数据在很大程度上似乎是只读的,而且并不是特别有趣。它们就是你在 ps 或 top 输出中看到的内容:
At first blush, the closest you can get to interacting with PIDs via files is to peek at process metadata via a pseudo-filesystem called
/proc. Alas, the data appears to be largely read-only and not particularly interesting. It’s the stuff you see in the output of ps or top:
$ cat /proc/self/status
Name: cat
Umask: 0077
State: R (running)
...
Pid: 29329
PPid: 17523
...
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
...
VmSize: 2420 kB
...
通过 /proc/self/mem 与进程内存交互
如果你曾在 Linux 上四处探索 /proc/<pid>/ 目录,你可能会注意到一个神秘的文件,它看起来似乎无法读取:
Interacting with Process Memory via
/proc/self/memIf you ever snooped around the
/proc/<pid>/directory on Linux, you might have noticed a mysterious file that seemingly can’t be read:
$ cat /proc/self/mem
cat: /proc/self/mem: I/O error
要想从这个“文件”中获取任何东西,你需要先 lseek(…) 到一个特定的偏移量,然后才能调用 read(…) 或 write(…);或者,你也可以在调用 pread(…) 或 pwrite(…) 时直接传入偏移量。如果你遵循这个步骤,你就可以实时无缝地获取或修改目标程序的内存。
To get anything out of that “file”, you need to lseek(…) to a specific offset before calling read(…) or write(…); alternatively, you can pass the offset when calling pread(…) or pwrite(…). If you follow that procedure, you can then seamlessly fetch or modify the memory of the target program in real time.
下面是它的实际工作方式——一个通过基于文件的接口执行表演性质的自我手术的程序(演示链接):
Here’s how it works in practice — a program that carries out performative self-surgery via the file-based interface (demo link):
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#define gcc_barrier() asm volatile ("" ::: "memory")
volatile int my_val = 0;
int main() {
/* Open the process' own memory file. */
int mem_fd = open("/proc/self/mem", O_RDWR);
if (mem_fd < 0) return 0;
/* Write '123' at the file offset associated with my_val. */
pwrite(mem_fd, &(int){123}, sizeof(int), (off_t)&my_val);
/* Display my_val, with compiler barrier to prevent reordering. */
gcc_barrier();
printf("my_val = %d\n", my_val);
}
大约 25 年前我第一次发现这个 API 时,我发现它异常优雅。标准的 Unix 调试接口 ptrace(…) 支持一对更为人熟知的 PTRACE_PEEKDATA 和 PTRACE_POKEDATA 方法,但 ptrace(…) 极其笨拙晦涩;相比之下,/proc/<pid>/mem 在其简洁性上堪称完美。
When I first discovered this API some 25 years ago, I found it to be remarkably elegant. The standard Unix debugging interface, ptrace(…), supports a pair of better-known
PTRACE_PEEKDATAandPTRACE_POKEDATAmethods, but ptrace(…) is incredibly janky; in contrast,/proc/<pid>/memis beautiful in its simplicity.
重生 memfetch
在 2002 年,我对这个 API 的迷恋促使我编写了一个非常简单的程序,名叫 memfetch;这个实用工具允许你对所选的任何进程内存进行非破坏性的“截图”,无论是为了满足好奇心、绕过反调试特性,还是从无响应的应用程序中恢复数据。这个周末,我把它重新翻了出来,并对代码进行了全面改造,使其能够在现代 64 位系统上运行:
Reviving
memfetchIn 2002, my fascination with this API prompted me to write a very simple program called memfetch; the utility allowed you to grab a non-destructive “screenshot” of the memory of any process of your choice, be it to satiate curiosity, to work around anti-debugging features, or to recover data from a non-responsive app. This weekend, I dug it up and overhauled the code to work on modern 64-bit systems:
$ ./memfetch 9742
memfetch 1.02 by Michal Zalewski <lcamtuf@coredump.cx>
[+] Attached to PID 9742 (/usr/bin/vim).
[*] Processing memory maps (index: mf-index.txt)...
- Skipping read-only section from 'vim' (224 kB).
- Skipping read-only section from 'vim' (1832 kB).
- Skipping read-only section from 'vim' (464 kB).
- Skipping read-only section from 'vim' (72 kB).
+ Dumping writable section from 'vim' (176 kB) to 'mf-map-005.bin'...
+ Dumping anonymous memory (56 kB) to 'mf-mem-006.bin'...
+ Dumping anonymous memory (4 kB) to 'mf-mem-007.bin'...
+ Dumping anonymous memory (40 kB) to 'mf-mem-008.bin'...
...
该程序为你提供了一组原始二进制文件,你可以对它们进行 grep 搜索、在编辑器中打开等等。你可以在此处下载源码。
The program gives you a collection of raw binary files that can be grepped, opened in an editor, and so forth. You can download the source here.
某些读者可能会觉得有趣的是,在 2000 年代初,Linux 2.2 允许你对生成的 /proc/<pid>/mem 文件描述符调用 mmap(…),从而将目标进程的内存镜像映射到你的地址空间。这简直好得令人难以置信:memfetch 的最初版本有时会因为页表(page tables)不同步而导致整个系统崩溃或挂起。不久之后,整个 mmap(…) 逻辑就被彻底删除了。
Some readers might find it amusing that in the early 2000s, Linux 2.2 allowed you to call mmap(…) on the resulting
/proc/<pid>/memfile descriptor, mirroring the memory of the target process to your address space. This was too good to be true: the initial version of memfetch would sometimes crash or hang the entire system due to page tables getting out of sync. Soon after, the entire mmap(…) logic was yanked out.更正:感谢 Jann Horn 向我指出了 pidfds 并发现了与
PTRACE_ATTACH相关的一个错误。我最初声称访问该文件是必需的,但实际上并非如此。Correction: Thanks to Jann Horn who pointed me to pidfds and spotted a mistake related to
PTRACE_ATTACH. I initially stated it’s necessary for accessing the file, but it isn’t.
如果你是一名软件工程师,你可能也会喜欢:
我撰写关于电子学、数学和其他内容的原创文章。如果你喜欢你所看到的,请订阅。
If you are a software engineer, you might also enjoy:
I write original articles about electronics, math, and other stuff. If you like what you see, please subscribe.