Orange'S:一个操作系统的实现

保护模式进阶

Posted by BINBIN Blog on October 28, 2019

保护模式进阶

之前讲述了进入保护模式的步骤!

复习一下:

  1. 定义GDT
  2. 用lgdt 加载 gdtr
  3. 打开A20 地址线
  4. 设置CR0 寄存器PE位为1
  5. 执行跳转,进入保护模式

在保护模式下,我们能访问4G 的内存!

所以试验一下读写大地址内存! 思路: 新建一个段,以5mb为基址,这个远超出1MB 的界限了。 先读出开始处8字节的内容,然后写入一个字符串,再从中读出8个字节。 这个读出来的字节就跟写入的字符串一样! 字符串保存在数据段中,数据段属于新加的,因为上一章只定义了三个描述符,一个空描述符,一个代码段描述符,一个显存描述符。

书中也提供了代码 chapter3/b/pmtest2.asm

代码清单:


1  	; ==========================================
2  	; pmtest2.asm
3  	; 编译方法:nasm pmtest2.asm -o pmtest2.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_NORMAL: Descriptor    0,         0ffffh, DA_DRW    ; Normal 描述符
16 	LABEL_DESC_CODE32: Descriptor    0, SegCode32Len-1, DA_C+DA_32; 非一致代码段, 32
17 	LABEL_DESC_CODE16: Descriptor    0,         0ffffh, DA_C      ; 非一致代码段, 16
18 	LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
19 	LABEL_DESC_STACK:  Descriptor    0,     TopOfStack, DA_DRWA+DA_32; Stack, 32 位
20 	LABEL_DESC_TEST:   Descriptor 0500000h,     0ffffh, DA_DRW
21 	LABEL_DESC_VIDEO:  Descriptor  0B8000h,     0ffffh, DA_DRW    ; 显存首地址
22 	; GDT 结束
23 
24 	GdtLen		equ	$ - LABEL_GDT	; GDT长度
25 	GdtPtr		dw	GdtLen - 1	; GDT界限
26 			dd	0		; GDT基地址
27 
28 	; GDT 选择子
29 	SelectorNormal		equ	LABEL_DESC_NORMAL	- LABEL_GDT
30 	SelectorCode32		equ	LABEL_DESC_CODE32	- LABEL_GDT
31 	SelectorCode16		equ	LABEL_DESC_CODE16	- LABEL_GDT
32 	SelectorData		equ	LABEL_DESC_DATA		- LABEL_GDT
33 	SelectorStack		equ	LABEL_DESC_STACK	- LABEL_GDT
34 	SelectorTest		equ	LABEL_DESC_TEST		- LABEL_GDT
35 	SelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT
36 	; END of [SECTION .gdt]
37 
38 	[SECTION .data1]	 ; 数据段
39 	ALIGN	32
40 	[BITS	32]
41 	LABEL_DATA:
42 	SPValueInRealMode	dw	0
43 	; 字符串
44 	PMMessage:		db	"In Protect Mode now. ^-^", 0	; 在保护模式中显示
45 	OffsetPMMessage		equ	PMMessage - $$
46 	StrTest:		db	"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
47 	OffsetStrTest		equ	StrTest - $$
48 	DataLen			equ	$ - LABEL_DATA
49 	; END of [SECTION .data1]
50 
51 
52 	; 全局堆栈段
53 	[SECTION .gs]
54 	ALIGN	32
55 	[BITS	32]
56 	LABEL_STACK:
57 		times 512 db 0
58 
59 	TopOfStack	equ	$ - LABEL_STACK - 1
60 
61 	; END of [SECTION .gs]
62 
63 
64 	[SECTION .s16]
65 	[BITS	16]
66 	LABEL_BEGIN:
67 		mov	ax, cs
68 		mov	ds, ax
69 		mov	es, ax
70 		mov	ss, ax
71 		mov	sp, 0100h
72 
73 		mov	[LABEL_GO_BACK_TO_REAL+3], ax
74 		mov	[SPValueInRealMode], sp
75 
76 		; 初始化 16 位代码段描述符
77 		mov	ax, cs
78 		movzx	eax, ax
79 		shl	eax, 4
80 		add	eax, LABEL_SEG_CODE16
81 		mov	word [LABEL_DESC_CODE16 + 2], ax
82 		shr	eax, 16
83 		mov	byte [LABEL_DESC_CODE16 + 4], al
84 		mov	byte [LABEL_DESC_CODE16 + 7], ah
85 
86 		; 初始化 32 位代码段描述符
87 		xor	eax, eax
88 		mov	ax, cs
89 		shl	eax, 4
90 		add	eax, LABEL_SEG_CODE32
91 		mov	word [LABEL_DESC_CODE32 + 2], ax
92 		shr	eax, 16
93 		mov	byte [LABEL_DESC_CODE32 + 4], al
94 		mov	byte [LABEL_DESC_CODE32 + 7], ah
95 
96 		; 初始化数据段描述符
97 		xor	eax, eax
98 		mov	ax, ds
99 		shl	eax, 4
100		add	eax, LABEL_DATA
101		mov	word [LABEL_DESC_DATA + 2], ax
102		shr	eax, 16
103		mov	byte [LABEL_DESC_DATA + 4], al
104		mov	byte [LABEL_DESC_DATA + 7], ah
105
106		; 初始化堆栈段描述符
107		xor	eax, eax
108		mov	ax, ds
109		shl	eax, 4
110		add	eax, LABEL_STACK
111		mov	word [LABEL_DESC_STACK + 2], ax
112		shr	eax, 16
113		mov	byte [LABEL_DESC_STACK + 4], al
114		mov	byte [LABEL_DESC_STACK + 7], ah
115
116		; 为加载 GDTR 作准备
117		xor	eax, eax
118		mov	ax, ds
119		shl	eax, 4
120		add	eax, LABEL_GDT		; eax <- gdt 基地址
121		mov	dword [GdtPtr + 2], eax	; [GdtPtr + 2] <- gdt 基地址
122
123		; 加载 GDTR
124		lgdt	[GdtPtr]
125
126		; 关中断
127		cli
128
129		; 打开地址线A20
130		in	al, 92h
131		or	al, 00000010b
132		out	92h, al
133
134		; 准备切换到保护模式
135		mov	eax, cr0
136		or	eax, 1
137		mov	cr0, eax
138
139		; 真正进入保护模式
140		jmp	dword SelectorCode32:0	; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0  处
141
142	;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
143
144	LABEL_REAL_ENTRY:		; 从保护模式跳回到实模式就到了这里
145		mov	ax, cs
146		mov	ds, ax
147		mov	es, ax
148		mov	ss, ax
149
150		mov	sp, [SPValueInRealMode]
151
152		in	al, 92h		; `.
153		and	al, 11111101b	;  | 关闭 A20 地址线
154		out	92h, al		; /
155
156		sti			; 开中断
157
158		mov	ax, 4c00h	; `.
159		int	21h		; /  回到 DOS
160	; END of [SECTION .s16]
161
162
163	[SECTION .s32]; 32 位代码段. 由实模式跳入.
164	[BITS	32]
165
166	LABEL_SEG_CODE32:
167		mov	ax, SelectorData
168		mov	ds, ax			; 数据段选择子
169		mov	ax, SelectorTest
170		mov	es, ax			; 测试段选择子
171		mov	ax, SelectorVideo
172		mov	gs, ax			; 视频段选择子
173
174		mov	ax, SelectorStack
175		mov	ss, ax			; 堆栈段选择子
176
177		mov	esp, TopOfStack
178
179
180		; 下面显示一个字符串
181		mov	ah, 0Ch			; 0000: 黑底    1100: 红字
182		xor	esi, esi
183		xor	edi, edi
184		mov	esi, OffsetPMMessage	; 源数据偏移
185		mov	edi, (80 * 10 + 0) * 2	; 目的数据偏移。屏幕第 10 行, 第 0 列。
186		cld
187	.1:
188		lodsb
189		test	al, al
190		jz	.2
191		mov	[gs:edi], ax
192		add	edi, 2
193		jmp	.1
194	.2:	; 显示完毕
195
196		call	DispReturn
197
198		call	TestRead
199		call	TestWrite
200		call	TestRead
201
202		; 到此停止
203		jmp	SelectorCode16:0
204
205	; ------------------------------------------------------------------------
206	TestRead:
207		xor	esi, esi
208		mov	ecx, 8
209	.loop:
210		mov	al, [es:esi]
211		call	DispAL
212		inc	esi
213		loop	.loop
214
215		call	DispReturn
216
217		ret
218	; TestRead 结束-----------------------------------------------------------
219
220
221	; ------------------------------------------------------------------------
222	TestWrite:
223		push	esi
224		push	edi
225		xor	esi, esi
226		xor	edi, edi
227		mov	esi, OffsetStrTest	; 源数据偏移
228		cld
229	.1:
230		lodsb
231		test	al, al
232		jz	.2
233		mov	[es:edi], al
234		inc	edi
235		jmp	.1
236	.2:
237
238		pop	edi
239		pop	esi
240
241		ret
242	; TestWrite 结束----------------------------------------------------------
243
244
245	; ------------------------------------------------------------------------
246	; 显示 AL 中的数字
247	; 默认地:
248	;	数字已经存在 AL 中
249	;	edi 始终指向要显示的下一个字符的位置
250	; 被改变的寄存器:
251	;	ax, edi
252	; ------------------------------------------------------------------------
253	DispAL:
254		push	ecx
255		push	edx
256
257		mov	ah, 0Ch			; 0000: 黑底    1100: 红字
258		mov	dl, al
259		shr	al, 4
260		mov	ecx, 2
261	.begin:
262		and	al, 01111b
263		cmp	al, 9
264		ja	.1
265		add	al, '0'
266		jmp	.2
267	.1:
268		sub	al, 0Ah
269		add	al, 'A'
270	.2:
271		mov	[gs:edi], ax
272		add	edi, 2
273
274		mov	al, dl
275		loop	.begin
276		add	edi, 2
277
278		pop	edx
279		pop	ecx
280
281		ret
282	; DispAL 结束-------------------------------------------------------------
283
284
285	; ------------------------------------------------------------------------
286	DispReturn:
287		push	eax
288		push	ebx
289		mov	eax, edi
290		mov	bl, 160
291		div	bl
292		and	eax, 0FFh
293		inc	eax
294		mov	bl, 160
295		mul	bl
296		mov	edi, eax
297		pop	ebx
298		pop	eax
299
300		ret
301	; DispReturn 结束---------------------------------------------------------
302
303	SegCode32Len	equ	$ - LABEL_SEG_CODE32
304	; END of [SECTION .s32]
305
306
307	; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
308	[SECTION .s16code]
309	ALIGN	32
310	[BITS	16]
311	LABEL_SEG_CODE16:
312		; 跳回实模式:
313		mov	ax, SelectorNormal
314		mov	ds, ax
315		mov	es, ax
316		mov	fs, ax
317		mov	gs, ax
318		mov	ss, ax
319
320		mov	eax, cr0
321		and	al, 11111110b
322		mov	cr0, eax
323
324	LABEL_GO_BACK_TO_REAL:
325		jmp	0:LABEL_REAL_ENTRY	; 段地址会在程序开始处被设置成正确的值
326
327	Code16Len	equ	$ - LABEL_SEG_CODE16
328
329	; END of [SECTION .s16code]
330

对比之前的代码,如图所示:

新增的描述符:

  • 第15行 LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
  • 第17-20行:

LABEL_DESC_CODE16: Descriptor    0,         0ffffh, DA_C      ; 非一致代码段, 16
LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
LABEL_DESC_STACK:  Descriptor    0,     TopOfStack, DA_DRWA+DA_32; Stack, 32 位
LABEL_DESC_TEST:   Descriptor 0500000h,     0ffffh, DA_DRW

我们这次增加的描述符比较多,第一个是normal 描述符,第二个是16位非一致性代码描述符 ,第三个是数据段描述符,第四个是堆栈描述符,第五个是测试描述符!

总共新增加了5个描述符,描述符的分析就不详细说明了,上一章节已经有详细的说明了!

增加的描述符自然有相应的选择子和代码!

首先先看看在32位段,我们做了什么!

代码第163行处

[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS	32]

PS再次复习一下寄存器的用法:

AH&AL=AX:累加寄存器,常用于运算; BH&BL=BX:基址寄存器,常用于地址索引; CH&CL=CX:计数寄存器,常用于计数; DH&DL=DX:数据寄存器,常用于数据传递。

CS(Code Segment):代码段寄存器; DS(Data Segment):数据段寄存器; SS(Stack Segment):堆栈段寄存器; ES(Extra Segment):附加段寄存器。

DS是段寄存器 一般放的是数据段的段地址至于BX 是一个灵活的寄存器 可以用它来做许多事情 当然也可以用来当指针 楼主所谓数据段的基地址是这个段的起始地址 要说他是基地址也没有错 但是bx里放的是某个字节或字的地址用[bx]来访问。DS放段地址,BX是通用寄存器.

ds与bx配合,es与dx配合,cx作为计数器。这既是cpu硬件设计使然,也是软件设计的标准用法,就如围棋中的定式,你必须这么用。bx是提供偏移地址。

  • IP(Instruction Pointer):指令指针寄存器,与CS配合使用,可跟踪程序的执行过程;
  • SP(Stack Pointer):堆栈指针,与SS配合使用,可指向目前的堆栈位置。
  • BP(Base Pointer):基址指针寄存器,可用作SS的一个相对基址位置;
  • SI(Source Index):源变址寄存器可用来存放相对于DS段之源变址指针;
  • DI(Destination Index):目的变址寄存器,可用来存放相对于 ES 段之目的变址指针。

https://dbb4560.github.io/2019/08/09/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A03-%E5%AF%84%E5%AD%98%E5%99%A8%E5%92%8C%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80%E7%9A%84%E5%BD%A2%E6%88%90/

从代码看出ds指向数据段 SelectorData,es指向了SelectorTest (也即是5MB 内存的段),gs指向了SelectorVideo 也就是显存,ss指向了新增的堆栈SelectorStack。

  • 第181行显示一个字符串, mov ah,och 0000 是黑底,1100 是红字!
  • 第182-183行 ` xor esi, esi xor edi, edi ` esi 和 edi清零!
  • 第184-185行
    mov	esi, OffsetPMMessage	; 源数据偏移
    mov	edi, (80 * 10 + 0) * 2	; 目的数据偏移。屏幕第 10 行, 第 0 列。 
    

把 OffsetPMMessage 的地址赋值给esi,(80 * 10 + 0) * 2 偏移量赋值给edi!

  • 第186行 cld 指令将 DF 标志清零,以指示传送是正方向的。
  • 第187行 .1 这个是标记而已
  • 第188行 loadsb:用于目的地址的内容读到源地址。 即目标地址为:ES:DI,源地址为DS:SI

汇编语言中,串操作指令LODSB/LODSW是块装入指令,其具体操作是把SI指向的存储单元读入累加器,LODSB就读入AL,LODSW就读入AX中,然后SI自动增加或减小1或2.其常常是对数组或字符串中的元素逐个进行处理。

还记得X86汇编的写法吗,就是直接设置cx 计数器,然后使用rep movsw 来实现的!

  • 第189行 test al, al 汇编test 指令执行后, OF=CF=0;对 ZF、 SF 和 PF 的影响视测试结果而定;对AF 的影响未定义。 al为00 进行and运算的时候ZF变成1 00000000 and 00000000 结果为0,所以ZF变成1 al为01 进行and运算的时候ZF变成0 00000001 and 00000001 结果为1,所以ZF变成0

所以主要影响ZF 标志位!ZF(Zero Flag),即零标志!

所以第190行 jz .2 Jump if Zero 就是要看ZF标志位! jz 的意思是 ZF 标志为 1 则转移; jnz 的意思是 ZF 标志不为 1(为 0)则转移。

所以遇到字符串为0,就认为拷贝完毕,直接跳到.2 程序代码段!

  • 第191 行 mov [gs:edi], ax ,若al不为0 则将ax的值(已经有源地址的数据)赋值给显存地址! 注意因为ah已经存储了字符属性!
  • 第192 行 ` add edi, 2` 地址偏移2个字节!
  • 第193行 ` jmp .1 ` 跳回.1 继续执行! jmp无条件跳转指令
  • 第196-203行 如下:
      call	DispReturn
    
      call	TestRead
      call	TestWrite
      call	TestRead
    
    
      ; 到此停止
      jmp	SelectorCode16:0
    

首先说明 DispReturn 代码段!


; ------------------------------------------------------------------------
DispReturn:
	push	eax
	push	ebx
	mov	eax, edi
	mov	bl, 160
	div	bl          ; eax/bl 执行后al=当前行号  al = ax / 160, ah = ax mod 160
	and	eax, 0FFh   ; 只保留行号,列号清0 即 eax = al
	inc	eax          ; eax+=1,使eax为当前行的下一行
	mov	bl, 160
	mul	bl           ; eax*bl,eax为当前行的下一行的开始
	mov	edi, eax     ; 使edi指向当前行的下一行的开始
	pop	ebx
	pop	eax

	ret
; DispReturn 结束---------------------------------------------------------

DispReturn模拟一个回车的显示,(让下一个要显示的字符在下一行的开头处显示),其中edi始终指向要显示的下一个字符的位置。

注1: 8025彩色字模式的显示显存在内存中的地址为B8000h~BFFFH,共32k.向这个地址写入的内容立即显示在屏幕上边.在8025彩色字模式 下共可以显示25行,每行80字符,每个字符在显存中占两个字节,第一个字节是字符的ASCII码.第二字节是字符的属性,(80字符占160个字节)。 这段程序的功能是: 计算 edi = ( edi / 160(保留最后8位)+ 1) * 160! 就是回车功能!

书中提到一个细节就是

在程序的整个执行过程中,edi始终指向要显示的下一个字符的位。所以,如果程序中除显示字符外还用刀edi,一定要事先保存好edi的值!!!免得显示混乱~

  • 下一步是调用TestRead

; ------------------------------------------------------------------------
TestRead:
	xor	esi, esi
	mov	ecx, 8
.loop:
	mov	al, [es:esi] ;   从新增的5MB开始地址处开始
	call	DispAL
	inc	esi
	loop	.loop

	call	DispReturn

	ret
; TestRead 结束-----------------------------------------------------------

这段代码使用cx作为计数器,使用loop循环! 因为我们的目的是读取8个字节,所以ecs设置为8,es已经指向新增的5Mb的LABEL_DESC_TEST ! 下面要调用 DispAL! 注意edi的值没有动!

DispAL 代码如下:



; ------------------------------------------------------------------------
; 显示 AL 中的数字 (以16进制显示出来)
; 默认地:
;	数字已经存在 AL 中
;	edi 始终指向要显示的下一个字符的位置
; 被改变的寄存器:
;	ax, edi
; ------------------------------------------------------------------------
DispAL:
	push	ecx
	push	edx

	mov	ah, 0Ch			; 0000: 黑底    1100: 红字
	mov	dl, al          ; 用dl临时存储al的值
	shr	al, 4           ; 右移4位,相当于al / 16,目的是取高4位计算
	mov	ecx, 2          ; 计数器设置2
.begin:
	and	al, 01111b      ; 将AL 的高4位清零
	cmp	al, 9           ; al和9(1001)比较
	ja	.1              ; 若al大于9,就跳转到.1
	add	al, '0'         ; '0' 0x30 转换成ASCII码
	jmp	.2              ; 无条件跳转
.1:
	sub	al, 0Ah         ; al = al- 0x0A                                                          
	add	al, 'A'         ; al = al + 'A'
.2:
	mov	[gs:edi], ax    ;ax此时已经存储了转换后的ASCII码
	add	edi, 2          ; 让edi指向下一个位置

	mov	al, dl          ; dl把值赋值给al
	loop	.begin      ; 从begin开始,此时al计算切显示低4位
	add	edi, 2

	pop	edx
	pop	ecx

	ret
; DispAL 结束-------------------------------------------------------------

这段代码就不详细分析了! 就复习一下上面提到的指令

逻辑右移指令 shr

or可以将指定的位设置成1且 该指令会置CF=OF=0,其结果影响SF、ZF、PF ,and可以将指定的位设置成0 且 该指令会置CF=OF=0,其结果影响SF、ZF、PF。

cmp 指令在功能上和 sub 指令相同,唯一不同之处在于, cmp 指令仅仅根据计算的结果设置相 应的标志位,而不保留计算结果,因此也就不会改变两个操作数的原有内容。 cmp 指令将会影响到 CF、 OF、 SF、 ZF、 AF 和 PF 标志位。

拿指令 cmp ax,bx 来说,我们关心的是 AX 中的内容是否等于 BX 中的内容, AX 中的内容是否大于 BX 中的内容, AX 中的内容是否小于 BX 中的内容,等等, AX 是被测量的对象, BX 是测量的基准。

ja 若al大于9,就跳转到.1

DispReturn 代码前面已经讲得很详细了!

下一个执行代码出就是

call TestWrite

再次附上代码:


; ------------------------------------------------------------------------
TestWrite:
	push	esi
	push	edi             ; 保存现场
	xor	esi, esi
	xor	edi, edi            ; SI DI 都清零
	mov	esi, OffsetStrTest	; 源数据偏移 
	cld                     ; cld 指令将 DF 标志清零,以指示传送是正方向的。
.1:
	lodsb
	test	al, al
	jz	.2
	mov	[es:edi], al
	inc	edi
	jmp	.1
.2:

	pop	edi
	pop	esi

	ret
; TestWrite 结束----------------------------------------------------------

要写的数据的地址是OffsetStrTest 在第46-47行!

StrTest:		db	"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest		equ	StrTest - $$

另外,保护模式下用到堆栈了,push 和 pop 都是堆栈指令的!这个是因为我们建立了堆栈,在前面提到的新增的描述符中!

LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA+DA_32; Stack, 32 位

代码在第52-61行


; 全局堆栈段
[SECTION .gs]
ALIGN	32
[BITS	32]
LABEL_STACK:
	times 512 db 0

TopOfStack	equ	$ - LABEL_STACK - 1

; END of [SECTION .gs]

下一步就是跳转到实模式!

jmp SelectorCode16:0

这个会跳到 [SECTION .s16code] 代码段!

书上说的,我打字写一下:

我们从实模式进入保护模式时直接用一个跳转就可以了,但是返回的时候稍微复杂一些。 因为在准备结束保护回到实模式之前,需要加载一个合适的描述符选择子到有关的段寄存器,以使对应段描述符高速缓冲寄存器中含有合适的段界限和属性。

而且我们不能从32位代码段返回实模式,只能从16位代码段中返回。 这是因为无法实现从32位代码段返回时cs高速缓冲寄存器的中的属性符合实模式的要求(实模式不能改变段属性)。

这就是为何前面我说的新增了normal 描述符 LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符

返回实模式之前把对应的选择子selectorNormal 加载到ds es ss!


; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN	32
[BITS	16]
LABEL_SEG_CODE16:
	; 跳回实模式:
	mov	ax, SelectorNormal 
	mov	ds, ax
	mov	es, ax
	mov	fs, ax
	mov	gs, ax
	mov	ss, ax

	mov	eax, cr0
	and	al, 11111110b
	mov	cr0, eax

LABEL_GO_BACK_TO_REAL:
	jmp	0:LABEL_REAL_ENTRY	; 段地址会在程序开始处被设置成正确的值

Code16Len	equ	$ - LABEL_SEG_CODE16

; END of [SECTION .s16code]

代码说明: SelectorNormal 赋值给ds es fs gs ss! 然后清除cr0 的PE 位! 然后跳转,注意这个跳转语句后面有注释! 书上说是 程序刚开始的16位代码段已经设置正确了!

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

	mov	[LABEL_GO_BACK_TO_REAL+3], ax

mov [LABEL_GO_BACK_TO_REAL+3], ax

从代码可以看出ax 的值就是16位代码开始处(实模式)的段地址!

所以之前的 jmp 0:LABEL_REAL_ENTRY ; 其实就变成 jmp cs_real_mode:LABEL_REAL_ENTRY 跳转到LABEL_REAL_ENTRY

代码如下:

重新设置各个寄存器ds es ss,恢复了SP, 关闭A20 地址线,打开终端!

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

LABEL_REAL_ENTRY:		; 从保护模式跳回到实模式就到了这里
	mov	ax, cs
	mov	ds, ax
	mov	es, ax
	mov	ss, ax

	mov	sp, [SPValueInRealMode] ; 恢复sp的值

	in	al, 92h		; `.
	and	al, 11111101b	;  | 关闭 A20 地址线
	out	92h, al		; /

	sti			; 开中断

	mov	ax, 4c00h	; `.
	int	21h		; /  回到 DOS
; END of [SECTION .s16]

后面从实模式跳转到保护模式就不讲了!!

编译nasm pmtest2.asm -o pmtest2.com

启动bochs 后,自动启动freedos 输入b:\pmtest2.com 显示一堆字符串(16进制格式的) 显示完毕自动返回到freedos界面了!

如图所示: