1.1 进入保护模式的主要步骤:
1.1.1 GDT表
GDT表是存储GDT描述符的数组,每一个描述符对应着一个段。GDT表中的第一项必须为全0。表项类型为结构体Descriptor,结构如下:
; usage: Descriptor Base, Limit, Attr
; %1 Base: dd
; %2 Limit: dd (low 20 bits available)
; %3 Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro ; 共 8 字节 |
根据nasm编译器的规则,结构体定义以%macro开始,以%endmacro结束。结构体名为Descriptor,宏名后的3表示该结构体有三个成员变量。
GDT表的定义方式如下:
[SECTION .gdt]
; GDT Base Addr Limit Attrib
LABEL_DES_GDT: Descriptor 0, 0, 0
LABEL_DES_CODE32: Descriptor 0, Code32Len - 1, DA_C + DA_32
LABEL_DES_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW
; GDT end |
这里可以看到给结构体赋值的方式,每一项对应结构体中的一个成员变量。以表项LABEL_DES_VIDEO为例,0B8000h对应%1,0ffffh对应%2,DA_DRW对应%3。
注意:表项LABEL_DES_CODE32保存的是我们要跳入的32位代码段的GDT表项,其中Base Addr是0,因为此时还不知道32为代码段的地址,在后面的16为代码段中才会设置它。
1.1.2 Gdtr
GdtLen equ $ - LABEL_DES_GDT ; Gdt Len
GdtPtr dw GdtLen - 1 ; Gdt Limit
dd 0 ; Gdt Base Addr |
GDT表的地址保存在寄存器Gdtr中,该地址首先通过上面的语句计算而得后存在变量GdtPtr中,之后才加载。GdtPtr低2字节保存GDT表长度,通过“$ - LABEL_DES_GDT”计算而得。GdtPtr高4字节保存GDT表基址,这里先置0,后面会存入地址。
1.1.3 Selector选择子
; Gdt Selector
SelectorCode32 equ LABEL_DES_CODE32 - LABEL_DES_GDT
SelectorVideo equ LABEL_DES_VIDEO - LABEL_DES_GDT
|
选择子Selector中保存的是各段对应GDT表项相对地址,通过上面的语句计算而得。
1.1.4 16位段与32位段
[SECTION .s16]
[BITS 16]
……
[SECTION .s32]
[BITS 32]
…… |
由于本程序实现了有实模式到保护模式的跳转,所以需要在程序中定义两个代码段,一个是16位段;一个是32位段。
1.1.5 初始化32位代码段描述符
; Init 32 bits code segment descriptor
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_CODE32
mov word [LABEL_DES_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DES_CODE32 + 4], al
mov byte [LABEL_DES_CODE32 + 7], ah
|
首先要设置32为代码段对应GDT表项的基址。基址对应着表项中的第2、3、4、7,四个字节。
1.1.6 准备加载gdtr
; Prepare for loading gdtr
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_DES_GDT
mov dword [GdtPtr + 2], eax |
这段代码用于设置变量GdtPtr,长度为2字节。
1.1.7 加载gdtr
; Load gdtr
lgdt [GdtPtr] |
此处加载gdtr寄存器,x86使用专门的指令lgdt实现gdtr寄存器的设置。
1.1.8 关闭中断
由于实模式与保护模式下的中断模式不同,所以在实模式/保护模式转换时必须关闭中断。
1.1.9 打开A20
; open A20
in al, 92h
or al, 00000010b
out 92h, al |
80286及之前的cpu最大寻址空间为1M,超过1M的地址(也就是使用了A20之后的地址线),会开始从0重新计算,即使跳入保护空间,也无法使用超过1M的地址。
80386开始,增加了保护模式,寻址空间变为4G。为了与80286之前的cpu兼容,超过1M的地址平时是关闭的,只有打开A20后才能访问超过1M的地址。
上面只是打开A20的一种方式,但不是唯一的方式。
1.1.10 设置cr0的PE位
; open protected mode
mov eax, cr0
or eax, 1
mov cr0, eax |
此处打开cr0的PE位,实际上就使能了保护模式,也就是说,“mov cr0, eax”这一句后,系统就运行于保护模式下了。但是,此时ics的值仍然是实模式下的值,我们需要把代码段的Selector选择子装入cs。
1.1.11 跳入保护模式
; jump into protected mode
jmp dword SelectorCode32:0
|
这个跳转是为了设置cs寄存器,其目的是选择子SelectorCode32对应的GDT描述符LABEL_DESC_CODE32对应的段首地址,即标号LABEL_ CODE32C处。
该句指令还在16位段中执行,而目标地址却是32位的,从这一点上看,他是混合16位于32位的代码。所以,jmp后的dword就必不可少了。如果没有dword,编译出来的只是16位代码。假设目标地址偏移量为0x12345678,执行jmp SelectorCode32:0x12345678,则等价于jmp SelectorCode32:0x5678。
Nasm编译器的好处之一就是可以通过jmp后添加dword,指定编译后为32位指令。
至此,我们真正跳入了保护模式。
1.1.12 显示字符
LABEL_CODE32:
mov eax, SelectorVideo
mov gs, eax
mov edi, (80 * 6 + 6) * 2 ; Line: 6, Column: 6
mov ah, 0Ch ; 0000: background(black) 1100: foreground(red)
mov al, 'P'
mov [gs:edi], ax |
这是进入保护模式后执行的代码。SelectorVideo选择子对应着显存的基址,我们通过把‘P’放入显存,实现了字符‘P’的显示。
本文乃fireaxe原创,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,并注明原作者及原链接,严禁用于任何商业用途。
作者:fireaxe_hq@hotmail.com
博客:fireaxe.blog.chinaunix.net