简述计算机启动过程
涛叔我到了小学六年级才第一次见到计算机。当时最令我惊讶不解的是它需要很长的启动过程。启动时会在屏幕上滚动显示各种不认识的字母。上大学以后,因为不是计算机科班,只学了操作系统和编程语言,并没有系统的学习计算机组成原理。工作之后更是鲜少需要用到相关知识。所以到现在对这个问题的理解还很模糊。最近读到 Hackman 的文章1,简明介绍了早期计算机硬件的启动过程,虽然有点过时,但基本原理跟现代 CPU 还是相通的。今天把学习笔记分享给大家。
通常计算机通电之后会重置 CPU。CPU 芯片都有专门的重置引脚,叫 RESET 或者 RST。主板加电之后会将该引脚置为高电平。CPU 收到信号后会初始化各类状态。
这里有一个问题,为什么 CPU 启动后不能直接工作,而是需要重置一遍呢?Hackman 的解释也比较有意思。他说主板加电后会产生『脏电流』。这里的脏可以理解为通电瞬间的电压不稳定。计算机所使用的数字电路对电压范围有一定的要求,所以会产生不可预知的状态。
这里说的比较笼统,我补充一个具体的例子,说明为什么加电后电路处于未知状态。
下图是 R-S 触发器的原理图2,想必大家都不陌生。在计算机内部大量使用 R-S 触发器和它的衍生电路。
R-S 触发器的真值表如下:
S | R | Q | |
---|---|---|---|
1 | 0 | 1 | 0 |
0 | 1 | 0 | 1 |
0 | 0 | Q | |
1 | 1 | - | - |
R-S 触发器可以保存上次设置的状态。也就是说只要 S 加过高电平,那后面 Q 一直是高电平,除非给 R 设置高电平来重置状态。如果 S 和 R 都是低电平,那么 Q 就保持不变。
现在问题来了。对于 R-S 触发器,首次加电后 Q 是什么状态呢?答案是不确定。可能是高电平,也可能是低电平。大家看上面的电路图。理论上 S 和 R 都是低电平。如果 NOR1 先输出高电平,那么 NOR2 必然输出低电平,就时会将 Q 锁定在高电平;反之如果是 NOR2 先输出高电平,则 Q 会锁定在低电平。
为了让计算机正常工作,就必须主动将 R 设为高电平,完成初始化。重置之后 Q 必然是低电平状态。
重置过程会持续一两秒的时间。完成之后主板会将 RST 脚重新置为低电平,然后 CPU 开始正常工作。CPU 的工作可以简单理解为执行内存中的一系列指令。所有的 CPU 都是不断地从内存中获取对应的指令,然后执行。
这里的内存包括 RAM3 和 ROM4 两种。ROM 保存在主板的专用芯片中,断电也不会丢失。RAM 就是我们常说的内存条,用于临时存放数据和指令,断电后会清空。系统启动后,CPU 会先执行 ROM 中的指令。CPU 读取内存数据要用到地址总线。所谓地址就是内存编号,CPU 要读取某地址的内存数据就需要将对对应的地址写入地址总线。
那 CPU 执行的第一条指令在内存的什么位置呢?不同的 CPU 有不同的处理方式。有些会直接执行特定地址的内的指令,也有一些使用所谓的 reset vector,它们会先检查一段特殊的内存,根据结果再到不同的内存地址加载指令。
比如 Z80 CPU 重置后首先执行 0x0000 地址处的指令,非常简单。65025 CPU 在内存的 0xFFFC 和 0xFFFD 保存所谓的 reset vector。这两个字节以小端方式保存实际指令的内存地址。如果 0xFFFC 的值为 0x00,0xFFFD 的值为 B0,6502 CPU 就会从 0xB000 处加载并执行对应的指令。这种设计有两个好处。其一可以灵活控制 CPU 的执行指令,工程师可以根据不同的条件向 reset vector 写入不同的地址,从而控制 CPU 的行为。其二是不会占用 0x0000 开始的低地址空间6。Z80 的方案比较简单,但缺点是低地址空间只能留给 ROM 使用。
CPU 有一个专门的寄存器,叫指令指针7,它里面保存 CPU 下一条要执行的指令地址。 CPU 每执行一条指令,IP 寄存器就会加一。如果遇到 JMP 指令,CPU 会将 JMP 的目标地址写入 IP 寄存器,然后开始执行 IP 指向的内存里的指令。CPU 指令也叫 opcode,它们是一串特定的 0 和 1 的组合。比如在 x86 CPU 中,0x90 表示 NOP,CPU 碰到它后什么也不做,然后执行下一条指令。
无论 CPU 从什么地方加载指令,系统一定首先执行 ROM 中的指令。计算机启动后需要做一些基础的检查和初始化,这部分指令保存在主板上的 BIOS 内。电脑上电后首先运行的就是 BIOS 系统。
BIOS 执行完成后,BIOS 会决定接下来再要执行哪些指定。一般情况下,BIOS 会查找系统上的磁盘并将特定扇区加载到内存,然后开始执行里面的指令。这部分指令通常也叫 boot loader,以前开源世界比较常见的就是 GRUB。到这个时候,BIOS 算是交出了系统控制权。 boot loader 会根据配置加载对应的系统内存,并将 CPU 交给操作系统。
计算机有不同的内存区域或者设备,但总线只有一条。CPU 在访问内存时要怎样指定不同的设备呢?最简单的办法是使用转换芯片,比如 3-8 转换器。
我们可以用三根地址线来选择不同的芯片。三位二进制有 8 种不同的组合,分别是:000, 001,010,011,100,101,110,111。我们可以通过 3-8 转换器将这 8 种组合转换成 8 个零一信号 A0-A7,并跟每个设备的的选择引脚(CS)相连,这样就可以使用不同的地址来选择不同的设备。
3-8 转换器原理图如下:
假设总线为 16 位,用 3 位选择设备,则还有 13 位可用设备内寻址。也就是说每个设备最多支持 8k 内存,总共加起来有 64k 内存。虽然跟现在几吉甚至是几十吉的内存没法比,这在当年已经很多了。对应的地址空间如下:
000: 0x0000 to 0x1FFF
001: 0x2000 to 0x3FFF
010: 0x4000 to 0x5FFF
011: 0x6000 to 0x7FFF
100: 0x8000 to 0x9FFF
101: 0xA000 to 0xBFFF
110: 0xC000 to 0xDFFF
111: 0xE000 to 0xFFFF
以上就是本文的主要内容。除了参考 Hackman 的文章之外,我还参考了《编码:隐匿在计算机软硬件背后的语言》一书。这也是一本让人相见恨晚的神书,书中从编码到逻辑电路再到 CPU 原理,循序渐进,讲得非常好。后续我会适时分享学习心得,敬请期待。
本文示意图均使用 TikZ 绘制,具体参考我的文章 ../unix/markdown-drawing.html↩︎
Random-access Memory,随机访问内存,支持读写↩︎
Read-only Memory 只读内存↩︎
6502 和 Z80 都是曾经很流行的 8 位 CPU 芯片↩︎
早期计算机没有 MCU,使用内存都用绝对地址,也就是物理地址,所以硬件如何使用地址空间会影响应用程序。现代的 CPU 都使用 MCU 做映射,程序使用的逻辑地址,不受物理地址影响。↩︎
instruction pointer,简写为 IP 寄存器。其实也叫 program counter,简称 PC 寄存器。↩︎