跳转至

07 基址、界限与分段 — 两个寄存器的魔法,和它的天花板

1. 这一章讲什么

上一章承诺了「私有地址空间」的幻觉,本章开始兑现。第一版解法朴素得惊人: CPU 里加两个寄存器——基址(放哪儿)和界限(有多大)。程序照旧从 0 编址, 每条地址由硬件当场加上基址、检查界限,翻译和保护一次完成。这是 1950 年代末 分时机器的原创,今天一切虚拟内存的祖先。

然后本章做这本书最擅长的事:把这个解法推到墙角,看它在哪坏掉—— 坏在地址空间中间那一大段空洞上。修补的方案叫分段; 分段引出的新毛病(外部碎片)没有好解,于是逼出下一章的分页。

2. 顶层全景

  基址/界限(整地址空间一大块):
    程序生成虚拟地址 ──▶ 硬件:先查 虚拟地址 < 界限? ──否──▶ 异常,进程被毙
                                   │是
                                   ▼
                              物理地址 = 虚拟地址 + 基址 ──▶ 访存
  分段(每个逻辑段一对基址/界限):
    代码、堆、栈各自安家,空洞不占物理内存
    代价:物理内存里出现越来越多「塞不下任何段」的小空隙(外部碎片)
        │
        ▼  没有好解 → 换固定大小的块 → 分页(下一章)

图说:上半是「一整块搬家」,下半是「分房间搬」;分房间省地方, 但房间大小不一,空出来的边角料越来越难用。

3. 核心原理

3.1 主线例子:一条指令的完整旅程

书里用了一段真实 x86 汇编当主线例子,我们照抄数值走一遍1:

void func() {
    int x = 3000;
    x = x + 3;    // 我们关心的就是这一行
}

编译成汇编是三行:movl 0x0(%ebx), %eax(把 x 的值取进寄存器)、 addl $0x03, %eax(加 3)、movl %eax, 0x0(%ebx)(写回去)。 设 x 在虚拟地址 15KB,进程被操作系统放在物理内存 32KB 处, 界限寄存器 = 16KB。旅程如下:

  1. 取指:程序计数器 = 128(虚拟),硬件加基址 32768 → 物理 32896, 从那里取出第一条指令2。
  2. 执行:指令要读虚拟地址 15KB。硬件先查界限:15KB < 16KB,合法; 再加基址:15KB + 32KB = 47KB——x 的真实住址3。
  3. 写回同理:47KB。三次访存,程序全程不知 32KB 的存在。

越界的演示(书里那张 4KB 地址空间放在 16KB 处的翻译表4):

虚拟地址 计算 结果
0 0 + 16KB 物理 16KB ✓
1KB 1KB + 16KB 物理 17KB ✓
3000 3000 + 16384 19384 ✓
4400 4400 ≥ 4096(界限) 异常(越界)

图说:三行合法一行违法——界限检查拦下的就是那个「段错误」的祖先。

3.2 为什么必须是硬件,软件行不行

软件也试过:早年的静态重定位由加载器(loader)把程序里的地址 逐个改写(比如把 1000 改写成 4000)。两条死罪:一是没有保护—— 程序自己算出一个坏地址,没有任何东西拦它;二是装定之后很难搬家5。 结论书里写得很重:真正的保护大概必须有硬件支持6。 动态重址(硬件在每次访存时翻译)同时解决了这两个问题: 程序运行时也能整体搬——把进程停下、内存拷贝到新位置、 改存好的基址值,恢复后程序毫无察觉7。

硬件清单(每 CPU 一对):基址/界限寄存器、翻译与比较电路 (这部分后来叫 MMU,内存管理单元)、改这两个寄存器的特权指令、 越界时发起异常的机制8。操作系统对应四件事:创建进程时从空闲列表 (free list)找位置、终止时回收、上下文切换时保存/恢复基址界限 (每个 CPU 只有一对,换进程必须换寄存器)、提供异常处理9。

3.3 分段:把一对寄存器复制成三对

基址/界限的天花板很快露出来:16KB 的地址空间里,堆和栈其实只用几 KB, 中间一大段空闲——但「整块搬家」逼着物理内存为这整个 16KB 买单, 包括中间的空洞。32 位地址空间(4GB)配一个只用几 MB 的程序, 浪费是灾难级的10。

分段的想法:地址空间本来就有三个逻辑段(代码、堆、栈), 每段配一对基址/界限寄存器,各自独立安置在物理内存。 硬件怎么知道一条地址属于哪段?主流是显式法:拿地址的最高几位 当段号。书里的例子:14 位地址、3 段,用最高 2 位选段, 剩下 12 位是段内偏移11。翻译变成两段式伪代码(照抄原书12):

  段号   = (虚拟地址 & 0x3000) >> 12     # 最高 2 位
  偏移   = 虚拟地址 & 0x0FFF             # 低 12 位
  if 偏移 >= 界限[段号] → 异常(保护错)
  else 物理地址 = 基址[段号] + 偏移

段内翻译的具体数(寄存器值:代码基址 32K 长 2K,堆 34K 长 3K,栈 28K 长 2K13):

  • 代码段内虚拟地址 100:段号 00,偏移 100 < 2K ✓ → 32K + 100 = 32868。
  • 堆内虚拟地址 4200:二进制最高两位 01 = 堆段;偏移 = 4200 − 4096 = 104 (必须先减去段的起始虚拟地址 4KB!)→ 34K + 104 = 3492014。 这里有个易错点:4200 不能直接加基址(那样得 39016,错)—— 段基址加的是「段内偏移」,不是整个虚拟地址。
  • 栈要特殊处理:它是向低地址长的段。硬件加一位「正向生长」标志; 访问栈内虚拟地址 15KB(段号 11,低 12 位 = 3KB)时,偏移要换成负数: 3KB − 4KB(段最大) = −1KB,基址 28K + (−1K) = 27K, 界限检查看 |−1KB| ≤ 2K ✓15。

3.4 共享与保护位

加几个保护位(读/写/执行),分段突然多了个超能力: 把代码段设成只读,同一个物理代码段就能映射进多个进程的地址空间—— 各进程以为在用自己的私有内存,实际共享着同一份不能改的代码,幻觉不破16。 这就是「共享库」的远古形态。

3.5 天花板:外部碎片,和「段错误」这个词的来历

分段省下了空洞,却把碎片问题请进了物理内存:段是变长块, 进程来来去去,空闲内存被切成越来越小的碎块——总空闲明明够, 却没有任何一块连续空间装得下新段。书里图 16.6 的对比: 紧凑(compaction)能重整,但代价是停机拷贝17。

「如果 1000 个解法存在,就说明没有伟大的那个」——书里这句提示 点破了变长块问题的本质:外部碎片没有好算法,只有凑合18。 顺带,一个术语在此兑现:在分段机器上访问非法地址触发的异常, 就叫 segmentation fault(段错误);段式机器早已消失,词留了下来, 至今是 C 程序员最怕的报错19。

真正的出路只有一条,书里提前剧透:永远不按变长块分配—— 把空间切成固定大小的块,碎片问题从根上消失。这就是分页,下一章。

4. 作者的判断与证据

书里给了证据的判断:

  • 基址/界限的翻译机制、硬件需求表、OS 职责表,全部给出可核对的条目89。
  • 静态重址 vs 动态重址的取舍、分段的共享/保护位机制,有硬件算法伪代码佐证12。

作者的判断(立场):

  • 「真正的保护需要硬件」是作者明确的强论断,引 [WL+93]6。
  • 对碎片问题的判词「没有伟大解,唯一真正的解法是完全回避」 是本部分最重要的预言,下一章兑现18。

5. 边界与局限

  • 本章的地址空间是 14/16KB 的玩具尺寸,为的是算数干净; 真实 32/64 位下的问题规模(和分段法撑不住的理由)靠读者自己放大。
  • 细粒度分段(一个地址空间几千个段,Multics、Burroughs B5000) 只给了历史段落——它需要的段表其实就是「页表的前身」, 书里没把这条线索显式画出。
  • 分段的紧凑(compaction)只提了一句代价高,没给量化。

6. 可带走的

  1. 地址翻译 = 每次访存时硬件做的一次改写:加基址、查界限。 保护的钥匙在硬件手里,软件(加载器改写)给不了保护。
  2. 上下文切换要换的东西又多了一样:基址/界限寄存器—— 进程的「环境」远不止寄存器。
  3. 分段教会两件事:一,按逻辑单位(段)管理比按整体管理省得多; 二,变长分配必然碎片化,这是结构性结论,不是算法不够聪明。
  4. 段错误的词源在硬件——越界访问触发的是「保护异常」, 不是「程序崩了」这么模糊的事。
  5. 保护位(读/写/执行)是共享的前提:共享的内存必须不可写, 否则隔离破功。今天每个进程的「代码段只读」仍是这个设计。
  6. 看到「有 1000 种算法在解同一个问题」时,就该怀疑问题本身该换表述 (这里是:别用变长块)。

7. 原文地图

主题 原书章 原文位置
本章假设(连续、小、等大) 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:53(搜「Assumptions」)
汇编三行与 x=3000 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:90(搜「3000」)
取指翻译 128→32896 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:229(搜「32896」)
物理地址 = 虚拟地址 + 基址 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:218(搜「physical address =」)
界限检查与异常 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:252(搜「bounds register」)
4KB 翻译表(含 Fault) 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:281(搜「19384」)
静态重址的两大缺陷 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:194(搜「SOFTWARE-BASED」)
「真正的保护需要硬件」 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:205(搜「hardware support」)
硬件需求总表 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:324(搜「Hardware Requirements」)
OS 职责(空闲列表、切换保存) 15. Mechanism: Address Translation text/17-ch15-15-mechanism-address-translation.txt:386(搜「free list」)与:392(搜「context switch」)
分段解决空洞浪费 16. Segmentation text/18-ch16-16-segmentation.txt:17(搜「crux」)
段寄存器值表 16. Segmentation text/18-ch16-16-segmentation.txt:96(搜「32K」)
段错误词源 16. Segmentation text/18-ch16-16-segmentation.txt:113(搜「SEGMENTATION FAULT」)
地址 100 → 32868 16. Segmentation text/18-ch16-16-segmentation.txt:121(搜「32868」)
4200 → 偏移 104 → 34920 16. Segmentation text/18-ch16-16-segmentation.txt:124(搜「4200」)
显式段号 + 翻译伪代码 16. Segmentation text/18-ch16-16-segmentation.txt:177(搜「SEG_MASK」)
反向生长的栈:15KB → 27K 16. Segmentation text/18-ch16-16-segmentation.txt:234(搜「15KB」)
保护位与代码共享 16. Segmentation text/18-ch16-16-segmentation.txt:254(搜「Sharing」)
紧凑前后对比 16. Segmentation text/18-ch16-16-segmentation.txt:302(搜「Compacted」)
「1000 个解法 = 没有伟大解」 16. Segmentation text/18-ch16-16-segmentation.txt:351(搜「1000 SOLUTIONS」)


  1. 出处:「15. Mechanism: Address Translation」第 68–118 段(text/17-ch15-15-mechanism-address-translation.txt:90,搜「3000」)。C 代码、x86 汇编与内存访问序列见该例。 ↩

  2. 出处:「15. Mechanism: Address Translation」第 223–230 段(text/17-ch15-15-mechanism-address-translation.txt:229,搜「32896」)。 ↩

  3. 出处:「15. Mechanism: Address Translation」第 241–243 段(text/17-ch15-15-mechanism-address-translation.txt:243,搜「47 KB」)。 ↩

  4. 出处:「15. Mechanism: Address Translation」第 269–288 段(text/17-ch15-15-mechanism-address-translation.txt:281,搜「19384」)。 ↩

  5. 出处:「15. Mechanism: Address Translation」第 194–206 段(text/17-ch15-15-mechanism-address-translation.txt:196,搜「static relocation」)。 ↩

  6. 出处:「15. Mechanism: Address Translation」第 203–206 段(text/17-ch15-15-mechanism-address-translation.txt:205,搜「hardware support is likely needed」)。 ↩↩

  7. 出处:「15. Mechanism: Address Translation」第 401–407 段(text/17-ch15-15-mechanism-address-translation.txt:402,搜「move an address space」)。 ↩

  8. 出处:「15. Mechanism: Address Translation」第 290–352 段(text/17-ch15-15-mechanism-address-translation.txt:324,搜「Hardware Requirements」)。 ↩↩

  9. 出处:「15. Mechanism: Address Translation」第 354–415 段(text/17-ch15-15-mechanism-address-translation.txt:374,搜「OS Requirements」)。 ↩↩

  10. 出处:「16. Segmentation」第 7–28 段(text/18-ch16-16-segmentation.txt:17,搜「CRUX」)。 ↩

  11. 出处:「16. Segmentation」第 140–200 段(text/18-ch16-16-segmentation.txt:143,搜「explicit approach」)。 ↩

  12. 出处:「16. Segmentation」第 176–188 段(text/18-ch16-16-segmentation.txt:177,搜「SEG_MASK」)。 ↩↩

  13. 出处:「16. Segmentation」第 95–99 段(text/18-ch16-16-segmentation.txt:95,搜「Segment」)。寄存器值:Code 32K/2K,Heap 34K/3K,Stack 28K/2K。 ↩

  14. 出处:「16. Segmentation」第 124–131 段(text/18-ch16-16-segmentation.txt:124,搜「4200」)。 ↩

  15. 出处:「16. Segmentation」第 213–250 段(text/18-ch16-16-segmentation.txt:236,搜「0x3C00」)。 ↩

  16. 出处:「16. Segmentation」第 254–283 段(text/18-ch16-16-segmentation.txt:254,搜「Sharing」)。 ↩

  17. 出处:「16. Segmentation」第 339–348 段与图 16.6(text/18-ch16-16-segmentation.txt:302,搜「Compacted」)。 ↩

  18. 出处:「16. Segmentation」第 351–355 段(text/18-ch16-16-segmentation.txt:351,搜「1000 SOLUTIONS」)。原文:真正的解法是彻底回避变长分配。 ↩↩

  19. 出处:「16. Segmentation」第 113–117 段(text/18-ch16-16-segmentation.txt:113,搜「SEGMENTATION FAULT」)。 ↩