回到go_to_protected_mode()的110行,调用一个enable_a20()函数。这里又用到“PC汇编及BIOS编程”的知识了。PC及其兼容机的第21根地址线(A20)较特殊,这就是“Intel 80286工作模式” 提到的PC中安排的一个“门”控制该地址线是否有效。到了80286,系统的地址总线有原来的20根发展为24根,这样能够访问的内存可以达到2^24=16M。Intel在设计80286时提出的目标是向下兼容。所以,在实模式下,系统所表现的行为应该和8086/8088所表现的完全一样,也就是说,在实模式下,80286以及后续系列,应该和8086/8088完全兼容。但最终,80286芯片却存在一个BUG:因为有了80286有A20线,如果程序员访问100000H-10FFEFH之间的内存,系统将实际访问这块内存,而不是象8086/8088一样从0开始。我们来看一副图:
为了解决上述兼容性问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根) 的有效性,被称为A20 Gate:
1 如果A20 Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;
2 如果A20 Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式即取模方式(8086仿真)。绝大多数IBM PC兼容机默认的A20 Gate是被禁止的。现在许多新型PC上存在直接通过BIOS功能调用来控制A20 Gate的功能。
上面所述的内存访问模式都是实模式,在80286以及更高系列的PC中,即使A20 Gate被打开,在实模式下所能够访问的内存最大也只能为10FFEFH,尽管它们的地址总线所能够访问的能力都大大超过这个限制。为了能够访问10FFEFH以上的内存,则必须进入保护模式。
那么,如何打开这个“门”呢?这就是enable_a20()函数干的事情,来自
linux/arch/x86/boot/a20.c:
/* 125 * Actual routine to enable A20; return 0 on ok, -1 on failure 126 */ 127 128#define A20_ENABLE_LOOPS 255 /* Number of times to try */ 129 130int enable_a20(void) 131{ 132 int loops = A20_ENABLE_LOOPS; 133 int kbc_err; 134 135 while (loops--) { 136 /* First, check to see if A20 is already enabled 137 (legacy free, etc.) */ 138 if (a20_test_short()) 139 return 0; 140 141 /* Next, try the BIOS (INT 0x15, AX=0x2401) */ 142 enable_a20_bios(); 143 if (a20_test_short()) 144 return 0; 145 146 /* Try enabling A20 through the keyboard controller */ 147 kbc_err = empty_8042(); 148 149 if (a20_test_short()) 150 return 0; /* BIOS worked, but with delayed reaction */ 151 152 if (!kbc_err) { 153 enable_a20_kbc(); 154 if (a20_test_long()) 155 return 0; 156 } 157 158 /* Finally, try enabling the "fast A20 gate" */ 159 enable_a20_fast(); 160 if (a20_test_long()) 161 return 0; 162 } 163 164 return -1; 165} |
enable_a20()函数返回0,则直接到go_to_protected_mode的116行,否则重启。下面重点来分析一下enable_a20()函数。函数首先进行一个最大循环为255次的有限循环中。然后不断调用探测函数a20_test_short()和a20_test_long():
/* Returns nonzero if the A20 line is enabled. The memory address 50 used as a test is the int $0x80 vector, which should be safe. */ 51 52#define A20_TEST_ADDR (4*0x80) 53#define A20_TEST_SHORT 32 54#define A20_TEST_LONG 2097152 /* 2^21 */ 55 56static int a20_test(int loops) 57{ 58 int ok = 0; 59 int saved, ctr; 60 61 set_fs(0x0000); 62 set_gs(0xffff); 63 64 saved = ctr = rdfs32(A20_TEST_ADDR); 65 66 while (loops--) { 67 wrfs32(++ctr, A20_TEST_ADDR); 68 io_delay(); /* Serialize and make delay constant */ 69 ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr; 70 if (ok) 71 break; 72 } 73 74 wrfs32(saved, A20_TEST_ADDR); 75 return ok; 76} 77 78/* Quick test to see if A20 is already enabled */ 79static int a20_test_short(void) 80{ 81 return a20_test(A20_TEST_SHORT); 82} 83 84/* Longer test that actually waits for A20 to come on line; this 85 is useful when dealing with the KBC or other slow external circuitry. */ 86static int a20_test_long(void) 87{ 88 return a20_test(A20_TEST_LONG); 89} |
一路走来的同志们看这些代码应该不难,就是用fs配合gs来测试A20 Gate是否被打开。我们在之前已经提到,如果A20 Gate被打开了,则在实模式下,程序员可以直接访问100000H~10FFEFH之间的内存,如果A20 Gate被禁止,则在实模式下,若程序员访问100000H~10FFEFH之间的内存,则会被硬件自动转换为0H~0FFEFH之间的内存,所以我a20_test函数就是利用这个差异来检测A20 Gate是否被打开。
首先,61、62行,把fs和gs的值分别设置为0x0000和0xffff。然后64行调用rdfs32函数得到0000: 4*0x80的内容并存放到32位的临时变量saved和ctr中。4*0x80=0x200,所以saved和ctr中存放的是内存地址0x200处的值。对应内核映像解压缩后的内存布局,我们知道这个地址属于BIOS的一些数据存放的区域,虽然我不知道这个地址到底是存的什么数据,但是可以肯定的是,这个数据可以随意修改,来做我们的A20测试的。
67行进入循环,使ctr加1,具体等于多少不知道,把这个32位的值写入0000: 4*0x80对应的内存单元中。69行,“^”是按位异或运算符,即如果ffff: 4*0x80+0x10内存单元存放的值与ctr相等,则说明有可能刚刚写入的ctr其实是写入的0000: 4*0x80内存单元。注意,ffff: 4*0x80+0x10换算实模式地址就是ffff0+4*0x80+0x10=0x100200,不过也不排除偶然的情况,这两个内存单元相等。所以继续循环,修改一下ctr的值,多试几次。
74行,跳出循环后,你刚才给人家0x200的地址对应的内存修改了数据,得给人家改回去啊,最后返回ok。如果执行了loops次这个ok都是0,就说明A20肯定没有打开。
回到enable_a20函数,如果bootloader没有已经关闭了A20的话(grub是肯定关闭了的),也就是第一个a20_test_short()没有成功,试试142行enable_a20_bios():
91static void enable_a20_bios(void) 92{ 93 struct biosregs ireg; 94 95 initregs(&ireg); 96 ireg.ax = 0x2401; 97 intcall(0x15, &ireg, NULL); 98} |
嗯,没问题,调用BIOS的15号服务程序,打开A20。如果没成功,执行147行代码:
18#define MAX_8042_LOOPS 100000 19#define MAX_8042_FF 32 20 21static int empty_8042(void) 22{ 23 u8 status; 24 int loops = MAX_8042_LOOPS; 25 int ffs = MAX_8042_FF; 26 27 while (loops--) { 28 io_delay(); 29 30 status = inb(0x64); 31 if (status == 0xff) { 32 /* FF is a plausible, but very unlikely status */ 33 if (!--ffs) 34 return -1; /* Assume no KBC present */ 35 } 36 if (status & 1) { 37 /* Read and discard input data */ 38 io_delay(); 39 (void)inb(0x60); 40 } else if (!(status & 2)) { 41 /* Buffers empty, finished! */ 42 return 0; 43 } 44 } 45 46 return -1; 47} |
0x64号端口号对应的是键盘控制器(keyboard controller, KBC)的状态寄存器,首先30行获得该控制器的状态值,保存到内部变量status中。status不能为0xff,否则出错返回。其次,status的最低位D0如果被设置,则说明键盘缓存中还有数据,则通过inb(0x60)从键盘缓存对应的端口读出来,并且置空。
介绍一下键盘控制器:主板的一个芯片。通过LPC总线和南桥相连。一般作用:键盘控制、LCD明暗度的调节、低级电源的管理、风扇、蓝牙等一些小功能。
注意这个逻辑关系,如果键盘缓存中没有内容,并且status的次低位D1不为1,则说明键盘缓存是空的,这时候empty_8042返回0,enable_a20再一次检测一下BIOS然后来到了153行,调用enable_a20_kbc()函数,利用键盘控制器来尝试打开A20:
100static void enable_a20_kbc(void) 101{ 102 empty_8042(); 103 104 outb(0xd1, 0x64); /* Command write */ 105 empty_8042(); 106 107 outb(0xdf, 0x60); /* A20 on */ 108 empty_8042(); 109 110 outb(0xff, 0x64); /* Null command, but UHCI wants it */ 111 empty_8042(); 112} |
如果还打不开A20,没辙了,就来到enable_a20()的159行进行最后的努力,调用enable_a20_fast()函数。
114static void enable_a20_fast(void) 115{ 116 u8 port_a; 117 118 port_a = inb(0x92); /* Configuration port A */ 119 port_a |= 0x02; /* Enable A20 */ 120 port_a &= ~0x01; /* Do not reset machine */ 121 outb(port_a, 0x92); 122} |
这个函数是个熟面孔,在我们的博文“保护模式编程实例”中,那个EnableA20汇编宏就是干的这么一个事情,将0x92端口的D1位置位。
注意这个enable_a20_fast,基于x86的主板都提供0x92端口来作为一个主板控制寄存器,所以,如果这个函数执行后都还是打不开A20的话,那也就没辙了,只好放弃。