代码清单
首先附上代码清单:
1 ;代码清单12-1
2 ;文件名:c12_mbr.asm
3 ;文件说明:硬盘主引导扇区代码
4 ;创建日期:2011-10-27 22:52
5
6 ;设置堆栈段和栈指针
7 mov eax,cs
8 mov ss,eax
9 mov sp,0x7c00
10
11 ;计算GDT所在的逻辑段地址
12 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位线性基地址
13 xor edx,edx
14 mov ebx,16
15 div ebx ;分解成16位逻辑地址
16
17 mov ds,eax ;令DS指向该段以进行操作
18 mov ebx,edx ;段内起始偏移地址
19
20 ;创建0#描述符,它是空描述符,这是处理器的要求
21 mov dword [ebx+0x00],0x00000000
22 mov dword [ebx+0x04],0x00000000
23
24 ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
25 mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xfffff
26 mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符
27
28 ;创建保护模式下初始代码段描述符
29 mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,512字节
30 mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符
31
32 ;创建以上代码段的别名描述符
33 mov dword [ebx+0x18],0x7c0001ff ;基地址为0x00007c00,512字节
34 mov dword [ebx+0x1c],0x00409200 ;粒度为1个字节,数据段描述符
35
36 mov dword [ebx+0x20],0x7c00fffe
37 mov dword [ebx+0x24],0x00cf9600
38
39 ;初始化描述符表寄存器GDTR
40 mov word [cs: pgdt+0x7c00],39 ;描述符表的界限
41
42 lgdt [cs: pgdt+0x7c00]
43
44 in al,0x92 ;南桥芯片内的端口
45 or al,0000_0010B
46 out 0x92,al ;打开A20
47
48 cli ;中断机制尚未工作
49
50 mov eax,cr0
51 or eax,1
52 mov cr0,eax ;设置PE位
53
54 ;以下进入保护模式... ...
55 jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移
56
57 [bits 32]
58 flush:
59 mov eax,0x0018
60 mov ds,eax
61
62 mov eax,0x0008 ;加载数据段(0..4GB)选择子
63 mov es,eax
64 mov fs,eax
65 mov gs,eax
66
67 mov eax,0x0020 ;0000 0000 0010 0000
68 mov ss,eax
69 xor esp,esp ;ESP <- 0
70
71 mov dword [es:0x0b8000],0x072e0750 ;字符'P'、'.'及其显示属性
72 mov dword [es:0x0b8004],0x072e074d ;字符'M'、'.'及其显示属性
73 mov dword [es:0x0b8008],0x07200720 ;两个空白字符及其显示属性
74 mov dword [es:0x0b800c],0x076b076f ;字符'o'、'k'及其显示属性
75
76 ;开始冒泡排序
77 mov ecx,pgdt-string-1 ;遍历次数=串长度-1
78 @@1:
79 push ecx ;32位模式下的loop使用ecx
80 xor bx,bx ;32位模式下,偏移量可以是16位,也可以
81 @@2: ;是后面的32位
82 mov ax,[string+bx]
83 cmp ah,al ;ah中存放的是源字的高字节
84 jge @@3
85 xchg al,ah
86 mov [string+bx],ax
87 @@3:
88 inc bx
89 loop @@2
90 pop ecx
91 loop @@1
92
93 mov ecx,pgdt-string
94 xor ebx,ebx ;偏移地址是32位的情况
95 @@4: ;32位的偏移具有更大的灵活性
96 mov ah,0x07
97 mov al,[string+ebx]
98 mov [es:0xb80a0+ebx*2],ax ;演示0~4GB寻址。
99 inc ebx
100 loop @@4
101
102 hlt
103
104 ;-------------------------------------------------------------------------------
105 string db 's0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.'
106 ;-------------------------------------------------------------------------------
107 pgdt dw 0
108 dd 0x00007e00 ;GDT的物理地址
109 ;-------------------------------------------------------------------------------
110 times 510-($-$$) db 0
111 db 0x55,0xaa
进入 32 位保护模式
在 16 位模式下,传送到 DS 中的值是逻辑段地址;在 32 位保护模式下,传送的是段描述符的选择子。无论传送的是什么,这都不重要,重要的是, 在 16 位模式和 32 位模式下,一些老式的编译器会生成不同的机器代码。
NASM 编译器还是非常优秀的,起码它不会有这样的问题。因此,不管处 理器模式如何变化,也不管指令形式如何变化,以下代码编译后的结果都一模一样:
[bits 16]
mov ds,ax ;8E D8
mov ds,eax ;8E D8
[bits 32]
mov ds,ax ;8E D8
mov ds,eax ;8E D8
其他从通用寄存器到段寄存器的传送也符合这样的编译规则。
- 第 7、 8 行,用于通过寄存器 EAX 来初始化堆栈段寄存器 SS。
创建GDT 和安装段描述符前面章节已经有说明了! 思路是一样的!
上一章里, GDT 的大小和线性基地址分别是用两个标号 gdt_size 和 gdt_base 声明和初始化的:
gdt_size dw 0
gdt_base dd 0x0000007e00
如后面的第 107、 108 行所示,现在已经改成:
pdgt dw 0
dd 0x00007e00
另外一个区别是计算 GDT 逻辑地址的方法:
在 32 位处理器上,即使是在实模式下,也可以使用 32 位寄存器。所以,第 12 行,直接将 GDT 的 32 位线性基地址传送到寄存器 EAX 中。
32 位处理器可以执行以下除法操作: ` div r/m32 ` 其中, 64 位的被除数在 EDX:EAX 中, 32 位被除数可以在 32 位通用寄存器中,也可以在 32 位内存单元中。
第 13~15 行,用 64 位的被除数 EDX:EAX 除以 32 位的除数 EBX。指令执行后, EAX 中的商是段地址,仅低 16 位有效; EDX 中的余数是段内偏移地址,仅低 16 位有效。
第 17、 18 行,初始化段寄存器 DS,使其指向 GDT 所在的逻辑段。
第 21、 22 行,安装空描述符。该描述符的槽位号是 0。
第 25、 26 行,安装保护模式下的数据段描述符。 该段的线性基地址位于整个内存的最低端,为 0x00000000;属于 32 位的段,段界限是 0xFFFFF。但是要注意,段的粒度是以 4KB 为单位的。对于以 4KB(十进制数 4096 或者十六进制数 0x1000)为粒度的段,描述符中的界限值加 1,就是该段有多少个 4KB!
其实际使用的段界限为
(描述符中的段界限值+1)×0x1000-1
展开后
描述符中的段界限值×0x1000+0x1000-1
在换算成实际使用的段界限时,其公式为
描述符中的段界限值×0x1000+0xFFF
这就是说,实际使用的段界限是
0xFFFFF×0x1000+0xFFF=0xFFFFFFFF
也就是 4GB。就 32 位处理器来说,这个地址范围已经最大了。
第 29、30 行,安装保护模式下的代码段描述符。该段是 32 位的代码,线性基地址为 0x00007C00;段界限为 0x001FF,粒度为字节。对于向上扩展的段来说,段界限在数值上等于段的长度减去 1,因此该段的长度是 0x200,即 512 字节。 该段实际上就是当前程序所在的段(正在安装该描述符呢),也就是主引 导程序所在的区域。尽管在描述符中把它定义成 32 位的段,但它实际上既包含 16 位代码,也包含32 位代码。
[bits 32]之前的代码是 16 位的,之后的代码是 32 位的。
第 33、 34 行,安装保护模式下的数据段描述符。
该段是 32 位的数据段,线性基地址为0x00007C00;段界限为 0x001FF,粒度为字节。可以看出,该描述符和前面的代码段描述符,描述和指向的是同一个段。
为什么呢? 就是为了方便修改代码,就这么简单!后面是从书上直接摘抄上来,自己直接看!
在保护模式下,代码段是不可写入的。所谓不可写入,并非是说改变了内存的物理性质, 使得内存写不进去, 而是说,通过该段的描述符来访问这个区域 时, 处理器不允许向里面写入数据或者更改数据。
很多时候,又需要对代码段做一些修改。比如在调试程序时,需要加入断点指令 int3。不管怎么样,如果需要访问代码段内的数据,只能重新为该段安装一个新的描述符,并将其定义为可读可写的数据段。这样,当需要修改代码段内的数据时,可以通过这个新的描述符来进行。
当两个以上的描述符都描述和指向同一个段时,把另外的描述符称为别名(alias)。别名技术并非仅仅用于读写代码段,如果两个程序想共享同一个内存区域,可以分别为每个程序都创建一个描述符,而且它们都指向同一个内存段,这也是别名应用的例子。
第 36、 37 行,安装保护模式下的堆栈段描述符。 该 段 的 线 性 基 地 址 是 0x00007C00 , 段 界 限 为0xFFFFE,粒度为 4KB。
第 40 行,设置 GDT 的界限值为 39,因为这里共有 5 个描述符,总大小为 40 字节,界限值为 39。
后面的代码用于进入保护模式,差不多和上一章相同!
它们在内存中的映象如图 12-1 所示:
修改段寄存器时的保护
代码清单 12-1 第 55 行,这是一条直接远转移指令:
jmp dword 0x0010:flush
这条指令会隐式地修改段寄存器 CS。
修改段寄存器的指令还出现在第 59~68 行。
代码段执行时的保护
代码段是向上(高地址方向)扩展的,因此,实际使用的段界限就是当前段内最后一个允许访问的偏移地址。当处理器在该段内取指令执行时,偏移地址由 EIP 提供。指令很有可能是跨越边界的,一部分在边界之内,一部分在边界之外,或者一条单字节指令正好位于边界上。因此,要执行的那条指令,其长度减 1 后,与 EIP 寄存器的值相加,结果必须小于等于实际使用的段界限,否则引发处理器异常。即:
0≤(EIP+指令长度-1)≤实际使用的段界限
代码段描述符中给出的界限值是 0x001FF,粒度是字节,可以认为它就是段内最后一个允许访问的偏移地址。如图 12-3 所示,在处理器取得一条指令后, EIP 寄存器的数值加上该指令的长度减 1,得到的结果必须小于等于 0x000001FF,如果等于或者超出这个数值,必然引发异常中断。
做为一个额外的例子,现在,假设当前代码段的粒度是 4KB,那么,因为描述符中的段界限值是 0x001FF,故实际使用的段界限是
0x1FF×0x1000+0xFFF=0x001FFFFF
可以认为,此数值就是当前段内最后一个允许访问的偏移地址。任何时候, EIP 寄存器的内容加上取得的指令长度减 1,都必须小于等于 0x001FFFFF,否则将引发处理器异常中断。
堆栈操作时的保护
堆栈本身始终总是向下增长的,即,向低地址方向推进。 段的扩展方向 用于处理器的界限检查,而对堆栈的性质以及在堆栈上进行的操作没有关系。
在堆栈段中, 实际使用的段界限也和粒度(G)位相关,如果 G=0,实际使用的段界限就是描述符中记载的段界限;如果 G=1,则实际使用的段界限为
描述符中的段界限值×0x1000+0xFFF
堆栈段是向下扩展的,每当往堆栈中压入数据时, ESP 的内容要减去操作数的长度。所以,和向高地址方向扩展的段相比,非常重要的一点就是,实际使用的段界限就是段内不允许访问的最低端偏移地址。至于最高端的地址,则没有限制,最大可以是 0xFFFFFFFF。也就是说,在进行堆栈操作时,必须符合以下规则!
实际使用的段界限+1≤(ESP 的内容-操作数的长度)≤0xFFFFFFFF
看代码清单 12-1 第 67~69 行。这三行设置堆栈的线性基地址为0x00007C00,段界限为 0xFFFFE,粒度为 4KB,并设置堆栈指针寄存器 ESP 的初值为 0。
因为段界限的粒度是 4KB(G=1),故实际使用的段界限
0xFFFFE×0x1000+0xFFF=0xFFFFEFFF
又因为 ESP 的最大值是 0xFFFFFFFF,因此,如图 12-4 所示,在操作该段时,处理器的检查规则是:
0xFFFFF000≤(ESP 的内容-操作数的长度) ≤0xFFFFFFFF
堆栈指针寄存器 ESP 的内容仅仅在访问堆栈时提供偏移地址,操作数在压入堆栈时的物理地址要用段寄存器的描述符高速缓存器中的段基址和 ESP 的内容相加得到。因此,该堆栈最低端的有效物理地址是:
0x00007C00+0xFFFFF000=0x00006C00
最高端的有效物理地址是
0x00007C00+0xFFFFFFFF=0x00007BFF
也就是说,当前程序所定义的堆栈空间介于地址为 0x00006C00~0x00007BFF 之间,大小是 4KB。
现在结合该堆栈段,用一个实例来说明处理器的检查过程。代码清单第 69 行将 ESP 的初始值设定为 0,因此,当第一次进行压栈操作时,假如压入的是一个双字(4 字节):
push ecx
因为压栈操作是先减 ESP,然后再访问堆栈,故 ESP 的新值是(可以自行用 Windows 计算器算一下)
0-4=0xFFFFFFFC
这个结果符合上面的限制条件,允许操作。此时,被压入的那个双字,其线性地址为
0x00007C00+0xFFFFFFFC=0x00007BFC
数据访问时的保护
这里所说的数据段,特指向上扩展的数据段,有别于堆栈和向下扩展的数据段。 定义了一个具有 4GB 长的段,段的基地址是 0x00000000,段界限是 0xFFFFF,粒度为 4KB。因此,实际使用的段界限是0xFFFFF×0x1000+0xFFF=0xFFFFFFFF。
代码清单 12-1 第 71~74 行,从物理地址 0x000B8000 开始写入 16 字节的内容,用于演示4GB 内存地址空间的访问。
段寄存器 ES 当前正指向 0 到 4GB 的内存空间,其描述符高速缓存 器中的基地址是 0x00000000,加上指令中提供的 32 位偏移量,所访问的地方正是显示缓冲区(显存)所在的区域。这其中的道理很简单,首先,内存的寻址依赖于段基地址和偏移地址,段基地址是 0,所以,可以把任何要访问的物理地址作为偏移量。
使用别名访问代码段对字符排序
对一串散乱的字符进行排序,主要说明如何在保护模式下使用别名段!
代码清单 12-1 的第 105 行,用标号 string 声明,并初始化为以下字符:
s0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.
这串字符是主引导程序的一部分,在进入保护模式时,它就位于 32 位代码段中。代码段是用来执行的,能不能读出,取决于其描述符的类别字段。但是无论如何,它都不允许写入。
直接用别名描述符来修改就行!就是代码段的别名描述符,而且用段寄存器 DS 指向它。参见代码清单 12-1 第 59、60 行。
用的是冒泡排序法排序的!