pmtest1.asm
; pmtest1.asm ; 编译方法:nasm pmtest1.asm -o pmtest1.com ; ========================================== %include "pm.inc" ; 常量, 宏, 以及一些说明 org 0100h jmp LABEL_BEGIN [SECTION .gdt] ; GDT ; 段基址, 段界限 , 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段, 32 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址 ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 选择子 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ; END of [SECTION .gdt] [SECTION .s16] [BITS 16] LABEL_BEGIN: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0100h ; 初始化 32 位代码段描述符 xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_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 ; 为加载 GDTR 作准备 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt 基地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加载 GDTR lgdt [GdtPtr] ; 关中断 cli ; 打开地址线A20 in al, 92h or al, 00000010b out 92h, al ; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处 ; END of [SECTION .s16] [SECTION .s32]; 32 位代码段. 由实模式跳入. [BITS 32] LABEL_SEG_CODE32: mov ax, SelectorVideo mov gs, ax ; 视频段选择子(目的) mov edi, (80 * 10 + 0) * 2 ; 屏幕第 10 行, 第 0 列。 mov ah, 0Ch ; 0000: 黑底 1100: 红字 mov al, 'P' mov [gs:edi], ax ; 到此停止 jmp $ SegCode32Len equ $ - LABEL_SEG_CODE32 ; END of [SECTION .s32]
下面我以pmtest1.asm为例,回答下面的问题。
(1)[SECTION .XXX]为何物?
(2)段描述符(Descriptor)、全局描述符表(GDT)、全局描述符表寄存器(GDTR)、选择子(SelectorXXX) 为何物?有什么作用?
(3)实模式下的寻址方式与保护模式下的寻址方式的区别?
(4)段描述符宏定义和初始化段描述符
(5)加载GDTR
1、[SECTION .XXX]为何物?
SECTION和SEGMENT的作用相类似,就是代表“段”的意思。从整个程序来看,该程序分为3个模块,分别是[SECTION .gdt]、[SECITON .s16]、[SECTION .s32]三部分。我们很容易就可以看出,其中的[SECTION .gdt]应该是数据段,其他的两个是代码段。通过[SECTION .XXX]将程序分成不同模块,完成不同的功能,使得程序看起来清晰明了。
2、描述符(Descriptor)、全局描述符表(GDT)、全局描述符表寄存器(GDTR)、选择子(SelectorXXX) 是什么?用来做什么?
段(Segment),在80X86中,分段机制将内存空间分成一个或者多个线性区域,我们把这些线性区域称为段。我们需要将这些段区分开来,于是分段机制为每个段赋予3个属性,分别是:段基址(Base address):指定段在线性地址空间中的开始地址。段界限(Limit):表示了段内最大可用偏移量,也就是说它定义了段的长度。段属性(Attribute):指定了段的特性,包括:可读,可写或者可执行,特权等级等特性。
段描述符(Descriptor),在程序中,我们需要定义一个数据结构来记录段的属性,有段基址(Base),段界限(Limit),段属性(Attribute),我们称它为段描述符(Descriptor)。段是逻辑概念,而段描述符是表示段的数据结构,每个段描述符要占用8个字节的空间。
段描述符表(Descriptor Table),在一个程序中,不只存在一个段(段描述符)。所以我们需要将这些段描述符组织起来,于是定义了一个存储段描述符的数组,称为段描述符表。段描述符表有两种,一种是全局描述符表(GDT),一种是局部描述符表(LDT),系统中供所有的任务共享的是全局描述符表,而不同的任务却是使用自己的局部描述符表。
段选择子(SelectorXXX),把所有段描述符都存储在段描述符表中,当我们使用其中某一个段的时候,我们并不直接指向该段,而是通过该段描述符在段描述符表中的位置来访问的。故段选择子,就是一个16位的标识符,用来标识该段描述符在描述符表中的位置。
段描述符表寄存器,如何让系统知道段描述符表在什么地方呢?处理器提供了内存管理寄存器,分别是全局描述符表寄存器(GDTR)、局部描述符表寄存器(LDTR)。GDTR寄存器中用于存放全局描述符表GDT的32位线性基地址和16位的表的长度值。LDTR寄存器中用于存放局部描述符表LDT的32位线性基地址和16位的表的长度值。通过系统指令,lgdt将GDT的线性基址和长度值加载到GDTR寄存器中,lldt将LDT的线性基址和长度值加载到LDTR寄存器中。
下面我们来分析pmtest1.asm中的源代码:
[SECTION .gdt] ; GDT ; 段基址, 段界限 , 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段, 32 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址 ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 选择子 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ; END of [SECTION .gdt]
GdtLen是GDT的长度。GdtPtr为一个数据结构,包含两个元素,第一个元素是2 bytes的GDT界限。第二个元素是4 bytes的GDT的基地址。该数据结构与全局描述符表寄存器(GDTR)的数据结构相同,所以在加载GDTR的时候(源代码55行),就是将该GdtPtr加载到GDTR中。
由于第一个段LABEL_GDT是空描述符,它仅仅代表该GDT的初始地址,所以该描述符为空描述符,一般情况下,不为它创建选择子。然后该程序建立了两个选择子(24、25行)SelectorCode32和SelectorVideo,分别对应着这两个段LABEL_SESC_CODE32和LABEL_DESC_VIDEO。
这段代码的结构大家应该明白了吧。
下面我要分别介绍段描述符,全局描述符表寄存器,选择子的数据结构。
段描述符(Descriptor)的结构图如下:
段描述符占有8个字节,在这里我只想提醒大家一下,段基址分别占有BYTE2、BYTE3、BYTE4和BYTE7。
段选择子(SelectorXXX)的结构图如下图所示:
在这里简单的介绍一下,它使用13位来表示描述符索引,故每一个描述符表最多只用8192个描述符,除去第一个空描述符,则可以使用的描述符为8191个描述符。
全局描述符表寄存器(GDT)的结构图如下所示:
在这里,你可以和上面程序中的GdtPtr数据结构做比较,是不是格式相同。2个字节表示GDT界限,4个字节表示GDT基地址。
3、实模式下的寻址方式与保护模式下的寻址方式有什么不同?
在实模式下,也就是在8086系统下的寻址方式。 Intel 8086是16位的CPU,它有着16位的寄存器(Register),16位的数据总线(Data Bus)以及20位的地址总线(Address Bus)和1MB的寻址能力。一个地址是由段和偏移两部分组成的,物理地址遵循这样的计算公式:
物理地址(Physical Address) = 段值(Segment) * 16 + 偏移(Offset)。其中段值和偏移都是16位的。故寻址范围为1MB。
在保护模式下,有了分段机制,所以它的寻址方式发生了很大的变化。具体如下图所示:
在保护模式下,首先使用段选择子在段描述符表中查找到相对应的段描述符,找到32位段基址,然后在与32位的偏移量相加,得到线性地址。段基址和段偏移量都是32位的,所以寻址范围大小为4GB。在程序中jmp dword SlectorCode32:0的作用,就是进入保护模式下的寻址方式。其中,在使用某个段时,它的段选择子是存储在段寄存器中的。
这里面存在着一个问题,是否我们每次寻址都要先去全局描述符表寄存器(GDTR)中,查找到全局描述符表(GDT)的基址,然后再次根据选择子的索引跳转到该描述符所在的位置,然后取得段描述符中的基址,如果这样的话,我们里里外外采访了几次内存,太浪费时间了。实际上段寄存器结构是这样的:
这样的好处就是,我们可以直接获取段描述符。
4、描述符宏定义和初始化段描述符
描述符宏定义:
; 描述符 ; usage: Descriptor Base, Limit, Attr ; Base: dd ; Limit: dd (low 20 bits available) ; 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 字节
初始化段描述符代码:
; 初始化 32 位代码段描述符 xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_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为什么要初始化?你会发现这里只是修改了段描述符基地址,即LABEL_DESC_CODE32的BYTE2,BYTE4,BYTE7。是不是突然恍然大悟?因为在我们初始化该LABEL_DESC_CODE32描述符时,将其基地址初始化为0,所以我们要修改描述符的基地址为其实际的地址。这也是在前面介绍段描述符的时候,我提醒大家需要注意的地方,即描述符的基地址所占有的字节是BYTE2,BYTE4,BYTE7。
5、加载GDTR
; 为加载 GDTR 作准备 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt 基地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加载 GDTR lgdt [GdtPtr]