我们希望 “模拟” 一个操作系统:crazy-os.c 能加载 argv 指定的 binary (p1, p2, …),基于 mini-rv32ima.h 模拟单步执行 (RISC-V 指令),并能够处理 syscall。
编写一个 C 程序,用指针指向不同的位置:代码、数据、栈,打印地址,并从中读出一些数据。并且验证指向 main 指针读取到的数据确实是 main 的二进制代码。
我在课堂上学习了 Linux 进程的地址空间。请你帮我做一个命令行工具,从命令行接收一个 pid,解析它的地址空间,输出每个部分是什么,如果是可执行的区域,把第一个 KB 的内容反汇编 (要兼容 x86-64 和 arm64)。
Linux 系统使用 anonymous 的 mmap 来实现大段连续内存的分配——甚至在系统调用返回的瞬间,进程可以没有得到任何实际的内存,而是只要在首次访问时 (触发缺页异常) 分配即可。AddressSanitizer 就用 mmap 分配了 shadow memory,我们可以使用 strace 观察到这一点。
通过操作系统赋予我们实现调试器的机制,我们可以窥探甚至修改任何进程的代码和数据。这个能力使我们可以绕过游戏为我们设置的 “人为障碍”,取得更多的金钱、经验,或是锁定生命值等。
汉诺塔是递归和分治的经典问题,而同学们也曾经在理解这个程序的时候遇到困难。遇到困难是正常的:C/C++ 中的 “函数” 和数学的函数很不一样,例如我们可以把 Fibonacci 数列的递归写成
int x = f(n - 1);
int y = f(n - 2);
// 也可以 return y + x;
return x + y;
或是任意调换函数调用的次序,但汉诺塔不行。
不行的根本原因在于汉诺塔中的 printf 会带来全局的副作用。但 C/C++ 遵循 “顺序执行” 的原则,函数的执行有 “先后” (不像数学的函数,先后是无关的),按照不同顺序调用会导致程序输出不同的结果。实际上,C 标准中 return f(n - 1) + f(n - 2); 甚至不保证从左到右的调用顺序。(但现代编译器为了防止产生难以理解的执行,通常按照自然顺序调用、不做激进优化。)
操作系统通过虚拟内存为每个进程提供独立的地址空间,实现了进程间的隔离和保护。操作系统通过 mmap/munmap 实现地址空间的管理,并且还提供特定机制 (如 procfs、ptrace、process_vm_writev、共享内存等) 访问彼此的地址空间。
教科书 Operating Systems: Three Easy Pieces: