译自:http://www.brokenthorn.com/Resources/OSDe9.html
第9 章:开启A20
Mike, 2008
本系列文章旨在向您展示并说明如何从头开发一个操作系统。
欢迎! :)
在前一章里,我们了解到如何将处理器切换到32 位模式。我们也了解到如何访问最高4 GB 的内存。这很好,但,怎么做呢?
PC 启动时在实模式,限制使用16 位寄存器。并且,由于16 位的段地址。这限制了你在实模式里可以访问的内存数量,因此我们还不能访问超过1GB 的内存。甚至我们不能跨过1MB 界限!怎么做呢?我们需要打开第20 条地址线。这需要直接硬件编程,所以我们也需要了解关于硬件编程的问题。
这是我们要讨论的:
为了使用像C 语言这样的高级语言,访问超过1MB 的内存空间是必须的,因此 打开A20 ( 第20 条地址线) 是很重要的!
注意:现在我们不能访问超过1MB 的地址!否则会产生三重错误。
因为我们要讨论直接为硬件编程,所以这章要比前面的章节复杂些。别担心,在我们开发内核、设备驱动时会有更多的硬件编程经历。
准备好了吗?
对于那些一直读到这里的人来说,我曾提醒过你操作系统开发到底有多难。但我们还真的没有见到真正困难的地方。这里所以的感念都是最基本的,虽然都更近了一步。但是,事情只有会变得越来越复杂。
每个控制器都需要以特定的方式设定程序,以使其正确工作。例如, 要向硬盘写数据( 或读数据) ,你需要先确定这是一台IDE 设备还是SCSI 设备。然后要知道驱动器号。以及要用哪个IDE 控制器 或是SCSI 控制器 等等 。这些控制器都是不一样的。
再复杂些,一个“扇区”并不总是512 字节,这样“读写一个扇区”也就变得不一样了。
后面还有内存的管理,碎片,分页,虚地址空间和内存管理单元 (MMU) 等等等等。
读写驱动器又和操作其他设备不同,这在引导这一层更是如此。文件系统也会随着介质的不同而不同,所以使用FAT12 可以在软盘引导系统,而不能在一个CDFS 文件系统的CD ROM 上工作。通过抽象对硬件特殊的代码(底层代码),我们可以使多数的代码对多数设备有效。
当我们说" 将一个文件写到硬盘" 时, 我们不会定义“文件”是什么,因为我们不需要,我们不需要了解要操作的控制器,甚至也不关心写到的位置。这就是抽象很重要的原因!
这里的内容对于保护模式很重要,当然这些在实模式下也会工作的很好。要记住保护模式的规则:
... 因此,你必须做你自己的。
调试是艺术。它提供了一种方式来跟踪问题,以便在问变得严重前修复错误。内核调试要 调试内核层(环 0 )的程序,这从来都不是一个简单的工作。
像 C , C++ 语言的调试器,都提供了在运行时显示变量名、函数名、值和位置的功能,但是,问题是在我们的程序里还没有任何符号名。我们还在二进制这一层上。
这意味着我们得找一个直接显示内存的调试器。Bochs 就有这样一个。
Bochs 有个调试器叫作 bochsdbg.exe 。当你运行它的时候你会见到一个和 Bochs.exe 一样的启动界面,加载配置文件,开始模拟器。
Bochs 调试器和显示窗户就出现了,你会看到下面的显示:
[0x000ffff0] f000:fff0 (unk. ctxt): jmp f000:e05b ; ea5be000f0
< bochs:1> _
第 2 行, bochs 告诉你正在设置的命令序号 ( 这里是第 1 条命令,所以显示是 1) 。你可以在这里输入命令。
第1 行很重要。它告诉你当前指令, 绝对地址, 和段:偏移地址。也会显示机器语言的操作码 (Opcode) 。
help 命令给出一个有效命令的列表。
b (break) 命令允许你在某个内存地址设置断点。例如,如果要调试我们的操作系统,我们要在引导 (07c00:0) 的时候开始调试。因此,我们要在 0x7c00:0 设置断点,并且继续执行直到到达断点:
// BIOS 在 0xea5be000f0
[0x000ffff0] f000:fff0 (unk. ctxt): jmp f000:e05b
< bochs:1> b 0x7c00 // 在 0x7c00:0设置断点
< bochs:2> c //继续执行
< 0> Breakpoint 1, 0x7c00 in ?? < > //我们设置的断点到了
Next at t=834339
//这是引导出现的第1条指令
< 0> [0x00007c00] 0000:7c00 (unk. ctxt): jmp 7cb5 ; e9b200
上面的信息告诉我们,在我们的引导程序了的main() 函数的地址是0x7cb5 。这是因为在这条跳转指令和main ()函数之间是OEM 参数块。
我们知道stage2 被引导加载器加载到0x500 ,我们再设置断点:
< bochs:3> b 0x500
< bochs:4> c
< 0> Breakpoint 2, 0x500 in ?? < >
Next at t=934606
<0> [0x000000500] 0050:0000 (ink. ctxt): jmp 00a0 ; e99d00
< bochs:5> _
现在我们在 stag2 开始的地方了,这样我们就能一边调试一边参考我们的汇编文件了!酷!最好的是,你可以在显示窗口看到系统的输出正在动态更新。
s (Single Step) 命令 用于一次执行一条指令:
< bochs:6> s
Next at t=934607
<0> [0x0000005a0] 0050:00a0 (ink. ctxt): cli ; fa
< bochs:7> s
Next at t=934608
<0> [0x0000005a1] 0050:00a1 (ink. ctxt): xor AX, AX ; 31c0
< bochs:8> _
这条命令显示寄存器的当前值,标志寄存器,通用寄存器,测试、调试、控制和段寄存器。 GDTR, IDTR, LDTR, TR, 和 EIP 也在其中。
这条指令显示当前栈的值,当我们跟着栈时,这条命令很常用。
有更多的命令没有介绍。但是上面的是最有用的,了解如何使用调试器是很重要的,尤其是像我们这样在早期工作的时候。
这里是系统开发中比较困难的地方。
" 直接为硬件编程" 简单来说就是和直接和硬件交流。因为芯片是(在一定程度上)可编程的,我们可以控制他们。
在 第7 章 ,我们很详细的了解了系统是如何工作的。我们也讨论了软件端口如何工作,端口映射,IN 、OUT 指令, ,我也给出一张很大的表来说明在x86 体系下的一般端口映射。
无论处理器接收到IN 还是OUT 指令, 都会设置控制总线的I/O 访问位 。因为系统总线连接着内存控制器 和I/O 控制器 , 控制器会监听特定的地址及控制总线。如果I/O 访问为 为1 ( 电流通过其上) I/O 控制器会处理这个地址.
然后I/O 控制器会将这个端口地址发给所有连在其上的其他设备,并且等待一个返回的信号 ( 如果某个设备返回了信号,这表示这个端口地址属于这个设备,那么把数据交给这个设备就好了) 。 如果没有得到回复,I/O 控制器忽略这个IN/OUT 指令。
这正是端口映射所做的工作 ( 参考第7 章以获取详细信息)
一个控制器芯片可能会映射到一个端口地址范围。端口地址由BIOS POST 在BIOS 加载执行之前分配。为什么呢?大多数的设备需要不同类型的信息。一些端口代表" 寄存器", 而其他的这代表" 数据" 或是" 准备好" 端口。这不太好,我知道,而且还会更糟。在不同的系统,端口地址的变动很大,因为x86 要向后兼容,基本的设备 ( 键盘、鼠标等) 一般使用相同的地址,而更多复杂的设备不会。
为了更好的理解事情是怎么做的,我们看看控制器,毕竟我们要讨论很多关于他们的事儿——尤其是在保护模式 .
很多PC 都配置了Intel 8042 微控制器芯片 。这个控制器芯片要么嵌入到一个集成电路(IC )里,要么直接安装到主板上。它一般连在南桥 上。
这个控制器通过一个连接到你的键盘的芯片和键盘上的控制器芯片交流。
当你在键盘上按下一个键时, 就按下了键盘下面的一个橡胶块,橡胶块下面可以导电,当按下去的时候就联通了键盘上的电路,电流可以在上面流过,每个键对应一对电线,当信号改变时(取决于键是否按下),会产生一个编码(通过一系列电线),这个编码设置了键盘里的控制器,也就通过连接到计算机的硬件端口设置了计算机里面的那个控制器,这是通过一系列高低电脉冲实现的,根据时钟周期,每个脉冲信号被转换为位模式中的一位。
在主板上,这些通过南桥上的电信号到达8042 控制器,控制器将上面的编码解码得到扫描码,并保存在内部寄存器里,就是我们说的缓冲区,这个内部的寄存器是EEPROM 芯片,可以通过电擦洗,重写入我们需要的如何数据。
引导的时候,BIOS POST 为每个设备( 通过I/O 控制器) 分配端口地址。这是通过查询设备完成的。一般的,BIOS POST 会将这个内部寄存器分配到端口地址 0x60. 这表示, 当我们访问端口0x60 时, 我们实际上是从这个内部寄存器读数据。
你知道关于端口映射和IN/ OUT 指令的其他内容,我们从这个寄存器读数据试试:
in al, 0x60 ; 从8042 微控制器输入寄存器读数据
你可能会猜到, 8042 微控制器是键盘控制器 . 通过与这个芯片的一系列寄存器交流,我们可以读到键盘的输入,映射扫描码,以及其他的一系列操作:比如开启 A20.
你可能会好奇为什么需要和键盘控制器交流,来开启A20 。在后面你会看到。
最后我们讨论如何开启 A20 ,我知道,我知道,上面的内容和 A20 没有太多直接的关系,但是我还是想要在开启 A20 前,包含直接为硬件编程的基本内容,因为开启 A20 需要,其他控制器编程也需要。
开启A20 需要为键盘控制器编程,因此,我们只是讨论为键盘控制器编程而不是为键盘编程。
在IBM 设计 IBM PC AT 时,他们使用了新的Intel 80286 处理器 , 而这个处理器与前面的x86 处理器在实模式下不兼容。什么问题呢?旧的x86 处理器不使用A20 到A31 的地址线,也没有这么多条地址线。 所有的超过1 MB 的地址被回卷了。 而在80286 的地址空间里,需要32 条地址线,也就是说全部的32 条地址线都可以被访问,我们就有了回卷问题。
为了解决这个问题Intel 在处理器和系统总线之间加了一个逻辑门以控制第20 条地址线,这个逻辑门就叫做 Gate A20 。对于旧的程序可以关闭A20 ,对于新的则打开A20 。
在引导的时候,BIOS 会在计算和测试内存时打开A20 ,而在将控制权交给操作系统之前再关上。
有很多种方法来开启A20. 当A20 打开之后,我们就可以访问全部的32 条地址总线, 这样通过使用32 为的地址, 最大到0xFFFFFFFF - 4 GB 内存.
Gate A20 是一个逻辑或门,原来连在8042 控制器( 键盘控制器) 的P21 线上。这个逻辑门对应数据输出端口的第1 位 ,我们可以发送命令来接收数据或是修改它。通过检查这一位,并向输出线上些数据,我们就能控制这个逻辑门,以开启/ 关闭A20 。我们可以直接或是间接完成这个工作。下一节里会详细介绍。
在引导时,BIOS 在测试内存时会开启A20 而后为了向旧的计算机兼容再关闭上,因此默认的对于我们的操作系统A20 是关上的。
有多种方法来重新打开A20 ,这要看主板的配置。我会涉及多种开启A20 的常见方法。
下面是详细内容;)
有很多方法开启 A2 。如果你只是写一个只有你用的系统,你要做的只是找出一种你能用的就成,如果要求可移植性的话,就要多用几种方法了。
这个很快,但是可移植不好。
Sone 系统包括 MCA 和EISA 使用系统控制端口I/O 0x92 控制A20 。制造商对于端口0x92 有非常好的实现。下面是常用的几位:
这是用这种方法启动 A20 的例子 :
mov al, 2 ; 设置第2位(开启a20)
out 0x92, al
用这个端口也可以做其他是一些事情 :
mov al, 1 ; 设置第1位(快速重设)
out 0x92, al
这个方法在 Bochs 中可以使用。
虽然这是一个简单的方法。我发现这种方法与一些硬件会发生冲突。可能会导致系统停机,如果你想用方法,注意一些。
一些系统允许使用其他的 I/O 端口来启动 A20.
最常见的是I/O 端口 0xEE 。如果I/O 端口 0xEE (" 快速 A20 门") 在这个系统有效,从这个端口读数据会开启A20, 写数据会关闭A20 。相似的端口 0xEF (" 快速CPU 重设") 会重置系统。
其他系统会使用不同的端口(ie; AT&T 6300+ 需要写0x90 到I/O 端口 0x3f20 来开启A20, 还有写0 关闭 A20). 也有些 系统使用 I/O 端口0x65的第2位或是 I/O 端口0x1f8的第0位开启或关闭 A20 (0: 关, 1:开).
如你所见,A20的操作多数与硬件相关,所以你要先确定你的主板制造商。
很多 Bios 中断可以开启或是关闭 A20.
一些版本的 Bochs 可以使用这些方法而其他的版本并不支持 .
这个方法可以关闭 A20 。很容易用:
mov ax, 0x2400
int 0x15
Returns:
CF = 如果成功,为0
AH = 0
CF =如果发生错误为1
AH = 状态 (01=键盘控制器处在安全模式, 0x86=不支持这个方法)
这个方法开启 A20
mov ax, 0x2401
int 0x15
Returns:
CF = 如果成功,为0
AH = 0
CF =如果发生错误为1
AH = 状态 (01=键盘控制器处在安全模式, 0x86=不支持这个方法)
这个方法返回 A20 当前的状态。
mov ax, 0x2402
int 0x15
Returns:
CF = 如果成功,为0
AH = 状态 (01=键盘控制器处在安全模式, 0x86=不支持这个方法)
AL = 当前状态 (00: 关, 01: 开)
CX =如果键盘控制器在0xc000读尝试时未准备好被设为
CF = 错误时为1
这个方法询问 系统 对于A20 支持 .
mov ax, 0x2403
int 0x15
Returns:
CF =如果成功,为0
AH = 状态 (01=键盘控制器处在安全模式, 0x86=不支持这个方法)
BX =状态.
BX 包含一个位模式:
这可能是最常见的开启 A20 的方法,简单,但要求键盘控制器编程的知识。这也最有可移植性的方法。因为有键盘控制器编程的要求,我先介绍一下。
这也是我要先介绍硬件编程的原因。这是我们第一次介绍如何直接为硬件编程, 别担心,还不算难,我们会慢慢的增加难度:)
为了与控制器交流 , 我们得知道这个控制器的 I/O 端口映射。
这个控制器的端口映射如下:
端口映射 |
||
端口 |
读 / 写 |
描述 |
0x60 |
读 |
读输入缓存 |
0x60 |
写 |
写输出缓存 |
0x64 |
读 |
读状态寄存器 |
0x64 |
写 |
给控制器发命令 |
要给这个控制器发送命令,将命令写到端口0x64 。 如果这个命令要接收一个参数,参数写到端口0x60 。命令的所有返回值都可以从端口0x60 读回。
要注意键盘控制器很慢。因为我们的代码执行要比键盘控制器快得多, 我们需要一种方法来等待控制器完成它的工作,在继续我们的代码。
这经常使用检测控制器状态的方法实现。很困惑?别担心,后面会解释清楚的。
好,我们怎么得到控制器的状态呢?看看上面的表,我们知道从 0x64 读数据就行了。从这个寄存器读出的值有 8 为,格式如下:
如你所见,有很多东西,其中重要的都在上面加粗显示了,我们要检测控制器的输出、输入缓冲区是满的还是空的。
这是一个例子。我们向控制器发命令时,命令写到控制器的输入缓冲区。因此当输入缓冲区满的时候就不能执行,就像这样:
wait_input:
in al,0x64 ;读状态寄存器
test al,2 ; 超时第2位 (输入缓冲区状态)
jnz wait_input ; 如果非0(不空)跳转,继续等
在输入输出的时候都要这么做。
现在我们知道怎么等控制器了, 我们必须要告诉控制器我们要做什么。我们看看“命令字”。
再看看 I/O 端口映射表,我们可以通过写命令到 I/O 端口 0x64 来发送命令。
键盘控制器有很多命令,因为这不是键盘编程的教材, 我没有吧它们全列出来,但是下面的表中是最重要的:
键盘控制器的命令字 |
|
键盘 命令 |
描述 |
0x20 |
读键盘控制器命令字 |
0x60 |
写键盘控制器命令字 |
0xAA |
自检 |
0xAB |
接口检测 |
0xAD |
使键盘无效 |
0xAE |
使键盘有效 |
0xC0 |
读输入端口 |
0xD0 |
读输出端口 |
0xD1 |
写输出端口 |
0xDD |
开启 A20 地址线 |
0xDF |
关闭 A20 地址线 |
0xE0 |
读测试输入 |
0xFE |
系统重设 |
Mouse 命令 |
描述 |
0xA7 |
使鼠标端口失效 |
0xA8 |
使鼠标端口有效 |
0xA9 |
测试鼠标端口 |
0xD4 |
写鼠标 |
再说一次,这不是全部的命令,我们会在后面再说。
注意到命令 0xDD 和 0xDF 用于开启 / 关闭 A20 :
; 方法 3.1: 通过键盘控制器开启A20
mov al, 0xdd ; 命令 0xdd: 开启a20
out 0x64, al ; 发送命令到控制器
不是所有的键盘控制器都支持这个方法,如果有效的话,它确实很简单 ;)
也有使用键盘控制器的输出端口开启 A20 的,这样我们需要使用命令 D0 和 D1 来读、写输出端口 ( 参考键盘控制器的命令字 )
这种方法比起其他的要复杂的多,但也不太差。基本的想法是,停止键盘,并且读键盘控制器的输出端口。8042 有3 个端口s: 一个输入,其他的是输出,第3 个用于检测,这些" 端口" 实际上就是控制器上的硬件线路。
简单起见( 因为这不是键盘编程教材), 我们只看输出端口。
好的,从输出端口读数据,简单的发送一个读输出端口的命令(0xD0) 到控制器就行了: ( 参考键盘控制器的命令字)
; 读输出端口到al
mov al,0xD0
out 0x64,al
现在我们得到了输出端口的数据,但他不是太有用,实际上输出端口的格式如下,一个特定的位模式。
其中的大多数位我们不希望改变,将第0 为置0 会重启计算机;第1 位置1 会开启A20 ,你可以通过将这个值与某一位为1 的掩码做或操作设置特定的位,设好后,写回去就行了( 命令字0xD1).
写数据到输出端口,从输入、输出缓冲区取得控制器的数据。
这表示,如果我们要读输出端口, 数据要从控制器的输入缓冲区寄存器读 看看I/O 端口映射表,我们知道要从端口0x60 读数据。
看个例子,在读写操作时,我们要等待控制器完成操作。wait_input 用于等待输入缓冲区变空,而wait_output 等待输出缓冲区变空。
; 发送读输出端口命令
mov al,0xD0
out 0x64,al
call wait_output
; 读输入缓存区,并保存在栈中,数据从输出端口读入
in al,0x60
push eax
call wait_input
; 发送写输出端口命令
mov al,0xD1
out 0x64,al
call wait_input
; 从栈中弹出输出端口的数据,并设置第1位 (A20)
pop eax
or al,2 // 2 = 10(二进制)
out 0x60,al // 写数据到输出端口. 这通过输出缓冲区完成
以上是这个方法的全部 :) 这个方法比其他的要复杂但有更好的可移植性。
因为我们在虚拟机上运行,下面的多数情况都不会在 Bochs 下发生,但是对于真正的硬件就有麻烦了。
如果控制器执行了错误的命令,一般会做一些你不想去做的事儿,(比如从一个端口读数据而不是你想要的写)这可能会损害你的数据。例如 , 使用 in al, 0x61 而不是 in al, 0x60 , 就会读不同的寄存器 , 而不是期望的状态寄存器 ( 端口 0x60).
多数控制器忽视那些它不知道的命令,并把它扔掉 ( 清空命令寄存器,可能是这样 ) 。
一些控制器可能失灵,看看失灵那一节。
很少会发生,但是这是可能会发生的,有两个例子都和 Pentium 处理器相关,包括 FDIV 和 foof 错误。 FDIV 错误是 cpu 设计的问题,处理器 FPU 会给出错误的结果。
foof 更严重。当处理器处理命令0xf0 0x0f 0xc7 0xc8, 这是一条Hault and Catch Fire (HCF) 指令(一条没有文档定义的指令)。多数这样的指令会锁定处理器,并强制重启。在使用这样的指令时会发生不正常的边界效应。
要知道这些问题可能会发生,当他们发生的时候,控制器不会产生异常。
多数的控制器异常都是硬件" 设计失误" 的结果。
虽然少见,但也可能会由于软件的原因损坏硬件。一个简单例子是软盘驱动器,你可以通过软盘驱动器控制器 (FDC) 直接控制软盘驱动器的马达,当忘记关闭马达会导致软盘驱动器快速用旧和损坏,小心。
当控制总线产生一个问题的时候,会导致处理器产生异常,当然可能会导致重启。
当控制器有问题, Bochs 会产生三重错误,并且记录日志。
例如, 如果你发送一个未知命令 ( 如0) 给键盘控制器:
mov al, 0x00 ; 随便的一个命令
out 0x64, al ; 发送该命令给控制器
Bochs 会产生三重错误,并记录日志:
[KBD ] unsupported io write to key board port 64, value = 0
"KBD" 表示这条日志是有键盘控制器设备记录的。
所有 A20 的 代码在A20.inc 中。我写了几个不同的函数实现了使用不同的方法开启A20 ,所以如果一种方法失效了,就换一种。
为了减少复杂性,我决定,以下载的方式提供这章的演示Demo 当前的Stage2.asm 没改多少。
因为这个演示在显示上没有新东西,所以就没有截屏图片。
想看看.
从这里 下载(*.ZIP: 8KB).
啊,这章要比我想象的更大一些。
我们了解了很多新概念。我们也介绍了硬件编程。记住:在保护模式只能和硬件直接交流! 没有中断,没有BIOS ,一切要靠我们自己。
现在, 你可能开始感激Windows 了:) 毕竟,它做了很多困难的工作。
如果你不全部了解也别太担心——这很复杂,我知道。当我们开始编写内核的时候, 我们会有一整章来讨论为键盘控制器编程,并且将位它写个驱动程序,酷,不是吗?
下一张很很简单,我们对于保护模式的讨论暂告段落,我们要返回实模式。我们要增加FAT12 加载代码来加载我们的内核,A20 打开了,我们可以将它放在1MB!
还有我们会取一些BIOS 信息,还有一些其他的东西:) 等你。
下次见