前几天看了 FreeBSD 的 Architecture Handbook (一部分),内容深入浅 出,而且竟然能和最新的 FreeBSD 代码对上!这简直就是码农文档的典范,我 差点没印出来裱在床头每天拜一拜……

咳不过这不是今天的重点。今天的重点是, handbook 里提到一件很有意思的 事情,这件事情背负着PC发展的沧桑历史(喂……),而我是第一次注意到它。

FreeBSD 的 Boot Loader 片段

上面所说的 handbook 里 提到 这样一段汇编程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
seta20:
    cli         # Disable interrupts
seta20.1:
    dec %cx         # Timeout?
    jz seta20.3     # Yes

    inb $0x64,%al       # Get status
    testb $0x2,%al      # Busy?
    jnz seta20.1        # Yes
    movb $0xd1,%al      # Command: Write
    outb %al,$0x64      #  output port
seta20.2:
    inb $0x64,%al       # Get status
    testb $0x2,%al      # Busy?
    jnz seta20.2        # Yes
    movb $0xdf,%al      # Enable
    outb %al,$0x60      #  A20
seta20.3:
    sti         # Enable interrupts
    jmp 0x9010      # Start BTX

其实就是在检查和写入 0x64 、 0x60 这两个 port [1] ,而前面的文章原文 是这样说的:

The last code block of boot1 enables access to memory above 1MB and concludes with a jump to the starting point of the BTX server

也就是说,上面除了 jmp 指令之外的代码,都是用来启用1MB以上的内存 访问的。哦原来是这样……但是等等, 0x64 和 0x60 不是 连着键盘控制器 的吗 和内存有什么关系?WTF?

[1]不是 socket 监听的那个 port ,而是 inb/outb 指令操作的那个 port 哦~

A20地址线

“A20” 用来指代第21位地址线(因为地址线是从零开始编号的)。这一位地 址很特殊,在CPU启动之后默认总是0. 也就是说,即便CPU给地址总线发送 的物理地址是 0x101234 ,第21位地址也会被置成零,从而寻址到 0x1234 这个内存单元。

上面对 0x64 和 0x60 这两个 port 的操作,就是使 A20 地址线生效,不要 总是发个零出去……

至于 A20 为什么会被禁用,又为什么是用键盘控制器的 port 启用呢?这就要 从PC诞生之初说起了……

A20的历史

在PC刚出现的时候,CPU只有一款,那就是 8086 ,因为它和后续的 8088 既便宜又耐操,所以很快流行起来。这颗CPU有16位的寄存器,但是却有20条 地址线 [2] ,所以 Intel 发明了臭名昭著的用段寄存器访问更多内存的方 法。

举个例子, abcd:1234 这个地址(16进制),冒号前面的是段寄存器的值, 后面的是程序中访问的地址,那么真正的物理地址计算方法是 0xabcd * 0x10 + 0x1234 = 0xacf04 . 这是个20位的地址,刚好可以用在 8086的地址总线上。

这个计算方式有个很微妙的问题: ffff:ffff 这个最大的地址映射到物理地 址 0x10ffef ,TMD都超过20位了…… Intel 的解决方法是装作没看见第21位, 将这个地址当作 0xffef 去访问……

所以,当时的程序是可以通过访问 1MB 以上的地址,来获得物理地址 0xffef 之前的数据的;也真有程序利用了这一点,从而省掉载入段寄存器的操作。

接下来 Intel 与时俱进推出了80286,它还是16位的CPU,但是地址总线一下子 扩展到24位,所以CPU不能再对第21位地址视而不见了。当新的程序访问 ffff:ffff 这个地址时,它有可能是真的想访问物理地址 0x10ffef ;但是当 旧的程序访问 ffff:ffff 时,它肯定是想要访问 0xffef .

由于兼容旧程序是抢占市场的重要手段 [3] , Intel 决定让80286默认以 8086一样的行为工作,也就是对第21位地址视而不见,总是将 A20 置为零。 当程序确定它想要访问 1MB 以上的内存时,再通过特定的方式打开 A20.

而这个特定的方式——不知道当时 Intel 那帮人怎么想的——就是用键盘控制器 上多出来的一个状态位。据说原因就是,有人发现那一位刚.好.多.出.来.了……

于是就出现了 boot loader 里捣鼓 0x64 和 0x60 这两个 port 的代码。

[2]地址线比寄存器位数多是个传统;有人知道几乎所有的32位 x86 CPU 都有36条地址线么……不过貌似64位的CPU还没到要遵守这个传统的时候XD
[3]这也是个传统,AMD大获成功的64位架构也是和32位x86兼容的

A20的未来

A20的特殊性估计还会随着x86架构继续存在一段时间,因为虽然已经没有程序 会通过 ffff:ffff 地址去访问 0xffef 了,但是几乎所有现代操作系统都会 在启动阶段特意去启用A20.

由于启用 A20 这个操作实在太恶心了,其实也有人想过别的方法,像是用其他 的专用 port ,或是将启用 A20 的操作内置到 BIOS 中。可惜的是这些方法 最后都没有被统一,操作系统们也只好用最古老、最保守的 0x64 、 0x60 port 了。

一个小细节

前面说 80286 有24条地址线,但它还是16位CPU,那怎么访问 ffff:ffff 之后 的内存?这个地址换算成物理地址是 0x10ffef ,也就1MB多一点,最高3位的 地址线 A21 、 A22 、 A23 不就没用了?

没错,在“实模式”下,即使有24条地址线, 80286 也只能访问1MB多一点的内存。 Intel 在 80286 身上想要挽回 8086 时期使用段寄存器寻址的错误,推出了 “保护模式”,在保护模式下,CPU可以通过 页表 将16位虚拟内存地址映射到 24位物理地址,所以可以利用所有24位的地址空间。基本上所有现代操作系统都 工作在保护模式或者与其相似的 “长模式” 下,当CPU地址线增加的时候 操作系统只需要更改页表的格式,而且对非法地址的访问会被作为异常处理掉, 所以自 80286 以来再也没有出现过类似 A20 的问题。