现在的位置: 首页 > 综合 > 正文

一个操作系统的实现03之二 切换到保护模式

2013年12月03日 ⁄ 综合 ⁄ 共 4367字 ⁄ 字号 评论关闭

在对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。

【上篇】
【下篇】

抱歉!评论已关闭.