stdlf
分类: 服务器与存储
2011-08-24 13:26:23
在许多PC的CMOS设置里,都有一项叫做“A20 Enable“的设置,不知道大家是否就此设置困惑过,这个A20是什么呢?
说A20则不得不说PC机的内存,我尽量用简短的语言说明白PC机内存的一些独有的术语,在我刚接触PC机的时候(1985年前后),如果你的机器有一块10M或者20M的硬盘,那已经是一台不错的机器,如果你的机器有256K的内存,那绝对是高配置,那时候还没有3.5"的软盘,都是使用5"的容量仅360K的软盘,所以那个时候,设计IBM PC的IBM公司非常自信地认为,1M内存是一个根本达不到的天文数字,我想由于这种思想的作怪(我猜的),IBM非常愚蠢地把PC机1M存储空间的最上面的384K用作了ROM和系统设备,这种设计给现在的PC机的内存结构埋下了麻烦的伏笔,如果当初IBM把PC机的1M存储空间的最下面的384K用作ROM和系统设备,可能现在就会少好多麻烦,这个后面说。
写到这里的时候,我想起了许多事,大家是否知道我国的第一台电子计算机是什么型号?是什么样子?是1958年生产的103机,后来又有了全晶体管的电子计算机,很荣幸的是,在我工作过的单位里,曾经有过这么一台全晶体管的电子计算机,就是那种使用穿孔纸带输入计算机程序,占地好几百平方米,一旦开动需要几十人进行维护的家伙,我到这个单位的时候,这台机器早已不再运转的,80年代的时候,我有幸参加了这台机器的拆解工作,给我印象最深的是,我终于看到了当时在教科书里说的“磁鼓”(估计现在的教科书里也没有了),一种看上去像筛子一样的东西,那就是当年的存储器,用于在纸带上打孔进行程序输入的穿孔机我用过,样子很像老式的英文打字机。还有那种8英寸的软盘,估计现在也很少有人见过了。
好了言归正传,大家都知道,8088/8086只有20位地址线,按理它的寻址空间是2^20,应该是1024KB,但PC机的寻址结构是segment:offset,segment和offset都是16位的寄存器,最大值是0ffffh,换算成物理地址的计算方法是把segment左移4位,再加上offset,所以segment:offset所能表达的寻址空间最大应为0ffff0h + 0ffffh = 10ffefh(前面的0ffffh是segment=0ffffh并向左移动4位的结果,后面的0ffffh是可能的最大offset),这个计算出的10ffefh是多大呢?大约是1088KB,就是说,segment:offset的地址表达能力,超过了20位地址线的物理寻址能力,你说这是不是有点麻烦。在早先,由于所有的机器都没有那么大的内存,加上地址线只有20位,所以当你用segment:offset的方式企图寻址100000h这个地址时,由于没有实际的第21位地址线,你实际寻址的内存是00000h的位置,如果你企图寻址100001h这个地址时,你实际得到的内容是地址00001h上的内容,所以这个事对实际使用几乎没有任何影响,但是后来就不行了,出现了80286,地址线达到了24位,使segment:offset寻址100000h--10ffefh这将近64K的存储器成为可能,为了保持向下兼容,于是出现了A20 Gate,这是后话,我们后面再细说。
我们可能经常听到一些只有在PC机上才有的一些关于存储器的专有名词,包括:常规内存(Conventional Memory)、上位内存区(Upper Memory Area)、高端内存区(High Memory Area)和扩展内存(Extended Memory),我尽量把这几个东东说明白,这需要下面这张著名的图。
这张图很清楚地说明了问题,大家都知道,DOS下的“常规内存”只有640K,这640K就是从0--A0000H这段地址空间;所谓“上位内存区”,指的就是20位地址线所能寻址到的1M地址空间的上面384K空间,就是从A0001H--100000H这段地址空间,也就是我们说的用于ROM和系统设备的地址区域,这384K空间和常规内存的640K空间加起来就是20位地址线所能寻址的完整空间1024KB;由于80286和80386的出现使PC机的地址线从20位变成24位又变成32位,寻址能力极大地增加,1M以上的内存寻址空间,我们统称为“扩展内存”;这里面绝大部分内存区域只能在保护模式下才能寻址到,但有一部分既可以在保护模式下,也可以在实模式下寻址,这就是我们前面提到过的地址100000h--10ffefh之间的这块内存,为了表明其特殊性,我们把这块有趣的内存区叫做“高端内存”。
前面我们提过由于IBM的愚蠢设计给PC机的内存结构埋下了麻烦的伏笔,现在我们来说说这个麻烦。我们都见过PC机上的内存条,但是由于上位内存区的存在,这个内存条上的地址居然不能连续,就是说,这个内存条上要有0--A0000H的地址空间,还要有100000h--最大内存容量的地址空间,中间的384K地址空间必须留出来给ROM用,在现如今一个芯片就好几兆的情况下,你说这个内存条应该怎么做,当然我相信一定是可以做出来的,但肯定很麻烦,如果当初IBM把这个“上位内存区”放在地址低端,就是0--6000h这一部分,岂不是这个麻烦就没有了?!
但是,实际的内存条上地址都是连续的,并没有人把这段地址空间留出来给ROM使用,原因很简单,采用技术手段把这段地址空间空出来,比浪费这384K内存的成本还要高,所以在这个地址区域就出现了很奇怪的现象,ROM和RAM的地址重叠。 实际上,往往ROM并不能完全覆盖整个384K区域,这样就会有一些地址没有被ROM占用,那么这部分地址上的RAM仍然是可以使用的。实际上,和ROM重叠的这384K RAM一般也不会被浪费,说到这里,不得不说所谓的ROM Shadowing了,RAM和ROM的性能是有很大差异的,RAM的存取速度要远远大于ROM,而且RAM可以32位存取,ROM通常只能16位,所以目前的PC机对这块RAM和ROM重叠的区域的处理采用一种ROM Shadowing的技术方式,当机器加电后,先让ROM有效,RAM无效,然后读出ROM内容,再让ROM无效,RAM有效,把读出的ROM内容放到相同地址的RAM中,并把相应位置的RAM设定为只读,这样就把ROM搬到了RAM中,地址完全一样,只是性能比使用ROM要高些,这块RAM就好像ROM的Shadow一样。
回到我们的主题A20 Gate,出现80286以后,为了保持和8086的兼容,PC机在设计上在第21条地址线(也就是A20)上做了一个开关,当这个开关打开时,这条地址线和其它地址线一样可以使用,当这个开关关闭时,第21条地址线(A20)恒为0,这个开关就叫做A20 Gate,很显然,在实模式下要访问高端内存区,这个开关必须打开,在保护模式下,由于使用32位地址线,如果A20恒等于0,那么系统只能访问奇数兆的内存,即只能访问0--1M、2-3M、4-5M......,这显然是不行的,所以在保护模式下,这个开关也必须打开。
2.1 先看看 real mode 的寻址方法 8086/8088 的地址线有 20 条:A0 ~ A19,意味着 processor 可以将 20 位地址放上这 20 条地址线上,它的寻址能力是 1M (00000 ~ FFFFF),它的寻址方法是:segment:offset,这是一种被称为 logic address(逻辑地址)表示法,它需要转化为 processor 的 linear address(线性地址)表示: segment:offset ---------> segment << 4 + offset 如:F000:FFFF = F0000 + FFFF = FFFFF,这是 8086/8088 所能访问的最高地址。这种表示方法是 Intel 为了在 16 位 real mode 下能够访问 20 位地址空间所想设计出来的计算方式。 因此,8086/8088 的寻址范围是可以表示为:从 0000:0000 ~ 0000:FFFF 开始到 F000:0000 ~ F000:FFFF 在后续的 80286 上,Intel 实现了 24 位的 Address bus,那么在 real mode 下 80286 能够访问到的最高地址是 10FFEF,这个地址值是由下面的方法而来: FFFF:FFFF = FFFF0 + FFFF = 10FFEFh 这已经是 logic address 所能表达的极限范围了。100000h 以上的内存被称为 extend memory,从 100000h ~ 10FFEFh 这片内存区域在 DOS 下被称为 High Memory(高端内存)。高端内存是 80286 在 real mode 所能访问到的区域,而 8086/8088 所不能访问到的。 当在 8086/8088 下执行 FFFF:FFFF 这个内存寻址时,会产生什么结果呢? 结果很明显:由于 8086/8088 只有 20 条 address bus,地址 10FFEF 的高 4 位会被抛弃,实际上送上 address bus 的只有 0FFEFh 值,所以访问 FFFF:FFFF 地址结果只能访问到 1M 以内的地址。这就是 wraparound 现象:访问 1M 以上地址都会回绕到 1M 内的模值。 那么,当 80286 下访问 FFFF:FFFF 地址时,又会产生什么果呢? 由于 80286 具有 24 条 address bus,对于 FFFF:FFFF 地址的访问,会正确得到访问。 SO,访问 FFFF:FFFF 内存,使得 8086/8088 下产生 wraparound 现象,变相访问 0FFEF 地址内存。而在 80286 下得到正确的的 10FFEF 地址,不存在 wraparound 现象。因此:wraparound 现象在 8086/8088 才会产生。这样产生的问题是:访问高端内存时,80286 在 real mode 下和 8086/8088 的行为不一致! 为了使用 80286 和 8086/8088 在 real mode 下的行为一致,即:在 80286 下也产生 wraparound 现象。IBM 想出了古怪方法:当 80286 运行在 real mode 时,将 A20 地址线(第 21 条 address bus)置为 0 ,这样使得 80286 在 real mode 下第 21 条 address line 无效,从而人为造成了 wraparound 现象。 具体实现方法是: 设立一个 AND Gate(与门电路),AND gate 的 IN 输入端中一端接 A20 line 上,另一端接在 keyboard control 8042 上,而 AND gate 的 OUT 输出端接在 A20 line 上。只有两个 IN 端都为 1 时,OUT 端才为 1 A20 line 一直处于 1 状态(High 电平),而 8042 内的 A20 gate 一直处于 0(Low 电平),因此:必须使 Keyboard controller 8042 内的 A20 Gate 处于 high 时,A20 line 输出才有效。 A20 gate 也被称为 A20 MASK#。 SO,Keyboard Controller 8042 增加了一组命令去控制 A20 Gate 的开/关,给 8042 发送命令 0xDF 置 A20 gate 有效,给 8042 送命令 0xDD 置 A20 gate 无效。 现在的 system 中,南桥芯片的 A20 MASK# 缺省都是 MASK 状态,即:A20 gate 缺省都是开的。 在 OS 的 boot 阶段一般都要做打开 A20 gate 操作,虽然现在 A20 gate 缺省为开的。 打开 A20 gate 的方法最原始的是给 keyboard controller 8042 发送 A20 gate enable 命令字,就是上面所说的 0xDF 命令。 下面是我在 mouseOS 操作系统项目中 keyboard 驱动里的一小段代码(编译器是 nasm): ;----------------------------------- 这个代码实现很简单,向 0x64(I8402 write 端口)发送 0xDF 命令,这是控制 keyboard controller I8402 A20 gate 的其中一种方法。 另一种方法是:通过写 I8402 的 output 端口,直接置 I8402 的 pin 21(对应于 A20 gate line),同样来自 mouseOS 中的代码(nasm 编译器) ;----------------------------------- ;----------------------------------- ;------------------------------------------------------ ;----------------------------------------- ; 2: read byte from 8408 (0x60) ; 3: send write_output_port command to 8402 (0x64) ; 4: set A20 gate line ; 5: send to data port (0x60) 在 __enable_a20() 代码中,首先向 8402 发送 read output port 命令,然后从 0x60(8408 的数据端口)读出 output port 里的数据,将其它的 bit1 置为 1。再向 8402 发出 write output port 命令,将该数据写向 0x60(8408 的 data port)。 上面的方法都是基于 keyboard controller 来实现的,这是比较正统的实现方法。一个新的 fast A20 gate 方法,但是这种方法不是通用的方法。在一些系统可能得不到正确的结果 ;---------------------------------------- 打开 A20 gate 是为了在 80286/286+ 以后的 processor 上使用 protected mode 来访问完全的 24/24+ 位地址空间,如:在 32 位 protected mode 下,在不打开 A20 gate 的情况下,Bit20 为 0,导致 Bit20 留下一个空位。
; macro: WAIT_STATUS_FOR_WRITE
;-----------------------------------
%macro WAIT_STATUS_FOR_WRITE 0
%%wait_status_for_write_loop:
IO_DELAY
in al, I8402_STATUS_PORT
bt rax, 1
jc %%wait_status_for_write_loop
%endmacro
;-------------------------------------------------------
; macro: WRITE_BYTE_TO_I8402
; description:
; send byte to keyboard contrroler (I8402)
;-------------------------------------------------------
%macro WRITE_BYTE_TO_I8402 1
WAIT_STATUS_FOR_WRITE
mov al, %1
out I8402_COMMAND_PORT, al
%endmacro
;-----------------------------------------
; keyboard_enable_a20()
;-----------------------------------------
__keyboard_enable_a20:
WRITE_BYTE_TO_I8402 I8402_ENABLE_A20_CMD ; send 0xDF to I8402
ret
; macro: WAIT_STATUS_FOR_WRITE
;-----------------------------------
%macro WAIT_STATUS_FOR_WRITE 0
%%wait_status_for_write_loop:
IO_DELAY
in al, I8402_STATUS_PORT
bt rax, 1
jc %%wait_status_for_write_loop
%endmacro
; macro: WAIT_STATUS_FOR_READ
;-----------------------------------
%macro WAIT_STATUS_FOR_READ 0
%%wait_status_for_read_loop:
IO_DELAY
in al, I8402_STATUS_PORT
bt rax, 0
jnc %%wait_status_for_read_loop
%endmacro
;-------------------------------------------------------
; macro: WRITE_BYTE_TO_I8402
; description:
; send byte to keyboard contrroler (I8402)
;-------------------------------------------------------
%macro WRITE_BYTE_TO_I8402 1
WAIT_STATUS_FOR_WRITE
mov al, %1
out I8402_COMMAND_PORT, al
%endmacro
;------------------------------------------------------
; macro: WRITE_BYTE_TO_I8408
; description:
; send byte to keyboard encoder (I8408)
;------------------------------------------------------
%macro WRITE_BYTE_TO_I8408 1
WAIT_STATUS_FOR_WRITE
mov al, %1
out I8408_COMMAND_PORT, al
%endmacro
; macro: READ_BYTE_FROM_I8408
; description:
; read byte from keyboard encoder (I8408)
;------------------------------------------------------
%macro READ_BYTE_FROM_I8408 0
WAIT_STATUS_FOR_READ
in I8408_DATA_PORT
%endmacro
; __enable_a20() with 0xD0/0xD1
;-----------------------------------------
__enable_a20:
; 1: send read_output_port command to 8402 (0x64)
WRITE_BYTE_TO_I8402 I8402_READ_OUTPUT_CMD
READ_BYTE_FROM_I8408
mov ah, al
WRITE_BYTE_TO_I8402 I8402_WRITE_OUTPUT_CMD
mov al, ah
or al, 2
WRITE_BYTE_TO_I8408 al
ret
2.6 打开 A20 gate 的必要性
; macro: A20_ENABLE
; descriptor:
; Fast A20 gate enable
;----------------------------------------
%macro A20_ENABLE 0
in al, SYSTEM_CONTROL_PORT ; port - 0x92
or al, 0x02
out SYSTEM_CONTROL_PORT, al
%endmacro