您的位置:首页 > 产品设计 > 产品经理

《自己动手写操作系统》第三章pmtest2源码解析

2014-03-03 20:51 405 查看
摘要:本节,通过代码解析,帮你解决如下问题:保护模式和实模式下面的偏移量有什么不同?保护膜是和实模式下段基地址是一个意思吗?长跳转指令jmp 0:entry为什么能够在一个代码段中更改另一个代码段的代码?

一、总体思路剖析:

pmtest2,其实实现的是从实模式到保护模式,然后从保护模式回到实模式,最后回归到dos。其中,一开始就进入了实模式,然后在实模式下初始化段描述符,处理GDT等,进入保护模式,在保护模式下完成了一些显示字符串和拷贝读取字符串的功能;最后通过一个normal段,回到实模式,然后通过中断,回到dos程序中。这个过程具体代码如下:
; ==========================================; pmtest2.asm; 编译方法:nasm pmtest2.asm -o pmtest2.com; ==========================================%include	"pm.inc"	; 常量, 宏, 以及一些说明org	0100h	jmp	LABEL_BEGIN[SECTION .gdt]; GDT;                                         段基址,       段界限     , 属性LABEL_GDT:		Descriptor	       0,                 0, 0     		; 空描述符LABEL_DESC_NORMAL:	Descriptor	       0,            0ffffh, DA_DRW		; Normal 描述符LABEL_DESC_CODE32:	Descriptor	       0,  SegCode32Len - 1, DA_C + DA_32	; 非一致代码段, 32LABEL_DESC_CODE16:	Descriptor	       0,            0ffffh, DA_C		; 非一致代码段, 16LABEL_DESC_DATA:	Descriptor	       0,	DataLen - 1, DA_DRW		; DataLABEL_DESC_STACK:	Descriptor	       0,        TopOfStack, DA_DRWA + DA_32	; Stack, 32 位LABEL_DESC_TEST:	Descriptor	0500000h,            0ffffh, DA_DRWLABEL_DESC_VIDEO:	Descriptor	 0B8000h,            0ffffh, DA_DRW		; 显存首地址; GDT 结束GdtLen		equ	$ - LABEL_GDT	; GDT长度GdtPtr		dw	GdtLen - 1	; GDT界限		dd	0		; GDT基地址; GDT 选择子SelectorNormal		equ	LABEL_DESC_NORMAL	- LABEL_GDTSelectorCode32		equ	LABEL_DESC_CODE32	- LABEL_GDTSelectorCode16		equ	LABEL_DESC_CODE16	- LABEL_GDTSelectorData		equ	LABEL_DESC_DATA		- LABEL_GDTSelectorStack		equ	LABEL_DESC_STACK	- LABEL_GDTSelectorTest		equ	LABEL_DESC_TEST		- LABEL_GDTSelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT; END of [SECTION .gdt][SECTION .data1]	 ; 数据段ALIGN	32[BITS	32]LABEL_DATA:SPValueInRealMode	dw	0; 字符串PMMessage:		db	"In Protect Mode now. ^-^", 0	; 进入保护模式后显示此字符串OffsetPMMessage		equ	PMMessage - $$StrTest:		db	"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0OffsetStrTest		equ	StrTest - $$DataLen			equ	$ - LABEL_DATA; END of [SECTION .data1]; 全局堆栈段[SECTION .gs]ALIGN	32[BITS	32]LABEL_STACK:	times 512 db 0TopOfStack	equ	$ - LABEL_STACK - 1; END of [SECTION .gs][SECTION .s16][BITS	16]LABEL_BEGIN:	mov	ax, cs	mov	ds, ax	mov	es, ax	mov	ss, ax	mov	sp, 0100h	mov	[LABEL_GO_BACK_TO_REAL+3], ax	mov	[SPValueInRealMode], sp	; 初始化 16 位代码段描述符	mov	ax, cs	movzx	eax, ax	shl	eax, 4	add	eax, LABEL_SEG_CODE16	mov	word [LABEL_DESC_CODE16 + 2], ax	shr	eax, 16	mov	byte [LABEL_DESC_CODE16 + 4], al	mov	byte [LABEL_DESC_CODE16 + 7], ah	; 初始化 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	ax, ds	shl	eax, 4	add	eax, LABEL_DATA	mov	word [LABEL_DESC_DATA + 2], ax	shr	eax, 16	mov	byte [LABEL_DESC_DATA + 4], al	mov	byte [LABEL_DESC_DATA + 7], ah	; 初始化堆栈段描述符	xor	eax, eax	mov	ax, ds	shl	eax, 4	add	eax, LABEL_STACK	mov	word [LABEL_DESC_STACK + 2], ax	shr	eax, 16	mov	byte [LABEL_DESC_STACK + 4], al	mov	byte [LABEL_DESC_STACK + 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  处;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;LABEL_REAL_ENTRY:		; 从保护模式跳回到实模式就到了这里	mov	ax, cs	mov	ds, ax	mov	es, ax	mov	ss, ax	mov	sp, [SPValueInRealMode]	in	al, 92h		; ┓	and	al, 11111101b	; ┣ 关闭 A20 地址线	out	92h, al		; ┛	sti			; 开中断	mov	ax, 4c00h	; ┓	int	21h		; ┛回到 DOS; END of [SECTION .s16][SECTION .s32]; 32 位代码段. 由实模式跳入.[BITS	32]LABEL_SEG_CODE32:	mov	ax, SelectorData	mov	ds, ax			; 数据段选择子	mov	ax, SelectorTest	mov	es, ax			; 测试段选择子	mov	ax, SelectorVideo	mov	gs, ax			; 视频段选择子	mov	ax, SelectorStack	mov	ss, ax			; 堆栈段选择子	mov	esp, TopOfStack	; 下面显示一个字符串	mov	ah, 0Ch			; 0000: 黑底    1100: 红字	xor	esi, esi	xor	edi, edi	mov	esi, OffsetPMMessage	; 源数据偏移	mov	edi, (80 * 10 + 0) * 2	; 目的数据偏移。屏幕第 10 行, 第 0 列。	cld.1:	lodsb	test	al, al	jz	.2	mov	[gs:edi], ax	add	edi, 2	jmp	.1.2:	; 显示完毕	xchg	bx,bx	call	DispReturn	call	TestRead	call	TestWrite	call	TestRead	; 到此停止	jmp	SelectorCode16:0; ------------------------------------------------------------------------TestRead:	xor	esi, esi	mov	ecx, 8.loop:	mov	al, [es:esi]	call	DispAL	inc	esi	loop	.loop	call	DispReturn	ret; TestRead 结束-----------------------------------------------------------; ------------------------------------------------------------------------TestWrite:	push	esi	push	edi	xor	esi, esi	xor	edi, edi	mov	esi, OffsetStrTest	; 源数据偏移	cld.1:	lodsb	test	al, al	jz	.2	mov	[es:edi], al	inc	edi	jmp	.1.2:	pop	edi	pop	esi	ret; TestWrite 结束----------------------------------------------------------; ------------------------------------------------------------------------; 显示 AL 中的数字; 默认地:;	数字已经存在 AL 中;	edi 始终指向要显示的下一个字符的位置; 被改变的寄存器:;	ax, edi; ------------------------------------------------------------------------DispAL:	push	ecx	push	edx	mov	ah, 0Ch			; 0000: 黑底    1100: 红字	mov	dl, al	shr	al, 4	mov	ecx, 2.begin:	and	al, 01111b	cmp	al, 9	ja	.1	add	al, '0'	jmp	.2.1:	sub	al, 0Ah	add	al, 'A'.2:	mov	[gs:edi], ax	add	edi, 2	mov	al, dl	loop	.begin	add	edi, 2	pop	edx	pop	ecx	ret; DispAL 结束-------------------------------------------------------------; ------------------------------------------------------------------------DispReturn:	push	eax	push	ebx	mov	eax, edi	mov	bl, 160	div	bl	and	eax, 0FFh	inc	eax	mov	bl, 160	mul	bl	mov	edi, eax	pop	ebx	pop	eax	ret; DispReturn 结束---------------------------------------------------------SegCode32Len	equ	$ - LABEL_SEG_CODE32; END of [SECTION .s32]; 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, eaxLABEL_GO_BACK_TO_REAL:	jmp	0:LABEL_REAL_ENTRY	; 段地址会在程序开始处被设置成正确的值Code16Len	equ	$ - LABEL_SEG_CODE16; END of [SECTION .s16code]

二、代码解惑

1.align 32是什么意思?为什么要写align 32?答:align是一个让数据对齐的宏。通常align的对象是1、4、8等。这里的align 32是没有意义的,本来就是只有32b的地址总线宽度,怎么还32对齐?不可能。2.既然我们说PMMessage是表示段内offset,那么我们为什么还要定义一个变量OffsetPMMessage呢?$、$$、offsetpmmessage、pmmessage的地址分别是什么?$:当前行被汇编之后的地址,是实际的线性地址$$:一个section的开始地方被汇编以后的地址,也是实际的线性地址pmmessage:偏移地址(相对段的首地址)既然offset是偏移地址,为什么不直接用offset呢?要解答这些疑惑,我们首先去寻找段基址:你有没有注意到一个问题,在“初始化段描述符”的部分,我们用来初始化段基地址首地址的寄存器,都是cs和ds(实际上二者的数值相等);从某种角度上说,他们处在“同一个段”中。不过分属于不同的offset对应的部分,我们通过段描述符,实现了不同区域的不同权限管理。因为现在程序很小,我们可以在20b的offset之内包括所有的代码和数据,所以这样做是没有任何问题的。我们再看看保护模式下发生的变化:本来在实模式下,它们属于同一个段,但是最后段基地显然在初始化段描述符的时候悄然发生了变化(都采用了base*16+offset,而offset是不同的)。好了,再来看看“pmmessage-$$”的真实含义,pmmessage和$$都表示实模式下相对与段基地址的offset;但后来随着段基地址的漂移,$$变成了首地址,所以pmmessage对应于保护模式下的偏移自然也就发生了变化,需要减去$$对应的地址才行。大部分时候,我们在编程的时候,只需要关注offset就可以了。3.section和段之间有什么区别和联系?程序代码段执行的第一句,mov ax,cs对应的offset会是0吗?section和段之间没有必然的联系,一般我们习惯将一个section放在一个段里面,不过这是用户习惯,不是语法要求——我们可以把两个section放在段里面。mov ax,cs在这里,实模式下offset一般不等于0,保护模式下,这一句的偏移一般是0.4.这里,我们需要根据打印字符串部分总结一下字符串操作的原理、技巧?汇编中判断和循环语句都有哪些?参见汇编语言语法简要总结。5.换行的原理是什么?实际上就是靠操作edi来实现,edi=[edi/160]+16.保护模式下长跳转指令的原理?jmp 0:xx是如何跳转的。从指令结构上来讲,书上的解释已经很明确了——但是jmp 0:xx中的0是如何被改变的呢?因为jmp指令的机器码在编译的时候已经产生了,但是运行之后,它的机器码被改掉了。这样看起来解释得通,但是再仔细想想,是不是有什么地方不对劲?——代码段的内容怎么会可以更改呢?仔细看看代码,又发现了猫腻——原来对代码的改动发生在实模式,这时分段机制还没对代码起到保护作用。7.PMMessage后面定义的dd,但是内容不止4b,该如何处理?dd还是db,表示的是后面的一个单位,而不是所有的内容。例如dd 3,2,4,6,5:这样定义的就是20个字节。8.movzx指令mov eax,bx是非法的,所以要显示的高位补09.带有.的loop和一般的label有什么不同 	这是本地label的意思:NASM对于那些以一个句点开始的符号会作特殊处理,一个以单个句点开始的 Label会被处理成本地label, 这意味着它会跟前面一个非本地label相关联.比如:      label1  ; some code            .loop              ; some more code                    jne     .loop              ret             label2  ; some code            .loop              ; some more code                    jne     .loop              ret  上面的代码片断中,每一个'JNE'指令跳至离它较近的前面的一行上,因为'.loop'的两个定义通过与它们前面的非本地Label相关联而被分离开来了。 10.32b数据段和16b数据段有什么区别?32b的堆栈段呢对于stack,位数不同将导致压栈和出战的位数不同;对于数据段,没有什么区别;对于代码段,将决定是ecs还是cs等信息;所以,在32b的物理机器上,你可以都定义成16b的段;但是你不能在16b的机器上,定义32b的段11.保护模式和实模式段地址有什么区别?保护模式下32b,不用进行偏移就直接和offset相加;实模式下16b,需要左移四位然后在+offset。

三、调试情况:

打印read和write位置和内容都出现异常,无法回到dos程序之下。
解决方法:调整test段的地址,使得它的地址范围降低,然后像其他段描述符一样进行初始化。
遗留问题:没有解决test段在原文情况下不可写的情况,最后证明,这是一个随机会出现问题的地方。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: