基址、界限与分段 — 两个寄存器的魔法,和它的天花板
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。旅程如下:
- 取指:程序计数器 = 128(虚拟),硬件加基址 32768 → 物理 32896, 从那里取出第一条指令2。
- 执行:指令要读虚拟地址 15KB。硬件先查界限:15KB < 16KB,合法; 再加基址:15KB + 32KB = 47KB——x 的真实住址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. 作者的判断与证据
书里给了证据的判断:
作者的判断(立场):
5. 边界与局限
- 本章的地址空间是 14/16KB 的玩具尺寸,为的是算数干净; 真实 32/64 位下的问题规模(和分段法撑不住的理由)靠读者自己放大。
- 细粒度分段(一个地址空间几千个段,Multics、Burroughs B5000) 只给了历史段落——它需要的段表其实就是「页表的前身」, 书里没把这条线索显式画出。
- 分段的紧凑(compaction)只提了一句代价高,没给量化。
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」) |
Footnotes
-
出处:「15. Mechanism: Address Translation」第 68–118 段(
text/17-ch15-15-mechanism-address-translation.txt:90,搜「3000」)。C 代码、x86 汇编与内存访问序列见该例。 ↩ -
出处:「15. Mechanism: Address Translation」第 223–230 段(
text/17-ch15-15-mechanism-address-translation.txt:229,搜「32896」)。 ↩ -
出处:「15. Mechanism: Address Translation」第 241–243 段(
text/17-ch15-15-mechanism-address-translation.txt:243,搜「47 KB」)。 ↩ -
出处:「15. Mechanism: Address Translation」第 269–288 段(
text/17-ch15-15-mechanism-address-translation.txt:281,搜「19384」)。 ↩ -
出处:「15. Mechanism: Address Translation」第 194–206 段(
text/17-ch15-15-mechanism-address-translation.txt:196,搜「static relocation」)。 ↩ -
出处:「15. Mechanism: Address Translation」第 203–206 段(
text/17-ch15-15-mechanism-address-translation.txt:205,搜「hardware support is likely needed」)。 ↩ ↩2 -
出处:「15. Mechanism: Address Translation」第 401–407 段(
text/17-ch15-15-mechanism-address-translation.txt:402,搜「move an address space」)。 ↩ -
出处:「15. Mechanism: Address Translation」第 290–352 段(
text/17-ch15-15-mechanism-address-translation.txt:324,搜「Hardware Requirements」)。 ↩ ↩2 -
出处:「15. Mechanism: Address Translation」第 354–415 段(
text/17-ch15-15-mechanism-address-translation.txt:374,搜「OS Requirements」)。 ↩ ↩2 -
出处:「16. Segmentation」第 7–28 段(
text/18-ch16-16-segmentation.txt:17,搜「CRUX」)。 ↩ -
出处:「16. Segmentation」第 140–200 段(
text/18-ch16-16-segmentation.txt:143,搜「explicit approach」)。 ↩ -
出处:「16. Segmentation」第 176–188 段(
text/18-ch16-16-segmentation.txt:177,搜「SEG_MASK」)。 ↩ ↩2 -
出处:「16. Segmentation」第 95–99 段(
text/18-ch16-16-segmentation.txt:95,搜「Segment」)。寄存器值:Code 32K/2K,Heap 34K/3K,Stack 28K/2K。 ↩ -
出处:「16. Segmentation」第 124–131 段(
text/18-ch16-16-segmentation.txt:124,搜「4200」)。 ↩ -
出处:「16. Segmentation」第 213–250 段(
text/18-ch16-16-segmentation.txt:236,搜「0x3C00」)。 ↩ -
出处:「16. Segmentation」第 254–283 段(
text/18-ch16-16-segmentation.txt:254,搜「Sharing」)。 ↩ -
出处:「16. Segmentation」第 339–348 段与图 16.6(
text/18-ch16-16-segmentation.txt:302,搜「Compacted」)。 ↩ -
出处:「16. Segmentation」第 351–355 段(
text/18-ch16-16-segmentation.txt:351,搜「1000 SOLUTIONS」)。原文:真正的解法是彻底回避变长分配。 ↩ ↩2 -
出处:「16. Segmentation」第 113–117 段(
text/18-ch16-16-segmentation.txt:113,搜「SEGMENTATION FAULT」)。 ↩