http://blog.csdn.net/zgh1988/article/details/7098981

[置顶]全面剖析《自己动手写操作系统》第三章---保护模式       

        分类:            操作系统649人阅读评论(16)收藏举报

《自己动手写操作系统》读后感                http://blog.csdn.net/zgh1988/article/details/7059936

全面剖析《自己动手写操作系统》第一章   http://blog.csdn.net/zgh1988/article/details/7060032

全面剖析《自己动手写操作系统》第二章 http://blog.csdn.net/zgh1988/article/details/7062065

  1. 1 ; ========================================== 
  2. 2 ; pmtest1.asm 
  3. 3 ; 编译方法:nasm pmtest1.asm -o pmtest1.com 
  4. 4 ; ========================================== 
  5. 6 %include "pm.inc"    ; 常量, 宏, 以及一些说明 
  6. 8 org  0100h 
  7. 9  jmp LABEL_BEGIN 
  8. 10 
  9. 11 [SECTION .gdt] 
  10. 12 ; GDT 
  11. 13 ;                                         段基址,      段界限     , 属性 
  12. 14 LABEL_GDT:       Descriptor         0,                0, 0           ; 空描述符 
  13. 15 LABEL_DESC_CODE32:   Descriptor         0, SegCode32Len - 1, DA_C + DA_32    ; 非一致代码段, 32 
  14. 16 LABEL_DESC_VIDEO:    Descriptor   0B8000h,           0ffffh, DA_DRW      ; 显存首地址 
  15. 17 ; GDT 结束 
  16. 18 
  17. 19 GdtLen       equ $ - LABEL_GDT   ; GDT长度 
  18. 20 GdtPtr       dw  GdtLen - 1  ; GDT界限 
  19. 21      dd  0       ; GDT基地址 
  20. 22 
  21. 23 ; GDT 选择子 
  22. 24 SelectorCode32       equ LABEL_DESC_CODE32   - LABEL_GDT 
  23. 25 SelectorVideo        equ LABEL_DESC_VIDEO    - LABEL_GDT 
  24. 26 ; END of [SECTION .gdt] 
  25. 27 
  26. 28 [SECTION .s16] 
  27. 29 [BITS    16] 
  28. 30 LABEL_BEGIN: 
  29. 31  mov ax, cs 
  30. 32  mov ds, ax 
  31. 33  mov es, ax 
  32. 34  mov ss, ax 
  33. 35  mov sp, 0100h 
  34. 36  
  35. 37  ; 初始化 32 位代码段描述符 
  36. 38  xor eax, eax 
  37. 39  mov ax, cs 
  38. 40  shl eax, 4 
  39. 41  add eax, LABEL_SEG_CODE32 
  40. 42  mov word [LABEL_DESC_CODE32 + 2], ax 
  41. 43  shr eax, 16 
  42. 44  mov byte [LABEL_DESC_CODE32 + 4], al 
  43. 45  mov byte [LABEL_DESC_CODE32 + 7], ah 
  44. 46 
  45. 47  ; 为加载 GDTR 作准备 
  46. 48  xor eax, eax 
  47. 49  mov ax, ds 
  48. 50  shl eax, 4 
  49. 51  add eax, LABEL_GDT      ; eax <- gdt 基地址 
  50. 52  mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 
  51. 53 
  52. 54  ; 加载 GDTR 
  53. 55  lgdt    [GdtPtr] 
  54. 56 
  55. 57  ; 关中断 
  56. 58  cli 
  57. 59 
  58. 60  ; 打开地址线A20 
  59. 61  in  al, 92h 
  60. 62  or  al, 00000010b 
  61. 63  out 92h, al 
  62. 64 
  63. 65  ; 准备切换到保护模式 
  64. 66  mov eax, cr0 
  65. 67  or  eax, 1 
  66. 68  mov cr0, eax 
  67. 69 
  68. 70  ; 真正进入保护模式 
  69. 71  jmp dword SelectorCode32:0  ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0  处 
  70. 72 ; END of [SECTION .s16] 
  71. 73 
  72. 74 
  73. 75 [SECTION .s32]; 32 位代码段. 由实模式跳入. 
  74. 76 [BITS    32] 
  75. 77 
  76. 78 LABEL_SEG_CODE32: 
  77. 79  mov ax, SelectorVideo 
  78. 80  mov gs, ax          ; 视频段选择子(目的) 
  79. 81 
  80. 82  mov edi, (80 * 10 + 0) * 2  ; 屏幕第 10 行, 第 0 列。 
  81. 83  mov ah, 0Ch         ; 0000: 黑底    1100: 红字 
  82. 84  mov al, 'P' 
  83. 85  mov [gs:edi], ax 
  84. 86 
  85. 87  ; 到此停止 
  86. 88  jmp $ 
  87. 89 
  88. 90 SegCode32Len equ $ - LABEL_SEG_CODE32 
  89. 91 ; END of [SECTION .s32] 
 1 ; ==========================================
 2 ; pmtest1.asm
 3 ; 编译方法:nasm pmtest1.asm -o pmtest1.com
 4 ; ==========================================
 5
 6 %include	"pm.inc"	; 常量, 宏, 以及一些说明
 7
 8 org	0100h
 9	jmp	LABEL_BEGIN
10
11 [SECTION .gdt]
12 ; GDT
13 ;                                         段基址,      段界限     , 属性
14 LABEL_GDT:		Descriptor	       0,                0, 0     		; 空描述符
15 LABEL_DESC_CODE32:	Descriptor	       0, SegCode32Len - 1, DA_C + DA_32	; 非一致代码段, 32
16 LABEL_DESC_VIDEO:	Descriptor	 0B8000h,           0ffffh, DA_DRW		; 显存首地址
17 ; GDT 结束
18
19 GdtLen		equ	$ - LABEL_GDT	; GDT长度
20 GdtPtr		dw	GdtLen - 1	; GDT界限
21		dd	0		; GDT基地址
22
23 ; GDT 选择子
24 SelectorCode32		equ	LABEL_DESC_CODE32	- LABEL_GDT
25 SelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT
26 ; END of [SECTION .gdt]
27
28 [SECTION .s16]
29 [BITS	16]
30 LABEL_BEGIN:
31 	mov	ax, cs
32 	mov	ds, ax
33 	mov	es, ax
34 	mov	ss, ax
35 	mov	sp, 0100h
36 
37 	; 初始化 32 位代码段描述符
38 	xor	eax, eax
39 	mov	ax, cs
40 	shl	eax, 4
41	add	eax, LABEL_SEG_CODE32
42	mov	word [LABEL_DESC_CODE32 + 2], ax
43	shr	eax, 16
44	mov	byte [LABEL_DESC_CODE32 + 4], al
45	mov	byte [LABEL_DESC_CODE32 + 7], ah
46
47 	; 为加载 GDTR 作准备
48 	xor	eax, eax
49	mov	ax, ds
50	shl	eax, 4
51	add	eax, LABEL_GDT		; eax <- gdt 基地址
52	mov	dword [GdtPtr + 2], eax	; [GdtPtr + 2] <- gdt 基地址
53
54	; 加载 GDTR
55	lgdt	[GdtPtr]
56
57	; 关中断
58	cli
59
60	; 打开地址线A20
61	in	al, 92h
62	or	al, 00000010b
63	out	92h, al
64
65	; 准备切换到保护模式
66	mov	eax, cr0
67	or	eax, 1
68	mov	cr0, eax
69
70	; 真正进入保护模式
71	jmp	dword SelectorCode32:0	; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0  处
72 ; END of [SECTION .s16]
73
74
75 [SECTION .s32]; 32 位代码段. 由实模式跳入.
76 [BITS	32]
77
78 LABEL_SEG_CODE32:
79	mov	ax, SelectorVideo
80	mov	gs, ax			; 视频段选择子(目的)
81
82	mov	edi, (80 * 10 + 0) * 2	; 屏幕第 10 行, 第 0 列。
83	mov	ah, 0Ch			; 0000: 黑底    1100: 红字
84	mov	al, 'P'
85	mov	[gs:edi], ax
86
87	; 到此停止
88	jmp	$
89
90 SegCode32Len	equ	$ - LABEL_SEG_CODE32
91 ; END of [SECTION .s32]

在下面我要讲述3.1中所遇到的问题和迷惑。主要以pmtest1.asm为例,从以下几方面进行分析和讲解。
1)[SECTION .XXX]为何物?
2)全局描述符表(GDT)、描述符(Descriptor)、全局描述符表寄存器(GDTR)、选择子(SelectorXXX) 是什么?用来做什么?
3)实模式下的寻址方式与保护模式下的寻址方式的区别?
4)描述符宏定义和初始化段描述符
5)为加载gdtr做准备工作
6)其他
1、[SECTION .XXX]为何物?
	SECTION和SEGMENT的作用相类似,就是代表“段”的意思,从整个程序来看,该程序分为3个模块,分别是[SECTION .gdt]、[SECITON .s16]、[SECTION .s32]三部分。我们很容易就可以看出,其中的[SECTION .gdt]应该是数据段,其他的两个是代码段。通过[SECTION .XXX]将程序分成不同模块,完成不同的功能,使得程序看起来清晰明了。
2、描述符(Descriptor)、全局描述符表(GDT)、全局描述符表寄存器(GDTR)、选择子(SelectorXXX) 是什么?用来做什么?
 
 
	段,在80X86中,分段机制将内存空间分成一个或者多个线性区域,我们把这些线性区域称为段。我们需要将这些段区分开来,于是分段机制为每个段赋予3个属性,分别是:1、段基址(Base address):指定段在线性地址空间中的开始地址。2、段界限(Limit):表示了段内最大可用偏移量,也就是说它定义了段的长度。3、段属性(Attribute):指定了段的特性,包括:可读,可写或者可执行,特权等级等特性。
	段描述符(Descriptor),在程序中,我们需要定义一个数据结构来表示段,它同样包含三个元素,段基址(Base),段界限(Limit),段属性(Attribute),我们称它为段描述符(Descriptor)。段描述符和段就是同一个概念,每个段描述符要占用8个字节的空间。
	段描述符表(Descriptor Table),在一个程序中,不只存在一个段(段描述符)。所以我们需要将这些段描述符组织起来,于是定义了一个存储段描述符的数组,称为段描述符表。
	段选择子(SelectorXXX),把所有段描述符都存储在段描述符表中,当我们使用其中某一个段的时候,我们并不直接指向该段,而是通过该段描述符在段描述符表中的位置来访问的。故段选择子,就是一个16位的标识符,用来标识该段描述符在描述符表中的位置。
 
 
	段描述符表可以分为两类,一类是全局描述符表GDT(Global Descriptor Table),一类是局部描述符表LDT(Local Descriptor Table)。系统中供所有的任务共享的是全局描述符表,而不同的任务却是使用自己的局部描述符表。
	紧接着,如何让系统知道段描述符表在什么地方呢?处理器提供了内存管理寄存器,分别是全局描述符表寄存器(GDTR)、局部描述符表寄存器(LDTR)。GDTR寄存器中用于存放全局描述符表GDT的32位线性基地址和16位的表的长度值。LDTR寄存器中用于存放局部描述符表LDT的32位线性基地址和16位的表的长度值。通过系统指令,lgdt将GDT的线性基址和长度值加载到GDTR寄存器中,lldt将LDT的线性基址和长度值加载到LDTR寄存器中。
下面我们来分析程序中的代码:
  1. 11 [SECTION .gdt]
  2. 12 ; GDT
  3. 13 ; 段基址, 段界限 , 属性
  4. 14 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
  5. 15 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段, 32
  6. 16 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
  7. 17 ; GDT 结束
  8. 18
  9. 19 GdtLen equ $ - LABEL_GDT ; GDT长度
  10. 20 GdtPtr dw GdtLen - 1 ; GDT界限
  11. 21 dd 0 ; GDT基地址
  12. 22
  13. 23 ; GDT 选择子
  14. 24 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
  15. 25 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
  16. 26 ; END of [SECTION .gdt]
11 [SECTION .gdt]
12 ; GDT
13 ;                                         段基址,      段界限     , 属性
14 LABEL_GDT:		Descriptor	       0,                0, 0     		; 空描述符
15 LABEL_DESC_CODE32:	Descriptor	       0, SegCode32Len - 1, DA_C + DA_32	; 非一致代码段, 32
16 LABEL_DESC_VIDEO:	Descriptor	 0B8000h,           0ffffh, DA_DRW		; 显存首地址
17 ; GDT 结束
18
19 GdtLen		equ	$ - LABEL_GDT	; GDT长度
20 GdtPtr		dw	GdtLen - 1	; GDT界限
21		dd	0		; GDT基地址
22
23 ; GDT 选择子
24 SelectorCode32		equ	LABEL_DESC_CODE32	- LABEL_GDT
25 SelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT
26 ; END of [SECTION .gdt]
	在程序中,14、15、16行定义了3个段描述符,LABEL_GDT(空描述符),LABEL_DESC_CODE32(32位代码段描述符),LABEL_SESC_VIDEO(显示内存描述符)。每个描述符都包含了3个属性,段基址、段界限、段属性。
	将三个描述符组织到一起构成一个全局段描述符表(GDT)。12-17行完成了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。在下面初始化段描述符的时候需要用到这些。
段选择子的结构图如下图所示:
 
 
	在这里简单的介绍一下,它使用(15…3)来表示索引,故每一个描述符表最多只用213个描述符,除去第一个空描述符,则可以使用的描述符为8191个描述符。
	TI标志着该选择子所指向的段描述符是全局描述符,还是局部描述符。当TI=0时,表示全局描述符,当TI=1时,表示局部描述符。
	RPL请求优先级,稍后下一节将会提到。
全局描述符表寄存器(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、描述符宏定义和初始化段描述符
  1. 1 ; 描述符
  2. 2 ; usage: Descriptor Base, Limit, Attr
  3. 3 ; Base: dd
  4. 4 ; Limit: dd (low 20 bits available)
  5. 5 ; Attr: dw (lower 4 bits of higher byte are always 0)
  6. 6 %macro Descriptor 3
  7. 7 dw %2 & 0FFFFh ; 段界限 1 (2 字节)
  8. 8 dw %1 & 0FFFFh ; 段基址 1 (2 字节)
  9. 9 db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
  10. 10 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
  11. 11 db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
  12. 12 %endmacro ; 共 8 字节
 1 ; 描述符
 2 ; usage: Descriptor Base, Limit, Attr
 3 ;        Base:  dd
 4 ;        Limit: dd (low 20 bits available)
 5 ;        Attr:  dw (lower 4 bits of higher byte are always 0)
 6 %macro Descriptor 3
 7	dw	%2 & 0FFFFh				; 段界限 1				(2 字节)
 8	dw	%1 & 0FFFFh				; 段基址 1				(2 字节)
 9	db	(%1 >> 16) & 0FFh			; 段基址 2				(1 字节)
10	dw	((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)	; 属性 1 + 段界限 2 + 属性 2		(2 字节)
11	db	(%1 >> 24) & 0FFh			; 段基址 3				(1 字节)
12 %endmacro ; 共 8 字节

2、3、4、5行注释告诉我们,该宏定义需要三个变量,分别是段基址(4 bytes),段界限(4 bytes),段属性(dw)。

回顾刚才的段描述符结构,该宏定义,就是将变量Base,Limit,Attr分别安插到描述符中相应的位置。Base是1,Limit是2,Attr是3

7 是将Limit低16位赋值给描述符的BYTE0和BYTE1

8 是将Base低16位赋值给描述符的BYTE2和BYTE3

9 是将Base右移16位后的低8位(也就是原Base的第16—23位)赋值给描述符的BYTE4

10是将Limit右移8位之后的第8—11位和Attr的0—7和12—15位,组合起来存储到描述符的BYTE5和BYTE6

11是将Base右移24位后的低8位(也就是原Base的24—32位)赋值给描述符的BYTE7

初始化段描述符代码:

  1. 37 ; 初始化 32 位代码段描述符
  2. 38 xor eax, eax
  3. 39 mov ax, cs
  4. 40 shl eax, 4
  5. 41 add eax, LABEL_SEG_CODE32
  6. 42 mov word [LABEL_DESC_CODE32 + 2], ax
  7. 43 shr eax, 16
  8. 44 mov byte [LABEL_DESC_CODE32 + 4], al
  9. 45 mov byte [LABEL_DESC_CODE32 + 7], ah
37 	; 初始化 32 位代码段描述符
38 	xor	eax, eax
39 	mov	ax, cs
40 	shl	eax, 4
41	add	eax, LABEL_SEG_CODE32
42	mov	word [LABEL_DESC_CODE32 + 2], ax
43	shr	eax, 16
44	mov	byte [LABEL_DESC_CODE32 + 4], al
45	mov	byte [LABEL_DESC_CODE32 + 7], ah

为什么要初始化?你会发现这里只是修改了段描述符LABEL_DESC_CODE32的BYTE2,BYTE4,BYTE7。是不是突然恍然大悟?因为在我们初始化该LABEL_DESC_CODE32描述符时,将其基地址初始化为0,所以我们要修改描述符的基地址为其实际的地址。这也是在前面介绍段描述符的时候,我提醒大家需要注意的地方,即描述符的基地址所占有的字节是BYTE2,BYTE4,BYTE7。

5、为加载gdtr做准备工作

  1. 47 ; 为加载 GDTR 作准备
  2. 48 xor eax, eax
  3. 49 mov ax, ds
  4. 50 shl eax, 4
  5. 51 add eax, LABEL_GDT ; eax <- gdt 基地址
  6. 52 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
  7. 53
  8. 54 ; 加载 GDTR
  9. 55 lgdt [GdtPtr]
47 	; 为加载 GDTR 作准备
48 	xor	eax, eax
49	mov	ax, ds
50	shl	eax, 4
51	add	eax, LABEL_GDT		; eax <- gdt 基地址
52	mov	dword [GdtPtr + 2], eax	; [GdtPtr + 2] <- gdt 基地址
53
54	; 加载 GDTR
55	lgdt	[GdtPtr]

这个很好理解,我们就是对GdtPtr进行赋值,主要是初始化GDT的基地址。也就是将GDT的初始地址,赋值给GdtPtr的BYTE2,BYTE3,BYTE4,BYTE5。使GdtPtr的数据结构刚好符合GDTR,然后执行lgdt [GdtPtr],加载全局描述符表寄存器。将GDT的基地址和界限赋值给GDTR。

6、其他

至于接下来的 关中断、打开地址线A20、切换到保护模式、进入保护模式,跳转到32位代码段等一系列的问题,可以从书中找到合适的解释。

参考书籍:Linux内核完全剖析基于0.12内核

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章