实模式和保护模式是cpu的两种不同的工作模式,这两种工作模式主要的区别在于寻址方式的不同,寻址方式的意思是寻找地址的方式,这里稍微啰嗦一下,其实对于寻址更方便的理解方式应该是“寻值”,其实在汇编中无论是值还是地址或者变量本质上都是一个数值而已,所谓的“寻值”就是要确定这个东西在指令中究竟对应的数值是什么。
在介绍阆中不同工作模式下的不同寻址方式的时候,先介绍一下cpu中常见指令中的寻址。
1、立即寻址
mov ah ,80h ;把一个16进制数80放入寄存器中
80作为一个立即数,在指令中使用立即数的寻址方式称之为立即寻址,立即寻址在实模式和保护模式下并没有太大的区别,几乎一样
2、直接寻址
MOV BX, [1234H];
在指令中给出对应数值的直接有效地址的方式叫做直接寻址。
以上这条指令需要好好解释一下,因为两种模式下,直接寻址的工作方式是有区别的。上述这条指令,简单的来说就是:“把内存地址1234h的数据装入BX寄存器中,[]就像c中的数组一样,表示取[]中地址对应的数据”。但是在cpu进行实际执行这条指令的时候还有一些稍微复杂的工作。
先岔开一下说另一个事,我们看到的mov 实际上是伪指令,不同的mov指令对应的实际上的机器码是不同的,正如我们上面所看到的mov ah, 80h和mov bx,[1234h]这两个mov在经过汇编器的翻译之后会对应到不同的指令格式,所以是可以看成是两种不同的指令的,在cpu的执行方式是不同的。对于MOV BX, [1234H],在执行时,假设(DS)=2000H,内存单元21234H的值为5213H
cpu取出数据段寄存器DS中的值,将这个值乘以16之后再加上当前要访问的地址(也就是1234h),也就是如下的方式:
保护模式下寻址方式与实模式下有很大的不同,在保护模式中,内存中多了一张叫做GPT的表,这类似于一个数组,其中每个元素(都是一个结构体)(也就是表项)都指向一段对应的内存,包含着这块内存的基本属性的描述(比如说段基址,段界限什么的),如下图所示:
了解了cpu的寻址之后,接下来一步步分析于渊的代码:
6 %include "pm.inc" ; 常量, 宏, 以及一些说明
7
8 org 07c00h
9 jmp LABEL_BEGIN
首先是包含一些头文件和宏,规定起始于07c00h后就跳转到LABEL_BEGIN,接下来看看LABEL_BEGIN做了什么
28 [SECTION .s16] ;定义了一个节叫做.s16
29 [BITS 16] ;制定下面的指令位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 ;将eax置零
39 mov ax, cs ;将当前的代码段放入ax中
40 shl eax, 4 ; 16位下的段:基址寻址=段 × 16 + 基地址, 这里是> 先左移4位,代表乘16
41 add eax, LABEL_SEG_CODE32;将代码段的基地址加上
42 mov word [LABEL_DESC_CODE32 + 2], ax;初始化32位代码段的各个部分
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,
72 ; 并跳转到 Code32Selector:0 处
73 ; END of [SECTION .s16]
从37~45行的主要内容是对LABEL_DESC_CODE32这个地址对应的内存进行赋值,让我们来看看LABEL_DESC_CODE32对应的地址是什么东西
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; 非一致代码段
16 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
17 ; GDT 结束
在第15行我们找到了LABEL_DESC_CODE32,发现它是一个Descriptor类型,这个类型是作者自定义的宏结构体(理解成一个结构体就好),而后面跟着的三个参数可以用来初始化这个结构体对象,注意第11行显示的GDT注释,其实这一段就是GDT表,三个表项分别在LABEL_GDT,LABEL_DESC_CODE32,LABEL_DESC_VIDEO
所以也就是说37~45行就是对GPT表项的初始化,这个表项对应的内存段是一个代码段,因为41行中把LABEL_SEG_CODE32加入eax寄存器相加,并在后续讲运算完后的地址去初始化表项,LABEL_SEG_CODE32对应的代码如下,主要就是获取通过gpt中的对应屏幕内存段的表项(80~81),从而写屏幕映射的内存缓存(86)
79 LABEL_SEG_CODE32:
80 mov ax, SelectorVideo
81 mov gs, ax ; 获取gpt表项中屏幕对应的内存段的表项下标
82
83 mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
84 mov ah, 0Ch ; 0000: 黑底 1100: 红字
85 mov al, 'P'
86 mov [gs:edi], ax
87
88 ; 到此停止
89 jmp $
90
91 SegCode32Len equ $ - LABEL_SEG_CODE32
92 ; END of [SECTION .s32]
似乎跳的有点开,再回到程序的原来的运行的地方,这时候执行的是47~55行,如下,这一部分主要就是将GPT表的地址赋值给gdpt寄存器
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]
然后是57~63的关中断和打开A20门,在《oranges ‘s一个操作系统的实现》中P34开始对这部分有很详细的阐述了就不搬运了,
57 ; 关中断
58 cli
59
60 ; 打开地址线A20
61 in al, 92h
62 or al, 00000010b
63 out 92h, al
66~68是cr0寄存器的PE位置1,使cpu进入保护模式
65 ; 准备切换到保护模式
66 mov eax, cr0
67 or eax, 1
68 mov cr0, eax
最后是71行的,代码段的跳转,一开始死活没明白这句为什么跳转到一个偏移值就可以跳转代码,搞了很久才发现,这个时候cpu已经进入了保护模式,寻址方式已经是通过GPT进行的了,所以这个jmp是先把段偏移寄存器cs值设定成GPT表项中代码段表项的偏移值,然后跳转到该代码段的0偏移处,SelectorCode32:0后面的0表示的是从代码段的0偏移处开始执行,代码段我们已经介绍过了,就是37~45行对那个代码段表项的初始化成79~92的LABEL_SEG_CODE32的代码。如果对这里还是不明白那就只能是上面介绍保护模式下是这么寻址的部分没看
71 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
72 ; 并跳转到 Code32Selector:0 处
更多的寻址方式:http://blog.csdn.net/liutianshx2012/article/details/50731280
说说gpt:http://www.techbulo.com/708.html