编程语言研究书库

加载与运行应用 — 用分页给每个应用发一把「专属地址空间」

加载与运行应用 — 用分页给每个应用发一把「专属地址空间」

这一章讲什么: rpn 链接标准库后算错答案,本章从反汇编里破案: 可执行文件的机器语言里写死了绝对地址(字符串 "+" 住在 0x162), 而文件实际被加载到别处——「只调入口点」救不了满地的 0x162。 三种修法里选了分页:给每个应用备一套自己的地址翻译表, 大家的应用都从 0xffff800000000000 起跑,互不打架。分页是第 8 章埋下的伏笔, 本章是它第一次真正发力,也是全书理论密度最高的一章。

1. 先看现象:一个 0x162 引发的冤案

主线例子:

  $ objdump -d rpn 看到:mov esi, 0x162   ← strcmp(argv[i], "+") 的第二个参数
  hexdump -s 0x160 -n 4 rpn → "-.+."     ← 0x160/0x162 处真的存着 "-\0" "+\0"
  链接时 --image-base 0:链接器假定文件放在内存 0 处,字符串地址 = 文件内偏移
  MikanOS 把文件读进 file_buf(地址 ≠ 0)→ 程序拿 0x162 当指针 → 指向一堆垃圾
  自己写 strcmp/atol 的上版本碰巧没嵌绝对地址,所以是对的

  修复:分页。应用的虚拟地址从 0xffff800000000000 起(地址空间后半),
  内核按 ELF 程序头建一套专属页表,把 LOAD 段放到物理内存,
  应用里的 mov esi, 0x162 依旧,但 CPU 把它翻译到正确的物理位置 —— rpn 算出 5

2. 顶层全景

  19.1 案发:机器语言里嵌着绝对地址;反汇编 + hexdump 取证
  19.2 三种修法:定死地址(换机重链)/ 重定位(难,且是 ASLR 的技术)/
       分页(每应用一张转换表)← 选它;「虚拟地址」正名(= 事实上的地址)
  19.3–19.4 四级分页解剖:虚拟地址六段(9+9+9+9+12);
       一次完整翻译走查(mov [0xc0200042], rax);
       规范地址(位 63:48 必须与位 47 一致)→ OS 低半、应用高半的格局
  19.5 应用挪到高半:--image-base 0xffff800000000000 + -mcmodel=large
  19.6–19.7 LoadELF:SetupPageMaps()(递归 SetupPageMap,缺层就建)+ CopyLoadSegments()
  19.8 清理:CleanPageMaps() 递归释放;LoadFile() 收拢簇链读取;跑通

  图说:一个 bug 的破案之旅,顺便把 x86-64 分页从零讲到能写。

3. 核心原理

3.1 破案:绝对地址与「调入口点不够」

链接器必须假定一个加载地址;rpn 用 --image-base 0 假定放在 0, 于是字符串字面量的地址 = 文件内偏移,被直接编进机器指令(0x162)。 运行时文件在别处,「0x162」这个数不会跟着搬——调对入口点也无济于事, 满程序的绝对地址全部指向虚空1。 顺带一个知识:nasm 生成的 eb fd 是相对短跳转(-3,跳回上一条 hlt), 位置无关——所以 3 字节汇编程序没踩这个坑,换了 C++ 就踩了2。

3.2 三种修法,为什么是分页

修法做法为什么不选/选
定死地址链接时就定死应用加载地址嵌入式可行;PC 空闲内存因机而异,换机器要重新链接3
重定位加载后修改文件内的地址值过程难;ASLR(地址空间布局随机化)用的就是它4
分页每应用一套地址转换表,相同虚拟地址映射到不同物理地址选了它:应用互不见、也不碰 OS;机器语言可以随便嵌地址5

虚拟地址这个词值得按作者的解释记:「应用为 mov 指定的地址是它的 事实上的地址——不是 CPU 外部的真实地址,但对应用而言千真万确」6。 执行翻译的硬件叫 MMU(内建于 CPU,还管权限检查)7;「映射(map)」就是 「一个数 → 另一个数」的对应关系(同 std::map 的语义)8。

3.3 四级分页:一次翻译的完整走查

x86-64 四级分页把虚拟地址切成六段:9|9|9|9|12 位; 前四段分别是 PML4、PDP、PD、PT 四层数组(每层 512 项)的下标, 最后 12 位是页内偏移(4 KiB 页)9。 一次翻译(mov [0xc0200042], rax,CR3 = A):

  位[47:39] = 0  → 读 PML4[0]  → 得 PDP 表地址 B
  位[38:30] = 3  → 读 B[3]      → 得 PD 表地址 C
  位[29:21] = 1  → 读 C[1]      → 得 PT 表地址 D
  位[20:12] = 0  → 读 D[0]      → 得物理帧起始 E
  物理地址 = E + 0x42

每项条目的 addr 字段是「物理地址右移 12 位」存的(40 位), PML4[i] & 0x000ffffffffff000 得下一层表的真实地址10。 规范地址:位[63:48] 必须与位 47 全同(全 0 或全 1), 否则内存访问触发 CPU 异常——64 位地址空间实际只有两端的「小块」可用, 自然的分法就是 OS 放低半、应用放高半(主流 OS 恰好相反,脚注给了猜测理由)11。

3.4 应用住高半:两个编译开关

应用重链到 --image-base 0xffff800000000000; 超过 32 位的地址要求 -mcmodel=large(让编译器把函数/全局变量地址输出成 64 位)12。 readelf -l 看 rpn:三个 LOAD 段(R / RE / RW)的虚拟地址全部落在 0xffff800000000000 起, 入口点 0xffff800000001060——内核的任务就是照单建表、搬段13。

3.5 LoadELF:递归造表 + 搬段 + 退出清理

加载流程的骨架14:

  1. SetupPageMaps(dest, num_4kpages):从 CR3 拿到当前 PML4,交给递归的 SetupPageMap;
  2. SetupPageMap(page_map, level, addr, num_pages) 是核心: 每层都是「512 个 64 位条目的数组」,取 addr 对应层的 9 位当 entry_index; 条目 present=0 就 NewPageMap() 建一层新表挂上(SetNewPageMapIfNotPresent); 不是最底层就递归进下一层;返回值是「没处理完的页数」, 循环里 addr 做「进位」(高层字段 +1、低层字段清零)逐个条目推进15;
  3. CopyLoadSegments():遍历程序头,LOAD 段先建表后 memcpy—— 顺序不能反:身份映射只覆盖前 64 GiB,不写建表就复制,立刻页面故障16;
  4. 跑:入口点地址不用再调整(分页把它安置对了),调用,收退出码;
  5. CleanPageMaps() 清理:递归释放建出来的所有表(教学约定:PML4 只用一个高半条目)。

present 位的意义要记牢:0 = 无效条目,访问它指向的范围直接 页面故障—— 「段错误」的硬件面孔17。

4. 作者的判断与证据

判断:用破案的方式讲分页。 不先给定义,先给一个「明明链接对了却算错」的谜, 反汇编、hexdump、对答案,三种修法并列比较——分页是被问题逼出来的,不是被定义领出来的。

判断:「虚拟」应理解为「事实上的」。 专栏 19.1 专门正名:virtual address 是应用 千真万确的地址,只是不是物理地址。这个语感对后面所有内存章节都管用。

证据: rpn 重试后输出正确结果(图 19.8)——「应用在高半虚拟地址正常运行,分页成功」; 翻译走查的每一步都可对照 Intel SDM 复核。

5. 边界与局限

  • 每个应用跑完就建表/拆表,没有页表缓存与复用;应用多了这是可观的开销 (第 27 章的按需分页才是正经营生);
  • CleanPageMaps 假定 PML4 只有一个高半条目——教学约定,不是一般解;
  • RW 段的 .bss 式扩展沿用第 4 章的段语义,未展开;
  • 身份映射仍覆盖前 64 GiB:OS 的地址 = 物理地址,第 27 章之前一直如此。

6. 可带走的

本章主线例子,一行写完: 0x162 破案 → 分页(每应用一套翻译表)→ 应用高半 0xffff800000000000 → 递归 SetupPageMap 建表 + CopyLoadSegments 搬段 → rpn 输出 5。

  1. 可执行文件里嵌着绝对地址;--image-base 决定了它们是多少,「调对入口」救不了它们;
  2. 位置无关代码(相对跳转)不嵌地址,所以汇编玩具不踩坑、C++ 必踩;
  3. 三种修法:定死地址(不可移植)、重定位(难,ASLR 的技术)、分页(每应用一套表);
  4. 四级分页 = 9|9|9|9|12;翻译就是四级查表 + 页内偏移;条目地址 = 物理地址右移 12 位;
  5. 规范地址:位 63:48 跟位 47 走;OS 低半、应用高半;
  6. present=0 的条目被访问 = 页面故障;
  7. 加载应用 = 先建表、后搬段(顺序反了立刻故障);退出要清理页表;
  8. 大地址应用要 -mcmodel=large。

7. 原文地图

主题原书章原文位置
0x162 破案19.1 可执行文件和内存地址text/119-ch19-01-19-1.txt:4(搜「调整地址」) · text/120-ch19-02-19-2.txt:5(搜「mov esi,0x162」) · text/120-ch19-02-19-2.txt:20(搜「hexdump」)
三种修法19.2 地址转换text/120-ch19-02-19-2.txt:36(搜「预先确定」) · text/120-ch19-02-19-2.txt:44(搜「重新定位」) · text/120-ch19-02-19-2.txt:49(搜「我们为每个应用准备了专用的地」)
虚拟地址正名19.2 地址转换text/121-ch19-03-19-3-osbook-day19a.txt:3(搜「事实上」;text/120-ch19-02-19-2.txt:91,搜「事实上的地址」)
MMU 与映射语义19.2 地址转换text/120-ch19-02-19-2.txt:97(搜「Memory Management Unit」;text/120-ch19-02-19-2.txt:62,搜「std::map」)
四级地址结构与走查19.3 加载并运行应用(osbook_day19a)text/121-ch19-03-19-3-osbook-day19a.txt:19(搜「六部分」) · text/121-ch19-03-19-3-osbook-day19a.txt:49(搜「0xc0200042」)
规范地址与两半场19.3 加载并运行应用(osbook_day19a)text/121-ch19-03-19-3-osbook-day19a.txt:66(搜「规范地址」) · text/121-ch19-03-19-3-osbook-day19a.txt:68(搜「有两端的一小部分」)
高半应用与 mcmodel19.5 在后半部分运行应用text/122-ch19-05-19-5.txt:7(搜「0xffff800000000000」) · text/122-ch19-05-19-5.txt:10(搜「mcmodel」)
LOAD 段列表与加载任务19.6 加载应用text/122-ch19-05-19-5.txt:16(搜「readelf -l」) · text/123-ch19-06-19-6.txt:12(搜「两项主要任务」)
先建表后搬段19.6 加载应用text/123-ch19-06-19-6.txt:27(搜「页面故障」) · text/123-ch19-06-19-6.txt:30(搜「设置所需的页面」)
SetupPageMap 递归19.7 设置分层分页结构text/124-ch19-07-19-7.txt:14(搜「递归」) · text/124-ch19-07-19-7.txt:46(搜「entry_index为256」) · text/124-ch19-07-19-7.txt:83(搜「返回未处理页面的数量」)
present 与页面故障19.7 设置分层分页结构text/124-ch19-07-19-7.txt:60(搜「present标志」)
清理与最终跑通19.8 整理分层分页结构text/125-ch19-08-19-8.txt:19(搜「CleanPageMaps」) · text/125-ch19-08-19-8.txt:47(搜「无须调整」) · text/125-ch19-08-19-8.txt:51(搜「正确的计算结果」)

Footnotes

  1. 出处:「19.1 可执行文件和内存地址」(text/119-ch19-01-19-1.txt:4(搜「调整地址」); text/120-ch19-02-19-2.txt:5(搜「mov esi,0x162」); text/120-ch19-02-19-2.txt:20(搜「hexdump」))。 ↩

  2. 出处:「19.2 地址转换」(text/120-ch19-02-19-2.txt:36(搜「预先确定」); text/120-ch19-02-19-2.txt:44(搜「重新定位」); text/120-ch19-02-19-2.txt:49(搜「我们为每个应用准备了专用的地」))。 ↩

  3. 出处:「19.2 地址转换」(text/120-ch19-02-19-2.txt:36(搜「预先确定」); text/120-ch19-02-19-2.txt:44(搜「重新定位」); text/120-ch19-02-19-2.txt:49(搜「我们为每个应用准备了专用的地」))。 ↩

  4. 出处:「19.2 地址转换」()。 ↩

  5. 出处:「19.2 地址转换」()。 ↩

  6. 出处:「19.3 加载并运行应用(osbook_day19a)」(text/121-ch19-03-19-3-osbook-day19a.txt:19(搜「六部分」); text/121-ch19-03-19-3-osbook-day19a.txt:49(搜「0xc0200042」))。 ↩

  7. 出处:「19.3 加载并运行应用(osbook_day19a)」(text/121-ch19-03-19-3-osbook-day19a.txt:66(搜「规范地址」); text/121-ch19-03-19-3-osbook-day19a.txt:68(搜「有两端的一小部分」))。 ↩

  8. 出处:「19.5 在后半部分运行应用」(text/122-ch19-05-19-5.txt:7(搜「0xffff800000000000」); text/122-ch19-05-19-5.txt:10(搜「mcmodel」))。 ↩

  9. 出处:「19.6 加载应用」(text/122-ch19-05-19-5.txt:16(搜「readelf -l」); text/123-ch19-06-19-6.txt:12(搜「两项主要任务」))。 ↩

  10. 出处:「19.6 加载应用」(text/123-ch19-06-19-6.txt:27(搜「页面故障」); text/123-ch19-06-19-6.txt:30(搜「设置所需的页面」))。 ↩

  11. 出处:「19.7 设置分层分页结构」(text/124-ch19-07-19-7.txt:14(搜「递归」); text/124-ch19-07-19-7.txt:46(搜「entry_index为256」); text/124-ch19-07-19-7.txt:83(搜「返回未处理页面的数量」))。 ↩

  12. 出处:「19.7 设置分层分页结构」(text/124-ch19-07-19-7.txt:60(搜「present标志」))。 ↩

  13. 出处:「19.8 整理分层分页结构」(text/125-ch19-08-19-8.txt:19(搜「CleanPageMaps」); text/125-ch19-08-19-8.txt:47(搜「无须调整」); text/125-ch19-08-19-8.txt:51(搜「正确的计算结果」))。 ↩

  14. 出处:「19.1 可执行文件和内存地址」(text/119-ch19-01-19-1.txt:4(搜「调整地址」); text/120-ch19-02-19-2.txt:5(搜「mov esi,0x162」); text/120-ch19-02-19-2.txt:20(搜「hexdump」))。 ↩

  15. 出处:「19.2 地址转换」(text/120-ch19-02-19-2.txt:36(搜「预先确定」); text/120-ch19-02-19-2.txt:44(搜「重新定位」); text/120-ch19-02-19-2.txt:49(搜「我们为每个应用准备了专用的地」))。 ↩

  16. 出处:「19.3 加载并运行应用(osbook_day19a)」(text/121-ch19-03-19-3-osbook-day19a.txt:19(搜「六部分」); text/121-ch19-03-19-3-osbook-day19a.txt:49(搜「0xc0200042」))。 ↩

  17. 出处:「19.6 加载应用」(text/123-ch19-06-19-6.txt:27(搜「页面故障」); text/123-ch19-06-19-6.txt:30(搜「设置所需的页面」))。 ↩

On this page