目录
- 为什么是段值 ×16
- 核心:BIU 里的地址加法器
- 位级拆解:地址是怎么拼出来的
- 段落(paragraph)与 64 KB 段长
- 别名(aliasing):一个地址,上千种写法
- 边界与回绕:A20 那根线
- 1 MB 边界之外
- 一次真实访存的完整算法
- 与 Linux 的关系
- 社区里常被混淆的几件事
- 小结
- 附:相关高频面试题与参考解析
- 问题一:实模式下物理地址怎么算?
- 问题二:为什么段会重叠,一个地址有多种写法(别名)?
- 问题三:0xFFFF:0xFFFF 超过 1 MB 了吗?
- 问题四:默认的段寄存器有哪些,分别干什么?
- 问题五:实模式下段长为什么是 64 KB?
- 问题六:保护模式下的段和实模式有何不同?
- 问题七:1 MB 边界和 64 KB 段长是什么关系?
- 问题八:A20 是什么,为什么只屏蔽 A20 这一根线?
- 问题九:为什么段落(paragraph)大小是 16 字节?
- 问题十:实模式下能用 32 位寄存器(如 EAX)吗?
- 参考链接
实模式(real mode)是 x86 处理器加电后的初始状态,也是理解”段值:偏移”这套寻址玩法的起点。Intel 在 1978 年推出 8086 时,给了它 16 位的寄存器和 20 根地址线。16 位寄存器最多只能数到 0xFFFF(64 KB),而 20 根地址线能访问 1 MB 内存。为了用 16 位寄存器去够到 1 MB 空间,CPU 引入了一套”分段”机制:把地址拆成”段值”和”偏移”两部分,再拼成 20 位物理地址。
“段值×16+偏移”看似简单,要弄清的却是几件事:为什么偏偏是 ×16 而不是别的数、BIU(总线接口单元)里的地址加法器怎么算、位级上两段是怎么交叠的、为什么一个物理地址会对应上千种写法的”别名”现象,以及 1 MB 边界之外 A20 那根线引发的回绕谜题。
物理地址 = 段值 × 16 + 偏移
为什么是段值 ×16
8086 只有 16 位寄存器,却有 20 根地址线。一个朴素的想法是给段寄存器也配上足够的位、直接拼出 20 位地址;但 1978 年的 8086 是 40 引脚 DIP 封装,引脚极其紧张,根本没有多余的脚去多传 4 位地址。也有人提议”把段值左移 8 位凑出 16 MB”,但这会浪费大量地址空间,被 Intel 否决。最终的折中是把段值左移 4 位(即 ×16),让 16 位段值覆盖到 0xFFFF0,再叠加 16 位偏移,刚好够到 20 位 1 MB 的上界 [6]。
段值:偏移里的段值,来自 CS / DS / SS / ES / FS / GS 这几个段寄存器,它们存放的都是 16 位的”段值”。注意一个常被忽略的点:8086 只有 CS、DS、SS、ES 四个段寄存器;FS 和 GS 是 80386 才加入的。所以”段值:偏移”里的段值,本质就是一个 16 位的编号,CPU 把它左移 4 位(×16)得到段的起始物理地址,也就是段基址 [1]。
偏移的来源和段值不同,它不在段寄存器里,而是在 CPU 执行每条指令时临时算出来的。取指令时偏移就是指令指针 IP;栈操作时偏移是栈指针 SP 或基址指针 BP;做数据访问时偏移(Intel 称之为”有效地址”,effective address)由基址寄存器 BX/BP、变址寄存器 SI/DI 再加上指令里写死的一个常数(位移量)三者相加而成。段值决定”从哪一段开始”,偏移决定”这一段里再走多远”,后者来自通用寄存器与指令本身,而非某个固定寄存器 [1]。
核心:BIU 里的地址加法器
CPU 内部的 BIU(Bus Interface Unit,总线接口单元)里有一个地址加法器,专门负责把”段值”和”偏移”合成 20 位物理地址。
图1:BIU 地址加法器把段值与偏移合成为物理地址
用五步来描述这个过程:
- 取段寄存器里的 16 位段值;
- 把段值左移 4 位(×16),得到 20 位的段基址;
- 取 16 位偏移;
- 段基址与偏移相加,得到 20 位物理地址;
- 若地址超过 20 位(第 21 位为 1),结果可能被截回 1 MB 以内(详见后文 A20 一节)。
位级拆解:地址是怎么拼出来的
把”段值×16+偏移”放到二进制里看,就清楚为什么要 ×16。段值左移 4 位,意味着它的 bit4bit19 落到结果的 bit4bit19;偏移的 bit0bit15 落到结果的 bit0bit15。两者在 bit4~bit15 这一段是重叠的。正是这段重叠,让同一个物理地址可以写成多种 segment:offset 组合。
以段值 0x07C0、偏移 0x0000 为例:段值 0x07C0 左移 4 位得 0x7C00,偏移 0x0000,相加得 0x7C00。结果里段值的 bit4bit19 与偏移的 bit0bit15 在中间 12 位交叠 [1]。
图2:段值 0x07C0、偏移 0x0000 的位级叠加
再以段值 0xFFFF、偏移 0xFFFF 为例:段值 0xFFFF 左移 4 位得 0xFFFF0,加偏移 0xFFFF,得 0x10FFEF。这已经越过 1 MB(0xFFFFF)边界 0xFFF0(即 65520)字节,进入 HMA(高端内存区)。若 A20 关闭,超出 20 位的部分被丢弃,回绕成 0x0FFEF [1]。
图3:段值 0xFFFF、偏移 0xFFFF 的位级叠加(重叠段为 bit4~bit15)
回到 BIU:地址加法器的本质,就是一次”段值左移 4 位 + 偏移”的 20 位加法。
图4:物理地址的位级构成:段值占 bit4~bit19,偏移占 bit0~bit15,中间 12 位(bit4~bit15)重叠
段落(paragraph)与 64 KB 段长
段值左移 4 位后,段基址的低 4 位恒为 0,也就是说段基址总是 16 的倍数。x86 把连续的 16 字节称为一个”段落”(paragraph),所以每个段的起点都对齐到段落边界。一个 16 位偏移最大 0xFFFF,因此单个段最大 64 KB:段基址 0x0000 的段覆盖 0x0000 到 0xFFFF,段基址 0x1000 的段覆盖 0x1000 到 0x10FFF,段基址 0x2000 的段覆盖 0x2000 到 0x2FFFF,都是 64 KB(这里的 0x0000、0x1000、0x2000 都是段基址,不是段值)。正是因为段长只有 64 KB,而段基址可以每 16 字节起一个,段与段之间才大量重叠 [1]。
别名(aliasing):一个地址,上千种写法
同一个物理地址 A,只要存在段值 S 满足:
S × 16 ≤ A
A − S × 16 ≤ 0xFFFF
那么 (S, A − S × 16) 就是 A 的一组合法写法。也就是说,A 可以表示成”段值 S : 偏移 (A−S×16)“。S 每增大 1,段基址增大 16,偏移就减小 16,物理地址不变。这种”一址多写”的现象,英文叫 aliasing(俗称别名)。
以 0x7C00 为例:
S 满足 S × 16 ≤ 0x7C00 → S ≤ 0x7C0
S 取 0x07C0 时,偏移 = 0x7C00 − 0x07C0×16 = 0x7C00 − 0x7C00 = 0x0000
S 取 0 时,偏移 = 0x7C00 − 0 = 0x7C00,且 0x7C00 ≤ 0xFFFF
S 可取 0x0000 到 0x07C0,共 0x7C1 = 1985 组
因此 0x7C00 在 1 MB 空间里共有 1985 种写法。理论上限约 4096 组(2^12),因为重叠的 12 位(bit4~bit15)最多提供 2^12 种组合。常见的写法如:
0x07C0 : 0x0000
0x0000 : 0x7C00
0x0700 : 0x0C00
图5:0x7C00 的 1985 种写法(取其中一段示意)
常有人算出”65536 段 × 64 KB = 4 GB”,那是把段值和偏移都当成独立寻址来乘。实际上两者是相加而非相乘,且都受 20 位地址线约束,实模式最多到 0x10FFEF,远不到 4 GB。
边界与回绕:A20 那根线
1 MB 边界(0xFFFFF)之外,20 根地址线不够用了。第 21 根地址线叫 A20。开启 A20 后,0xFFFF:0xFFFF 能算到 0x10FFEF,超出 1 MB 的 0xFFF0 字节就是 HMA(High Memory Area,高端内存区);关闭 A20 时,第 21 位被强制为 0,地址回绕回 1 MB 以内。
图6:1 MB 边界、HMA 与 A20 回绕:0x10FFEF 与回绕后的 0x0FFEF
8086 只有 20 根地址线,本没有 A20 这个概念;80286 有了 24 根地址线后,为了兼容 8086 的回绕行为,IBM 在 1984 年的 PC AT 上加了一道”Gate-A20”开关,默认关闭,使第 21 位恒为 0,老程序依旧回绕;后来的 80486 / Pentium 则在 CPU 内用 A20M# 引脚实现同样的屏蔽。现代 Linux 启动早期仍需主动开启 A20 [6]。
Linux 内核 arch/x86/boot/a20.c(tag v6.14)里的 enable_a20() 依次尝试三套方案开启 A20:先发 BIOS 中断 INT 0x15(AX=0x2401);失败则用 8042 键盘控制器,向端口 0x64 写 0xD1、再向 0x60 写 0xDF 打开 A20;再不行就写端口 0x92,置 bit1、清 bit0 直接开 A20。每套方案之间最多重试 255 次,直到读到 CPU 地址线确实不再回绕才罢休 [5]。
为什么只屏蔽 A20 这一根线?因为只有第 21 位(A20)跨越了 1 MB 边界。A20 关闭时,0xFFFF:0xFFFF = 0x10FFEF 的第 21 位被清零变成 0x0FFEF;而 A21~A31 原本就是 0,不受影响。所以”只屏蔽 A20”的效果是:1–2 MB、3–4 MB 这类奇数兆区间会被回绕到前一个偶数兆区间,偶数兆区间以内的地址照常工作 [3][6]。
raw = (段值 << 4) + 偏移
物理地址 = A20开启 ? raw : (raw & 0xFFFFF)
1 MB 边界之外
0 到 1 MB 之间能否访问到 1 MB 以上的空间,取决于 A20 是否开启。现代 PC 的 BIOS 一般默认开着 A20,所以实模式也能摸到 HMA。
图7:实模式 1 MB 内存布局(不按比例)
| 地址范围 | 用途 |
|---|---|
| 0x00000 – 0x003FF | IVT,中断向量表,256 个入口,占 1 KB |
| 0x00400 – 0x004FF | BIOS 数据区(BDA) |
| 0x00500 – 0x9FFFF | 常规内存(DOS 下的 640 KB 可用区) |
| 0xA0000 – 0xBFFFF | 显存 / 视频缓冲区 |
| 0xC0000 – 0xEFFFF | 扩展 ROM(Option ROM) |
| 0xF0000 – 0xFFFFF | 系统 BIOS / UEFI 兼容 ROM |
DOS 的 640 KB 常规内存,刚好是 1 MB 里 0xA0000 以下的部分。引导扇区地址 0x7C00 的来历也和这张图有关:1981 年的 IBM PC 5150 用 8088,内存只有 32 KB(0x0000–0x7FFF)。BIOS 把 512 字节的引导扇区加载到内存顶端的 0x7FFF 往下数 512 字节,再留 512 字节作栈空间:0x7FFF − 512 − 512 + 1 = 0x7C00。这个值同时可以写成 0x0000:0x7C00 或 0x07C0:0x0000。这也是一个别名现象的活例子。PC 约定引导扇区就放在 0x7C00 [7]。
一次真实访存的完整算法
CPU 取指令或访问数据时,段寄存器与偏移的组合有默认规则:
取指令:物理地址 = CS × 16 + IP
访存: 物理地址 = DS × 16 + (BX/SI/DI/BP + 位移量)
栈操作:物理地址 = SS × 16 + SP/BP
串操作:DS:SI 与 ES:DI
“有效地址”(effective address,Intel SDM 用语)就是偏移那一半:由基址寄存器(如 BX、BP)加上变址寄存器(如 SI、DI)再加上一个位移量算出来;CPU 再把它和段基址(段值×16)相加,得到最终物理地址。
| 操作 | 默认段 | 偏移来源 |
|---|---|---|
| 取指令 | CS | IP |
| PUSH / POP / CALL / RET、访问 BP | SS | SP / BP |
| STOS / MOVS 等串操作 | ES | DI |
| 一般数据访问、串操作源 | DS | BX / SI / DI |
CS、SS、ES、FS、GS 这些只是”默认”规则:用段超越前缀(segment override prefix)可以临时改段。比如 ES: 前缀就让本该用 DS 的访问改走 ES。
整个过程经过 BIU 的两级寻址才得到物理地址:第一级是段寄存器里的段值左移 4 位得到段基址,第二级是段基址加上有效地址得到物理地址。
图8:一次访存的两级寻址:段寄存器到段基址,偏移(有效地址)叠加得物理地址
与 Linux 的关系
CPU 加电后从实模式起步,BIOS 把控制权交给引导程序;GRUB 先从实模式运行、随后切换到保护模式去加载内核;Linux 内核自身在 arch/x86/boot 里也经历从实模式到保护模式(置 CR0.PE)再到长模式(设 EFER.LME 并开 CR0.PG)的过程。
从 1978 年的 8086 到今天,x86 到目前为止每一代都仍从实模式复位起步。Intel 在 2023 年提出 X86S 提案想去掉这套历史包袱,但实模式作为启动入口短期内不会消失。实模式之外还有 unreal mode(也叫 big real mode):进入保护模式设好段描述符后再切换回实模式,段缓存里仍保留着保护模式设下的基址与界限,从而让实模式代码也能访问 4 GB 空间。早期 DOS 的 HIMEM.SYS 就用它突破 1 MB [6]。
图9:x86 模式链:实模式经 CR0.PE 进保护模式,再经 EFER.LME+CR0.PG 进长模式
社区里常被混淆的几件事
下面这些误区在 Stack Overflow、OSDev 等社区反复出现:
- 误解”实模式只能访问 1 MB”:开启 A20 后能到 0x10FFEF,越界的 0xFFF0 字节就是 HMA;只有关闭 A20 才回绕回 1 MB 以内。
- 误解”别名最多 4096 组”:那只是 2^12 的理论上限,对具体地址(如 0x7C00)实际只有 1985 组。
- 误解”实模式不能用 32 位寄存器”:段值、偏移是 16 位,但操作数大小前缀 0x66 可让 EAX 等 32 位寄存器在实模式下照常使用。
- 误解”段 64 KB、段值 16 位,相乘得 4 GB”:段值和偏移是相加不是相乘,且都受 20 位地址线约束,实模式最多到 0x10FFEF。
- 困惑”段值×16 为什么不是左移 8 位”:左移 8 位(×256)确实能拼出更大的地址,但会浪费大量地址空间、也超出 1 MB 的目标,所以被 Intel 否决;左移 4 位(×16)才是与 20 根地址线匹配的折中。
小结
- BIU 的地址加法器做”段值左移 4 位(×16)+ 偏移”的 20 位加法;重叠的 12 位(bit4~bit15)是别名现象的根源 [1]。
- 段值均为 16 位;FS/GS 是 80386 才加的段寄存器,8086 只有 CS/DS/SS/ES [1]。
- 0xFFFF:0xFFFF = 0x10FFEF,越过 1 MB 边界 0xFFFFF 共 0xFFF0 字节,即 HMA;A20 关闭时回绕成 0x0FFEF [1]。
- 默认段寄存器:DS 供一般访存,CS:IP 取指令,SS:SP 管栈,ES:DI 串操作;可用段超越前缀改写 [1]。
- 段长 64 KB 来自 16 位偏移;x86 把 16 字节叫一个段落,段基址都对齐到段落边界 [1][2][4]。
- 32 位寄存器可用 0x66 前缀,实模式不排斥 32 位操作数 [4]。
附:相关高频面试题与参考解析
以下题目提炼自操作系统与 x86 实模式方向在知乎、CSDN、SegmentFault、Stack Overflow、OSDev 等社区被反复问到的问题,选取与本文内容直接相关的十题。参考答案回引前文与官方资料。
问题一:实模式下物理地址怎么算?
**答题思路:**按”段值左移 4 位 + 偏移”的框架作答,先写公式,再点出 BIU 里的地址加法器完成合成。
**参考答案:**物理地址 = 段值 × 16 + 偏移。BIU 把 16 位段值左移 4 位得到段基址,再与 16 位偏移相加,得到 20 位物理地址 [1]。
问题二:为什么段会重叠,一个地址有多种写法(别名)?
**答题思路:**从位级重叠讲起,再用代数条件 S×16 ≤ A 且 A−S×16 ≤ 0xFFFF 说明”一址多写”。
**参考答案:**因为段值左移 4 位后,bit4bit15 与偏移的 bit0bit15 重叠。满足 S×16 ≤ A 且 A−S×16 ≤ 0xFFFF 的 (S, A−S×16) 都是 A 的合法写法 [1]。
问题三:0xFFFF:0xFFFF 超过 1 MB 了吗?
**答题思路:**先算得 0x10FFEF,再讲它越过 1 MB 边界 0xFFFFF 多少字节即 HMA,最后说 A20 关闭时回绕。
**参考答案:**0xFFFF×16 + 0xFFFF = 0xFFFF0 + 0xFFFF = 0x10FFEF,超过 1 MB 边界 0xFFFFF 共 0xFFF0 字节,即 HMA。A20 关闭时回绕成 0x0FFEF [1]。
问题四:默认的段寄存器有哪些,分别干什么?
**答题思路:**按”取指令 / 栈 / 串操作 / 一般数据访问”四类,分别列出默认段寄存器与对应的偏移来源。
**参考答案:**取指令用 CS(配 IP),栈操作用 SS(配 SP/BP),串操作用 ES(配 DI 作目的)与 DS(配 SI 作源),一般数据访问用 DS(配 BX/SI/DI);这些都可用段超越前缀临时改写 [1]。
问题五:实模式下段长为什么是 64 KB?
**答题思路:**从 16 位偏移的上界讲起,再点出段基址每 16 字节起一个,导致段与段大量重叠。
**参考答案:**偏移是 16 位,最大 0xFFFF,所以单个段最大 64 KB。段基址 = 段值×16,每 16 字节起一个段,段间大量重叠 [1]。
问题六:保护模式下的段和实模式有何不同?
**答题思路:**对比实模式的”段值×16”与保护模式的段选择子,强调保护模式经 GDT/LDT 查描述符得到 32 位基址与界限。
**参考答案:**保护模式的段寄存器存的是段选择子,经 GDT/LDT 查到段描述符,里面是 32 位基址和界限,不再用”段值×16” [1]。
问题七:1 MB 边界和 64 KB 段长是什么关系?
**答题思路:**先判定两者无关,再分别归因于 16 位偏移与 20 位地址线的约束,最后补 A20 开启可达 0x10FFEF。
**参考答案:**两者无关。64 KB 是单个段的最大长度(受 16 位偏移限制),1 MB 是整个实模式地址空间上界(受 20 位地址线限制);开启 A20 后可达 0x10FFEF [1][3]。
问题八:A20 是什么,为什么只屏蔽 A20 这一根线?
**答题思路:**先说明 A20 是第 21 根地址线、跨越 1 MB 边界,再解释只屏蔽它就能复现 8086 回绕,因为更高位本就是 0。
**参考答案:**A20 是第 21 根地址线,跨越 1 MB 边界。关闭时第 21 位清零,0xFFFF:0xFFFF 回绕成 0x0FFEF;A21~A31 本就是 0 不受影响,所以只屏蔽 A20 就足以复现 8086 的回绕行为 [3]。
问题九:为什么段落(paragraph)大小是 16 字节?
**答题思路:**从段值×16 使段基址低 4 位恒为 0 出发,落到”段都对齐到 16 字节边界”。
**参考答案:**段值左移 4 位(×16),段基址低 4 位恒为 0,所以段都对齐到 16 字节边界;x86 把连续 16 字节称为一个段落 [2][4]。
问题十:实模式下能用 32 位寄存器(如 EAX)吗?
**答题思路:**点出操作数大小前缀 0x66 让 16 位指令改用 32 位寄存器,实模式并不禁止 32 位操作数。
**参考答案:**可以。操作数大小前缀 0x66 让 16 位指令改用 32 位寄存器,实模式并不禁止 32 位操作数 [4]。
参考链接
[1] Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 1: Basic Execution Environment, Chapter 20 Real-Address Mode;Volume 3: System Programming Guide 的 CR0 等章节。https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
[2] Linux Kernel Documentation, Documentation/x86/boot.rst,The Linux/x86 Boot Protocol 与 setup 说明。https://www.kernel.org/doc/html/latest/
[3] Stack Overflow 上关于实模式寻址的高赞讨论(Peter Cordes、Michael Petch 等):
- How does the segment:offset addressing scheme work. https://stackoverflow.com/questions/8694950/how-does-the-segmentoffset-addressing-scheme-work
- Why Enable/Disable A20 Line. https://stackoverflow.com/questions/13893056/why-enable-disable-a20-line
- What happens when the physical address corresponding to CS:IP is more than 20 bits. https://stackoverflow.com/questions/60369191/what-happens-when-the-physical-address-corresponding-to-csip-is-more-than-20-bi
- Segment and offset uses 4 GB. https://stackoverflow.com/questions/60466156/segment-and-offset-uses
[4] OSDev Wiki:Real Mode 与 Memory Map (x86),段落、64 KB 段长、1 MB、HMA 等。https://wiki.osdev.org/Real_Mode https://wiki.osdev.org/Memory_Map_(x86)
[5] Linux 内核 arch/x86/boot/a20.c(tag v6.14),enable_a20() 依次尝试 BIOS INT 0x15 AX=0x2401、8042 端口 0x64/0x60、端口 0x92,最多重试 255 次。https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
[6] 维基百科:Real mode(1978 8086、20 位地址线)、A20 line(1984 PC AT Gate-A20、A20M#、HMA)、Intel 8086(1978、40 引脚 DIP)、x86 memory segmentation(GDT/LDT、FS/GS)、Unreal mode(big real mode、4 GB)。https://en.wikipedia.org/wiki/Real_mode https://en.wikipedia.org/wiki/A20_line https://en.wikipedia.org/wiki/Intel_8086 https://en.wikipedia.org/wiki/X86_memory_segmentation https://en.wikipedia.org/wiki/Unreal_mode
[7] CSDN《为何 MBR 引导地址是 0x7C00》与 OSDev Wiki Boot Sequence(IBM PC 5150、32 KB 内存、0x7FFF−512−512+1=0x7C00,BIOS 加载到 0x0000:0x7C00 或 0x07C0:0x0000)。https://blog.csdn.net/qq_37232329/article/details/79845307 https://wiki.osdev.org/Boot_Sequence