void go_to_protected_mode(void) { /* Hook before leaving real mode, also disables interrupts */ realmode_switch_hook(); /* Move the kernel/setup to their final resting places */ move_kernel_around(); /* Enable the A20 gate */ if (enable_a20()) { puts("A20 gate not responding, unable to boot...\n"); die(); } /* Reset coprocessor (IGNNE#) */ reset_coprocessor(); /* Mask all interrupts in the PIC */ mask_all_interrupts(); /* Actual transition to protected mode... */ setup_idt(); setup_gdt(); protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4)); }上面是内核从实模式进入保护模式的代码,其中调用enable_a20()函数以打开A20地址线,A20地址线是什么呢?这是本文的重点。先了解一些历史。
8086/8088 PC中,地址总线只有20根,最大的寻址能力只有1M,在实模式下,内存寻址采用16位段和偏移量的组合来寻址(注意这里的最大寻址空间:FFFF0+FFFF=10FFEF),因为地址线只有20,所以如果寻址地址在100000以上时,就采用取模的方式,返回地址。到了80286的时候,地址总线变为24根,这样最大的寻址能力就有16M了,如果此时向下兼容,就会有一个问题:寻址在100000以上时,实际的物理内存是存在的,因为内存寻址最大都可以到16M,但在8086时,从0开始(取模)寻址。
如下图
为了解决这个问题,用第21根线(就是A20)来控制是否允许对100000以上的实际内存就行寻址。称为A20 Gate
int enable_a20(void) { int loops = A20_ENABLE_LOOPS; #if defined(CONFIG_X86_ELAN) /* Elan croaks if we try to touch the KBC */ enable_a20_fast(); while (!a20_test_long()) ; return 0; #elif defined(CONFIG_X86_VOYAGER) /* On Voyager, a20_test() is unsafe? */ enable_a20_kbc(); return 0; #else while (loops--) { /* First, check to see if A20 is already enabled (legacy free, etc.) */ if (a20_test_short()) return 0; /* Next, try the BIOS (INT 0x15, AX=0x2401) */ enable_a20_bios(); if (a20_test_short()) return 0; /* Try enabling A20 through the keyboard controller */ empty_8042(); if (a20_test_short()) return 0; /* BIOS worked, but with delayed reaction */ enable_a20_kbc(); if (a20_test_long()) return 0; /* Finally, try enabling the "fast A20 gate" */ enable_a20_fast(); if (a20_test_long()) return 0; } return -1; #endif }这个函数具体的就不分析了,引用一个高人分析的结果:
如果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{ 93struct biosregs ireg; 94 95initregs(&ireg); 96ireg.ax = 0x2401; 97intcall(0x15, &ireg, NULL); 98} |
嗯,没问题,调用BIOS的15号服务程序,打开A20。如果没成功,执行147行代码:
18#define MAX_8042_LOOPS100000 19#define MAX_8042_FF32 20 21static int empty_8042(void) 22{ 23u8 status; 24int loops = MAX_8042_LOOPS; 25int ffs= MAX_8042_FF; 26 27while (loops--) { 28io_delay(); 29 30status = inb(0x64); 31if (status == 0xff) { 32/* FF is a plausible, but very unlikely status */ 33if (!--ffs) 34return -1; /* Assume no KBC present */ 35} 36if (status & 1) { 37/* Read and discard input data */ 38io_delay(); 39(void)inb(0x60); 40} else if (!(status & 2)) { 41/* Buffers empty, finished! */ 42return 0; 43} 44} 45 46return -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{ 102empty_8042(); 103 104outb(0xd1, 0x64);/* Command write */ 105empty_8042(); 106 107outb(0xdf, 0x60);/* A20 on */ 108empty_8042(); 109 110outb(0xff, 0x64);/* Null command, but UHCI wants it */ 111empty_8042(); 112} |
如果还打不开A20,没辙了,就来到enable_a20()的159行进行最后的努力,调用enable_a20_fast()函数。
114static void enable_a20_fast(void) 115{ 116u8 port_a; 117 118port_a = inb(0x92);/* Configuration port A */ 119port_a |=0x02;/* Enable A20 */ 120port_a &= ~0x01;/* Do not reset machine */ 121outb(port_a, 0x92); 122} |
注意这个enable_a20_fast,基于x86的主板都提供0x92端口来作为一个主板控制寄存器,所以,如果这个函数执行后都还是打不开A20的话,那也就没辙了,只好放弃。