Orange's 一个操作系统的实现(2)--- 从实模式跳转到保护模式

本篇主讲对应于 《Orange'S:一个操作系统的实现》第三章 a 代码 pmtest1.asm

; ==========================================
; pmtest1.asm
; 编译方法:nasm pmtest1.asm -o pmtest1.bin
; ==========================================

%include	"pm.inc"	; 常量, 宏, 以及一些说明

org	07c00h
	jmp	LABEL_BEGIN

[SECTION .gdt]
; 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	    ;显存首地址
; 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 * 11 + 79) * 2	; 屏幕第 11 行, 第 79 列。
	mov	ah, 0Ch			; 0000: 黑底    1100: 红字
	mov	al, 'P'
	mov	[gs:edi], ax

	; 到此停止
	jmp	$

SegCode32Len	equ	$ - LABEL_SEG_CODE32
; END of [SECTION .s32]

 

现在来逐行讲解

org	07c00h	
	jmp	LABEL_BEGIN

这句话告诉加载器,将这段程序加载到偏移段首地址07c00h处。 然后跳转到 LABEL_BEGIN 处开始执行

[SECTION .gdt]
; GDT
;                             		 段基址,       段界限     				, 属性
LABEL_GDT:	   	   Descriptor        0,                0, 					0            ; 空描述符
LABEL_DESC_CODE32: Descriptor        0, 			SegCode32Len - 1, 		DA_C + DA_32 ; 非一致代码段 段属性(DA_C: 98h 可执行; DA_32: 4000h 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  ;两个偏移量相减就是 LABEL_DESC_CODE32 相对于 LABEL_GDT 的偏移量
SelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT
; END of [SECTION .gdt]

这段代码对于很多初学者一头雾水,我们先看以下几个问题

  1. [SECTION .XXX]为何物?
  2. 段描述符(Descriptor)、全局描述符表(GDT)、全局描述符表寄存器(GDTR)、选择子(SelectorXXX) 为何物?有什么作用?

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寄存器中。

 继续分析上述代码:

在程序中,定义了3个段描述符

        LABEL_GDT(空描述符)。  Intel 要求  遵循Intel公司规定,全部置0

        LABEL_DESC_CODE32(32位代码段描述符)。32位代码段描述符,供保护模式下使用

        LABEL_SESC_VIDEO(显示内存描述符)。 显存段首地址,我们知道,显存首地址是0B8000H.

每个描述符都包含了3个属性,段基址、段界限、段属性。将三个描述符组织到一起构成一个全局段描述符表(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。

SelectorCode32		equ	LABEL_DESC_CODE32	- LABEL_GDT  
SelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT

问题: 两个段描述符相减是什么?

    两个段描述符相减就是 (LABEL_DESC_CODE32    - LABEL_GDT)  LABEL_DESC_CODE32 相对于 LABEL_GDT 的偏移量

     两个段描述符相减就是 (LABEL_DESC_VIDEO    - LABEL_GDT)   LABEL_DESC_VIDEO    相对于 LABEL_GDT 的偏移量

 

[BITS	16]		

[BITS 16]告诉编译器,这是一个16位代码段,所使用的寄存器都是16位寄存器

    mov	ax, cs
	mov	ds, ax
	mov	es, ax
	mov	ss, ax
	mov	sp, 0100h

    mov    sp, 0100h    ;设置堆栈的长度为100H byte

 

   ; 初始化 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

   xor    eax, eax    异或运算,清零操作,相当于 mov eax 0, 效率高于mov

   mov  ax, cs   

   shl  eax, 4    向左移动4位,就是×16。  到现在为止,eax就是代码段的物理首地址了   段值 × 16 + 偏移量    = 物理地址 

   add  eax, LABEL_SEG_CODE32   为eax (代码段首地址)加上 LABEL_SEG_CODE32 偏移量,得到的是 LABEL_SEG_CODE32 的真正物理地址

   mov  word [LABEL_DESC_CODE32 + 2], ax   [..] 代表一个内存单元,就是将 ax 移动到 LABEL_DESC_CODE32 的 2,3 字节处

   shr    eax, 16                    这句代码就将eax向右移动16位,低位被抛弃,高位变成了低位

   现在好办了,低16位又可以分为al,和 ah,那么现在我们就将al放到4位置,ah放到7位置吧

    mov    byte [LABEL_DESC_CODE32 + 4],  al   
    mov    byte [LABEL_DESC_CODE32 + 7], ah    

   因为历史原因 描述符的基地址所占有的字节是BYTE2,BYTE3,BYTE4,BYTE7(共32bit)

 

   ; 为加载 GDTR 作准备
	xor	eax, eax
	mov	ax, ds
	shl	eax, 4
	add	eax, LABEL_GDT		; eax <- gdt 基地址
	mov	dword [GdtPtr + 2], eax	; [GdtPtr + 2] <- gdt 基地址

    代码与上雷同,大体分析下:

     我们之前定义了Gdtptr的结构,包含两个字段,GTD界限(两个字节) 和 GDT基地址(四个字节)

     GdtPtr        dw    GdtLen - 1    ; GDT界限
                        dd    0        ; GDT基地址   

     mov    dword [GdtPtr + 2], eax    而这段代码是将eax中的gdt基地址,给Gdtptr结构赋值,从第二个字节开始。

   如何让系统知道段描述符表在什么地方呢?处理器提供了内存管理寄存器,分别是全局描述符表寄存器(GDTR)、局部描述符表寄存器(LDTR)

lgdt	[GdtPtr]

  通过系统指令,lgdt将GDT的线性基址和长度值加载到GDTR寄存器中

    ; 关中断
	cli

        CLI将IF置0,屏蔽掉“可屏蔽中断”,当可屏蔽中断到来时CPU不响应,继续执行原指令
         而STI 与之相反,STI将IF置1,允许“可屏蔽中断”,中断到来转而处理中断

    ; 打开地址线A20
	in	al, 92h			;表示从21H端口读取一字节数据到AL
	or	al, 00000010b	;逻辑或指令,按位进行或运算
	out	92h, al
   ; 准备切换到保护模式
	mov	eax, cr0
	or	eax, 1
	mov	cr0, eax

这个简单说一下,以后再详细
CR0也是一个寄存器,其中有个PE位,如果为0,就说明为实模式,
如果置1,说明为保护模式。现在我们要进入保护模式下工作,那么就要设置PE为1。

 

    ; 真正进入保护模式
	jmp	dword SelectorCode32:0	; 执行这一句会把 SelectorCode32 装入 cs,

现在已经再保护模式下了,当然要使用段选择子 + 偏移量来寻址啊,这样不就是寻址到了32位代码段中去了吗,偏移量为0不就说明从第一个代码开始执行。
不是吗 ?  呵呵,那dword了?
因为现在的代码段是16位,编译器只能将它编译位16位,但处于保护模式下,它的偏移量应该是32位,所以,要显示告诉编译器,我这里使用的是32位,把我这块给编译成32位的!!!
如果不加dword,
jmp    SelectorCode32:0
这句话不会出什么问题,16位的0是0,32位的0还是0,但如果这样呢?:
jmp    SelectorCode32:0x12345678
跳转到偏移0x12345678中,这时就错了
如果不将dword,编译器就将该地址截断成16位,取低位,变成了0x5678
所以我们必须这样做:
jmp    dword SelectorCodde32:0x12345678

 

; 到此停止
jmp	$

    $代表当前的地址
    那JMP $"就是跳转到当前的地址, 所以它是一个死循环,不继续执行下面的程序了。
    他的意思在于我要求的所有任务已经完成了,后面没有任务了,那么,就原地踏步吧!

最后一点小问题

    写完代码编译,自己用bximage工具重新做了个a.img,然后pmtest1.bin写入到a.img中,启动bochs后报错No bootable device

    查了半天代码,没找出来代码错误,然后就看了一下书,书中说是将第二章中的a.img拷贝过来,拷贝过来一式就好了。
    原因是:第二章中的代码最后有两句代码:

times 510 - ($ - $$)
dw 0xaa55


         所以第二章中的a.img中已经被填充为512字节并且以0xaa55结束,所以BISO就认为它是一个引导区,就去加载它。在第三章中没有这样做,所以得用第二章中的a.img,从头开始覆盖,而其代码量小于510字节,所以正好可以用。

转载于:https://my.oschina.net/yangshj/blog/3056214

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值