我们知道,现在大部分的CPU都是32位的,我们编写的OS当然也想在运行在32位的模式下,因为32位的CPU功能自然比16位的强大许多,32位的CPU在硬件给予了OS很大的支持。但是当机器刚刚启动时,CPU是运行在16位模式下的,所以我们必须学习怎么样进入32位模式,也即保护模式。
16位模式也就是实模式,是我们最开始学习汇编时CPU运行的模式,是用段寄存器左移4位+偏移地址来寻址的;而在保护模式下则大不一样,是先分段再分页寻址的,分页是可选的,分段是必须的。所以想进入保护模式的第一步就是建立一张段表,也就是GDT,这个表中的每一个项就是一个段描述符Descriptor,有8个字节,记录了一个段的起始地址,段界限,以及段属性,从而对每一个段进行保护。
- [section .gdt]
- LABEL_GDT:
- LABEL_DESC_DUMMY:
- times 8 db 0
- LABEL_DESC_CODE32:
- dw 0ffffh
- times 3 db 0
- db 9ch
- db 4fh
- db 0
- LABEL_DESC_VIDEO:
- dw 0ffffh
- dw 8000h
- db 0bh
- db 92h
- db 4fh
- db 0
如上面的代码所示,GDT中有3个描述符,第1个是哑描述符,没什么意义;第2个表示1个32位的可执行非一致代码段,段界限为0xfffff,段基址没有填写,日后会填充;第3个是1个可读写的数据段,段界限位0xfffff,段基址为0xb8000,此段指示了显存。描述符的结构在这里就不细说了,查查书就好了。
有了GDT,我们寻址的时候只要把想访问的段的相对于GDT的索引送到段寄存器就OK了,那么怎么知道GDT在内存中的位置呢?在CPU有一个48位的寄存器,就存放着GDT在内存的地址和GDT的长度。所以在进入保护模式必须给这个寄存器填上正确的地址和长度。如下代码所示:
- GDT_Len equ $ - LABEL_GDT
- GDT_Ptr:
- dw GDT_Len - 1
- dd 0
我们看到,GDT的长度已填上,GDT实际的物理地址还不知道,因为我们的测试程序是在DOS下动态加载的,所以在日后再填写,这个以后填充段描述符的段基地址是一样的道理。
还有一个概念是选择子selector,是一个16位的结构,它的高13位是相应段在GDT中的索引,为什么只用高13位表示呢?简单啊,因为一个描述符是8个字节,所以索引肯定是8个倍数,低3位就用不着,可以拿来做别的事,至于是什么事呢,日后再说。下面就是定义选择子的代码:
- Selector_Code32 equ LABEL_DESC_CODE32 - LABEL_DESC_DUMMY
- Selector_Video equ LABEL_DESC_VIDEO - LABEL_DESC_DUMMY
准备好了这些,还不够啊,还有一些东东没有填充好呢。先不理它,先明确我们进入保护模式后要干嘛,那就在屏幕上打印个字符吧,这样可以测试下我们的GDT有没有设置正确,下面就是这个32位的代码段:
- [section .s32]
- [bits 32]
- LABEL_CODE32:
- mov ax,Selector_Video
- mov gs,ax
- mov ah,0ch
- mov al,'x'
- mov [gs:80 * 10],ax
- jmp $
为了简单起见,打印完后进入死循环。
最后只剩16位代码了,这段代码当然是做与进入保护模式有关的事情了,下面是代码:
- [section .s16]
- [bits 16]
- LABEL_BEGIN:
- mov ax,cs
- mov ds,ax
- mov es,ax
- xor eax,eax
- mov ax,cs
- shl eax,4
- add eax,LABEL_CODE32
- mov word [LABEL_DESC_CODE32 + 2],ax
- shr eax,16
- mov byte [LABEL_DESC_CODE32 + 4],al
- mov byte [LABEL_DESC_CODE32 + 7],ah
- xor eax,eax
- mov ax,ds
- shl eax,4
- add eax,LABEL_GDT
- mov dword [GDT_Ptr + 2],eax
- lgdt [GDT_Ptr]
- cli
- in al,92h
- or al,00000010b
- out 92h,al
- mov eax,cr0
- or eax,1
- mov cr0,eax
- jmp dword Selector_Code32:0
8到15行是填充32位代码段的段基址,17到21行是填充GDT的地址,接着加载到GDTR寄存器中,然后是关掉中断,因为在保护模式下中断的机制跟实模式不太一样,先关掉日后再说。接着打开A20地址线使得能够寻址到更大的范围,接着打开cr0寄存器的最低位,值得让CPU进入保护模式,最后是历史性的跳转正式跳转到保护模式。
下面是完整的代码:
- org 0100h
- jmp LABEL_BEGIN
- [section .gdt]
- LABEL_DESC_DUMMY:
- times 8 db 0
- LABEL_DESC_CODE32:
- dw 0ffffh
- times 3 db 0
- db 9ch
- db 4fh
- db 0
- LABEL_DESC_VIDEO:
- dw 0ffffh
- dw 8000h
- db 0bh
- db 92h
- db 4fh
- db 0
- GDT_Len equ $ - LABEL_DESC_DUMMY
- GDT_Ptr:
- dw GDT_Len - 1
- dd 0
- Selector_Code32 equ LABEL_DESC_CODE32 - LABEL_DESC_DUMMY
- Selector_Video equ LABEL_DESC_VIDEO - LABEL_DESC_DUMMY
- [section .s16]
- [bits 16]
- LABEL_BEGIN:
- mov ax,cs
- mov ds,ax
- mov es,ax
- xor eax,eax
- mov ax,cs
- shl eax,4
- add eax,LABEL_CODE32
- mov word [LABEL_DESC_CODE32 + 2],ax
- shr eax,16
- mov byte [LABEL_DESC_CODE32 + 4],al
- mov byte [LABEL_DESC_CODE32 + 7],ah
- xor eax,eax
- mov ax,ds
- shl eax,4
- add eax,LABEL_DESC_DUMMY
- mov dword [GDT_Ptr + 2],eax
- lgdt [GDT_Ptr]
- cli
- in al,92h
- or al,00000010b
- out 92h,al
- mov eax,cr0
- or al,1
- mov cr0,eax
- jmp Selector_Code32:0
- [section .s32]
- [bits 32]
- LABEL_CODE32:
- mov ax,Selector_Video
- mov gs,ax
- mov ah,0ch
- mov al,'x'
- mov [gs:80 * 10],ax
- jmp $
运行结果如下: