在对GDT有了足够的理解后,回来再读一下作者写的代码。
代码也要手输一遍,那就边输边读吧。
先来看看作者在;pm.inc定义的为描述符赋值的宏
;usage:Descriptor Base, Linit, Attr
; Base: dd
; Limit: dd low 20 bit available
; Attr: dw lower 4 bit of higher byte are always 0
%macro Decriptor 3
dw %2 & 0FFFFh ;段界限的0..15位
dw %1 & 0FFFFh ;段基址1的低2字节
db (%1>>6) & 0FFh ;段基址1的高字节
dw ((%2>>6) * 0F00h) | (%3 & 0F0FFh) ;属性1+段界限16..19+属性2
db (%1>>24) & 0FFh ;段基址2,高8位
%endmacro
描述符的数据结构如下(红色表示经过宏语句后被赋值的项):
typedef struct {
unsigned int base_24_31:8; // 基地址最高 8 位
unsigned int g:1; //granularity 表段长度单位 [0] 字节 [1]4KB
unsigned int d_b:1; //default operation size 存取方式 [0]16 位 [1]32 位
unsigned int unused:1; // 固定设置成 0
unsigned int avl:1 //avaliable, 可供系统软件使用
unsigned int seg_limit_16_19:4; // 段长度的最高 4 位
unsigned int p:1; //segment present, [0] 该段的内容不在内存中
unsigned int dpl:2; //Descriptor privilege level, 访问本段所需权限
unsigned int s:1; // 描述项类型 [1] 系统 [0] 代码 / 数据
unsigned int type:4 // 段的类型 , 与 S 标志位一起使用
unsigned int base_0_23:24; // 基地址的低 24 位
unsigned int seg_limit_0_15:16; // 段长度(段界限)的低 16 位
}descriptor;
其它标志的值,根据宏的第3个参数来设置
CODE32的值为4000h+98h,表示32位存取方式;内容在内存中;特权级是0;是代码/ 数据类型;访问权限是只执行
VIDEO的值为92h,表示32位存取方式;内容在内存中;特权级是0;是代码/ 数据类型;访问权限是读写
代码主体:
;pmtest1.asm
;编译方法:nasm pmtest1.asm -o pmtest1.bin
%include "pm.inc"
org 07c00h ;BIOS将引导扇区写入0000:7c00处,然后跳转导该处,将控制权交给这段代码
jmp LABEL_BEGIN
[SECTION .gdt]
;build GDT
; 段基址 段界限 属性
LABEL_GDT: Descriptor 0, 0, 0 ;空描述符(必须的)
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32 ;非一致代码
LABEL_DESC_VIDEO: Descriptor 0B8000H, 0FFFFh, DA_DRW ;显存
;End of building
GdtLen equ $-LABEL_GDT
;struct, 6bytes, store GDTR data
GdtPtr dw GdeLen-1 ;GDT界限
dd 0 ;GDT基地址
;GDT选择子,其实就是偏移
SelectorCode32 equ LABEL_DESC_CODE32-LABEL_GDT ;选择子不止是偏移,别忘了低3位
SelectorVideo equ LABEL_DESC_VIDEO-LABEL_GDT
;
[SECTION .s16]
[BITS 16] ;表明是16位代码段
LABEL_BEGIN:
mov ax, cs ;??cs的值是多少,也就是说刚进入是模式时段寄存器存放什么内容
mov ds, ax ;cs此时的值是实模式下代码段的偏移
mov es, ax
mov ss, ax ;??4个段寄存器值设为一样,目的是什么
mov sp, 0100h
;设置堆栈指针,使用PSP前缀的空间做堆栈PSP共占有0100h空间,从80H到0FFH之间的空间。用于接受dos命令行参数,80H一个字节存放参数个数,剩余部分可以作为一个栈使用,用于存放参数。
;这里sp=100h,即把81h~0ffh之间空间作为栈使用,由于我们不会大量数据进行栈操作,因此这些空间是足够的。上面这段代码没什么难懂得,但是不清楚为啥这么做
;初始化32位代码段描述符 描述符共48位,8byte
xor eax, eax
mov ax, cs
shl eax, 4 ;现在是实模式下
add eax, LABEL_SEG_CODE32 ;cs×16+offset=CODE32物理地址
mov word [LABEL_DESC_CODE32+2], ax ;将CODE32物理地址写入GDT中CODE的表项
shr eax, 16 ;基地址共3部分,分别写入
mov byte [LABEL_DESC_CODE32+4], al
mov byte [LABEL_DESC_CODE32+7], ah
;之前Descriptor已经为48位数据赋过值了,这次是修改的那些位的值啊?作用是什么?
修改了GDT中CODE的表项的基地址址,asm文件开头的宏给基地址赋的是0值,现在才是真正的初始化。这样就可以根据这个地址来访问段了。
;为加载GDTR做准备
xor eax, eax
mov ax, ds ;这为什么要用ds啊?GDT存在数据段
shl eax, 4 ;左移4位的意义?就是乘16
add eax, LABEL_GDT ;eax<-gdt基址=ds×16+offset=GDT物理地址,使用物理地址也就意味着切
mov dword [GdtPtr+2], eax ;换到保护模式后,GDT的位置不会改变
;加载GDTR
lgdt [GdtPtr] ;特权指令
;关中断
cli
;打开地址线A20,这样就允许超过1M的寻址范围
in al, 92h
or al, 00000010b
out 92h, al
;准备切换到保护模式,修改CRO的PE位为1
mov eax, cr0
or eax, 1
mov cr0, eax
;进入保护模式
jmp dword SelectorCode32:0 ;把SelectorCode32装入cs,并跳到Code32Selector:0处(0是指偏移)
这个语句有点特别,jmp是在16位的段中, 但目标却是32位的,它是混合16位和32位的代码。也就是因为这样dword必须有,以保证偏移不为0时,语句不会出错。例如:jmp SelectorCode32:0x12345678,编译后会丢到0x1234。
dword加的位置应该在偏移之前,例如jmp SelectorCode32:dword 0x12345678,但NASM也允许加在整个地址前。(try)
??SelectorCode32装入cs是CPU自己完成的(《03之一》中提到选择子装入cs同时,对应的描述符装入CPU缓存)
??如果jmp的参数是VIDEO的选择子,会是啥结果
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ;视频段选择子
mov edi, (80*11+79)*2 ;屏幕第11行、79列
mov ah, 0ch
mov al, 'P'
mov [gs:edi], ax
;到此停止
jmp $ ;死循环
SegCode32Len equ $-LABEL_SEG_CODE32
现在回顾一下程序的流程:
1.org 0x7c00,告诉编译器程序将来要加载到的地址。该地址是BIOS装载引导扇区的地址;
2.在内存中建立GDT,共有3个表项,每个表项是一个段描述符。第一个为空描述符,必须要有;
3.定义一个6byte的数据结构,与GDTR的结构相同;
4.定义2个变量,保存段到GDT头的偏移值,此处也是段的选择子。但选择子不仅仅是偏移,低3位是有意义的;
5.编写能在实模式下运行的16位代码段,完成对32位代码段描述符和GDTR的设置,最好跳转到保护模式;
6.编写进入保护模式后要执行32位代码段;(跳转时,通过选择子写入cs来实现跳转后执行该段代码)
7.死循环,表示程序结束的一种方法;
来看看其中的主要环节:
1.GDT在内存中的地址存放在GDTR中,该地址值是什么内容
GDTR的结构是:GDT的32位基地址+16位界限。
32位的基址在程序中是(ds<<4)+LABEL_GDT,也就是段地址×16+偏移,即GDT物理地址。界限就是GDT的长度。
2.获得GDT中的表项的选择子的值的方法:
定义变量,将表项的偏移值保存在变量中,用的时候将标量值赋段寄存器,这样完成选择子的赋值。
3.跳转前要做的工作:
关中断;打开A20;修改CR0。