保护模式
X86汇编已经把保护模式讲的很清楚,就不详细记录了!
这里详细说明一下为什么第一个描述符是空的!
这里其实是跟CPU 支持多任务的设计有关系!
用于当一个任务使用的所有段都是系统全局段时。
GDTR(48位)用于描述GDT的基址和界限
LDTR(16位)用于描述当前任务的LDT在GDT中的选择子。
如果一个任务没有LDT,就会把LDTR清空,此时指向GDT中的第0项描述符,即为空描述符。
这里又引申出来LDT 没有空描述符直说!
为什么全局描述符表GDT的第0项总是一个空描述符,而局部描述符表却不是这样!
386的保护模式下:
DT=GDT*1+IDT*1+LDT*n;
IDT和每个LDT都要到GDT中报一次到.有一个描述符项与一张表对应.
其实什么是描述表呢?
很简单.1个段描述表记录记录一个段的特征信息 中断描述符表记录中断的端口和其对应的函数入口地址或门的入口函数地址
全局描述表GDT记录所有表的地址.其中的项称之为描述符.就是这里记录CPL.DPL的信息
前面说到GDT和IDT是整个系统一张,而LDT可以每个任务独占一长,用于存储每个任务私有的段的信息,所以当任务发生切换时,LDT也要随之切换,CPU中专门用一个16位的寄存器DTR来存储当前任务的LDT在GDT中的描述符的选择子,以此来定位当前任务的LDT。同时也存在这么一种情况,那就是一个任务使用的所有段都是系统全局的,它不需要用LDT来存储私有段信息,因此,当系统切换到这种任务时,会将LDTR寄存器赋值成一个空(全局描述符)选择子,选择子的描述符索引值为0,TI指示位为0,RPL可以为任意值,用这种方式表明当前任务没有LDT。这里的空选择子因为TI为0,所以它实际上指向了GDT的第0项描述符,第0项的作用类似于C语言中NULL的用法,它虽然是一个描述符,但却只起到到了标志的作用,规定GDT的第0项描述符为空描述符,其8个字节全为0,就是这个原因。如果把前面的空描述符选择子的TI位改为1,使之指向LDT中的0号描述符,这样的选择子就不是空选择子,它指向的LDT中的0号描述符是可以正常使用的,也就是LDT中没有空描述符一说!
代码详细讲解
随书代码都上传百度网盘 链接:https://pan.baidu.com/s/17tlHRQB3R_Pz86aAvDIFVw 提取码:m5ur
代码路径是 chapter3\apmtest1.asm 和 pm.inc
这次代码较少,只有92行,附上代码清单
1 ; ==========================================
2 ; pmtest1.asm
3 ; 编译方法:nasm pmtest1.asm -o pmtest1.bin
4 ; ==========================================
5
6 %include "pm.inc" ; 常量, 宏, 以及一些说明
7
8 org 07c00h
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; 非一致代码段
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 位代码段描述符
; 在实模式下通过段寄存器×16 + 偏移得到物理地址,
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 ; 段基址1
43 shr eax, 16
44 mov byte [LABEL_DESC_CODE32 + 4], al ; 段基址 1
45 mov byte [LABEL_DESC_CODE32 + 7], ah
46
47 ; 为加载 GDTR 作准备 ; 得到段描述符表的物理地址,并将其放到GdtPtr中
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]
74
75
76 [SECTION .s32]; 32 位代码段. 由实模式跳入.
77 [BITS 32]
78
79 LABEL_SEG_CODE32:
80 mov ax, SelectorVideo
81 mov gs, ax ; 视频段选择子(目的)
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]
93
94
头文件pm.inc
1
2
3 ; 描述符图示
4
5 ; 图示一
6 ;
7 ; ------ ┏━━┳━━┓高地址
8 ; ┃ 7 ┃ 段 ┃
9 ; ┣━━┫ ┃
10 ; 基
11 ; 字节 7 ┆ ┆ ┆
12 ; 址
13 ; ┣━━┫ ② ┃
14 ; ┃ 0 ┃ ┃
15 ; ------ ┣━━╋━━┫
16 ; ┃ 7 ┃ G ┃
17 ; ┣━━╉──┨
18 ; ┃ 6 ┃ D ┃
19 ; ┣━━╉──┨
20 ; ┃ 5 ┃ 0 ┃
21 ; ┣━━╉──┨
22 ; ┃ 4 ┃ AVL┃
23 ; 字节 6 ┣━━╉──┨
24 ; ┃ 3 ┃ ┃
25 ; ┣━━┫ 段 ┃
26 ; ┃ 2 ┃ 界 ┃
27 ; ┣━━┫ 限 ┃
28 ; ┃ 1 ┃ ┃
29 ; ┣━━┫ ② ┃
30 ; ┃ 0 ┃ ┃
31 ; ------ ┣━━╋━━┫
32 ; ┃ 7 ┃ P ┃
33 ; ┣━━╉──┨
34 ; ┃ 6 ┃ ┃
35 ; ┣━━┫ DPL┃
36 ; ┃ 5 ┃ ┃
37 ; ┣━━╉──┨
38 ; ┃ 4 ┃ S ┃
39 ; 字节 5 ┣━━╉──┨
40 ; ┃ 3 ┃ ┃
41 ; ┣━━┫ T ┃
42 ; ┃ 2 ┃ Y ┃
43 ; ┣━━┫ P ┃
44 ; ┃ 1 ┃ E ┃
45 ; ┣━━┫ ┃
46 ; ┃ 0 ┃ ┃
47 ; ------ ┣━━╋━━┫
48 ; ┃ 23 ┃ ┃
49 ; ┣━━┫ ┃
50 ; ┃ 22 ┃ ┃
51 ; ┣━━┫ 段 ┃
52 ;
53 ; 字节 ┆ ┆ 基 ┆
54 ; 2, 3, 4
55 ; ┣━━┫ 址 ┃
56 ; ┃ 1 ┃ ① ┃
57 ; ┣━━┫ ┃
58 ; ┃ 0 ┃ ┃
59 ; ------ ┣━━╋━━┫
60 ; ┃ 15 ┃ ┃
61 ; ┣━━┫ ┃
62 ; ┃ 14 ┃ ┃
63 ; ┣━━┫ 段 ┃
64 ;
65 ; 字节 0,1┆ ┆ 界 ┆
66 ;
67 ; ┣━━┫ 限 ┃
68 ; ┃ 1 ┃ ① ┃
69 ; ┣━━┫ ┃
70 ; ┃ 0 ┃ ┃
71 ; ------ ┗━━┻━━┛低地址
72 ;
73
74
75 ; 图示二
76
77 ; 高地址………………………………………………………………………低地址
78
79 ; | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
80 ; |7654321076543210765432107654321076543210765432107654321076543210| <- 共 8 字节
81 ; |--------========--------========--------========--------========|
82 ; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓
83 ; ┃31..24┃ (见下图) ┃ 段基址(23..0) ┃ 段界限(15..0)┃
84 ; ┃ ┃ ┃ ┃ ┃
85 ; ┃ 基址2┃③│②│ ①┃基址1b│ 基址1a ┃ 段界限1 ┃
86 ; ┣━━━╋━━━┳━━━╋━━━━━━━━━━━╋━━━━━━━┫
87 ; ┃ %6 ┃ %5 ┃ %4 ┃ %3 ┃ %2 ┃ %1 ┃
88 ; ┗━━━┻━━━┻━━━┻━━━┻━━━━━━━┻━━━━━━━┛
89 ; │ \_________
90 ; │ \__________________
91 ; │ \________________________________________________
92 ; │ \
93 ; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓
94 ; ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃
95 ; ┣━━╋━━╋━━╋━━╋━━┻━━┻━━┻━━╋━━╋━━┻━━╋━━╋━━┻━━┻━━┻━━┫
96 ; ┃ G ┃ D ┃ 0 ┃ AVL┃ 段界限 2 (19..16) ┃ P ┃ DPL ┃ S ┃ TYPE ┃
97 ; ┣━━┻━━┻━━┻━━╋━━━━━━━━━━━╋━━┻━━━━━┻━━┻━━━━━━━━━━━┫
98 ; ┃ ③: 属性 2 ┃ ②: 段界限 2 ┃ ①: 属性1 ┃
99 ; ┗━━━━━━━━━━━┻━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━┛
100 ; 高地址 低地址
101 ;
102 ;
103
104 ; 说明:
105 ;
106 ; (1) P: 存在(Present)位。
107 ; P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中;
108 ; P=0 表示描述符对地址转换无效,即该段不存在。使用该描述符进行内存访问时会引起异常。
109 ;
110 ; (2) DPL: 表示描述符特权级(Descriptor Privilege level),共2位。它规定了所描述段的特权级,用于特权检查,以决定对该段能否访问。
111 ;
112 ; (3) S: 说明描述符的类型。
113 ; 对于存储段描述符而言,S=1,以区别与系统段描述符和门描述符(S=0)。
114 ;
115 ; (4) TYPE: 说明存储段描述符所描述的存储段的具体属性。
116 ;
117 ;
118 ; 数据段类型 类型值 说明
119 ; ----------------------------------
120 ; 0 只读
121 ; 1 只读、已访问
122 ; 2 读/写
123 ; 3 读/写、已访问
124 ; 4 只读、向下扩展
125 ; 5 只读、向下扩展、已访问
126 ; 6 读/写、向下扩展
127 ; 7 读/写、向下扩展、已访问
128 ;
129 ;
130 ; 类型值 说明
131 ; 代码段类型 ----------------------------------
132 ; 8 只执行
133 ; 9 只执行、已访问
134 ; A 执行/读
135 ; B 执行/读、已访问
136 ; C 只执行、一致码段
137 ; D 只执行、一致码段、已访问
138 ; E 执行/读、一致码段
139 ; F 执行/读、一致码段、已访问
140 ;
141 ;
142 ; 系统段类型 类型编码 说明
143 ; ----------------------------------
144 ; 0 <未定义>
145 ; 1 可用286TSS
146 ; 2 LDT
147 ; 3 忙的286TSS
148 ; 4 286调用门
149 ; 5 任务门
150 ; 6 286中断门
151 ; 7 286陷阱门
152 ; 8 未定义
153 ; 9 可用386TSS
154 ; A <未定义>
155 ; B 忙的386TSS
156 ; C 386调用门
157 ; D <未定义>
158 ; E 386中断门
159 ; F 386陷阱门
160 ;
161 ; (5) G: 段界限粒度(Granularity)位。
162 ; G=0 表示界限粒度为字节;
163 ; G=1 表示界限粒度为4K 字节。
164 ; 注意,界限粒度只对段界限有效,对段基地址无效,段基地址总是以字节为单位。
165 ;
166 ; (6) D: D位是一个很特殊的位,在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常是堆栈段)的三种描述符中的意义各不相同。
167 ; ⑴ 在描述可执行段的描述符中,D位决定了指令使用的地址及操作数所默认的大小。
168 ; ① D=1表示默认情况下指令使用32位地址及32位或8位操作数,这样的代码段也称为32位代码段;
169 ; ② D=0 表示默认情况下,使用16位地址及16位或8位操作数,这样的代码段也称为16位代码段,它与80286兼容。可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大小。
170 ; ⑵ 在向下扩展数据段的描述符中,D位决定段的上部边界。
171 ; ① D=1表示段的上部界限为4G;
172 ; ② D=0表示段的上部界限为64K,这是为了与80286兼容。
173 ; ⑶ 在描述由SS寄存器寻址的段描述符中,D位决定隐式的堆栈访问指令(如PUSH和POP指令)使用何种堆栈指针寄存器。
174 ; ① D=1表示使用32位堆栈指针寄存器ESP;
175 ; ② D=0表示使用16位堆栈指针寄存器SP,这与80286兼容。
176 ;
177 ; (7) AVL: 软件可利用位。80386对该位的使用未左规定,Intel公司也保证今后开发生产的处理器只要与80386兼容,就不会对该位的使用做任何定义或规定。
178 ;
179
180
181 ;----------------------------------------------------------------------------
182 ; 在下列类型值命名中:
183 ; DA_ : Descriptor Attribute
184 ; D : 数据段
185 ; C : 代码段
186 ; S : 系统段
187 ; R : 只读
188 ; RW : 读写
189 ; A : 已访问
190 ; 其它 : 可按照字面意思理解
191 ;----------------------------------------------------------------------------
192
193 ; 描述符类型
194 DA_32 EQU 4000h ; 32 位段
195
196 DA_DPL0 EQU 00h ; DPL = 0
197 DA_DPL1 EQU 20h ; DPL = 1
198 DA_DPL2 EQU 40h ; DPL = 2
199 DA_DPL3 EQU 60h ; DPL = 3
200
201 ; 存储段描述符类型
202 DA_DR EQU 90h ; 存在的只读数据段类型值
203 DA_DRW EQU 92h ; 存在的可读写数据段属性值
204 DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值
205 DA_C EQU 98h ; 存在的只执行代码段属性值
206 DA_CR EQU 9Ah ; 存在的可执行可读代码段属性值
207 DA_CCO EQU 9Ch ; 存在的只执行一致代码段属性值
208 DA_CCOR EQU 9Eh ; 存在的可执行可读一致代码段属性值
209
210 ; 系统段描述符类型
211 DA_LDT EQU 82h ; 局部描述符表段类型值
212 DA_TaskGate EQU 85h ; 任务门类型值
213 DA_386TSS EQU 89h ; 可用 386 任务状态段类型值
214 DA_386CGate EQU 8Ch ; 386 调用门类型值
215 DA_386IGate EQU 8Eh ; 386 中断门类型值
216 DA_386TGate EQU 8Fh ; 386 陷阱门类型值
217
218
219 ; 选择子图示:
220 ; ┏━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┳━━┓
221 ; ┃ 15 ┃ 14 ┃ 13 ┃ 12 ┃ 11 ┃ 10 ┃ 9 ┃ 8 ┃ 7 ┃ 6 ┃ 5 ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ┃
222 ; ┣━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━┻━━╋━━╋━━┻━━┫
223 ; ┃ 描述符索引 ┃ TI ┃ RPL ┃
224 ; ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━┻━━━━━┛
225 ;
226 ; RPL(Requested Privilege Level): 请求特权级,用于特权检查。
227 ;
228 ; TI(Table Indicator): 引用描述符表指示位
229 ; TI=0 指示从全局描述符表GDT中读取描述符;
230 ; TI=1 指示从局部描述符表LDT中读取描述符。
231 ;
232
233 ;----------------------------------------------------------------------------
234 ; 选择子类型值说明
235 ; 其中:
236 ; SA_ : Selector Attribute
237
238 SA_RPL0 EQU 0 ; ┓
239 SA_RPL1 EQU 1 ; ┣ RPL
240 SA_RPL2 EQU 2 ; ┃
241 SA_RPL3 EQU 3 ; ┛
242
243 SA_TIG EQU 0 ; ┓TI
244 SA_TIL EQU 4 ; ┛
245 ;----------------------------------------------------------------------------
246
247
248
249 ; 宏 ------------------------------------------------------------------------------------------------------
250 ;
251 ; 描述符
252 ; usage: Descriptor Base, Limit, Attr
253 ; Base: dd
254 ; Limit: dd (low 20 bits available)
255 ; Attr: dw (lower 4 bits of higher byte are always 0)
256 %macro Descriptor 3
257 dw %2 & 0FFFFh ; 段界限1
258 dw %1 & 0FFFFh ; 段基址1
259 db (%1 >> 16) & 0FFh ; 段基址2
260 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
261 db (%1 >> 24) & 0FFh ; 段基址3
262 %endmacro ; 共 8 字节
263 ;
264 ; 门
265 ; usage: Gate Selector, Offset, DCount, Attr
266 ; Selector: dw
267 ; Offset: dd
268 ; DCount: db
269 ; Attr: db
270 %macro Gate 4
271 dw (%2 & 0FFFFh) ; 偏移1
272 dw %1 ; 选择子
273 dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h) ; 属性
274 dw ((%2 >> 16) & 0FFFFh) ; 偏移2
275 %endmacro ; 共 8 字节
276 ; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
277
这个头文件pm.inc 把注释写的非常详细,连寄存器都标注出来了!
nasm pmtest1.asm -0 pmtest1.bin
代码分析:
- 第38-45行
首先在实模式下计算出32位代码段的物理首地址
对照 段值 × 16 + 偏移量 = 物理地址
1 mov ax, cs
2 shl eax, 4 ;向左移动4位,不就是×16吗?呵呵
;到现在为止,eax就是代码段的物理首地址了,那么。。。看
3 add eax, LABEL_CODE32
;为eax (代码段首地址)加上 LABEL_CODE32偏移量,得到的不就是LABEL_CODE32的真正物理地址了吗 ?LABEL_CODE32在程序中,不就是32位代码段的首地址吗 ?
上面说过,代码中,使用的变量,或者标签 都是相对程序物理首地址的偏移量。
OK,现在我们已经知道了32位代码段的物理首地址,那么将eax放入到段描述符中就行了!
把eax里所存放的物理首地址拆开,分别放到2,3,4,7字节处 !
那么很显然,我们可以将eax寄存器中的ax先放到2,3字节处
` mov word [LABEL_CODE32 + 2],ax `
因为在偏移2个字节处,所以,首地址 + 2,才能定位到下标为2的字节开头处 而,word 告诉编译器,我要一次访问2个字节的内存 !
好,简单的搞定了,那么再看,我们现在要将eax高16字节分别放到下标为4,7字节处。
虽然eax的ax代表低16位,但是Intel并没有给高位一个名字定义,(不会是high ax,呵呵),所以,我们没有办法去访问高位。但是我们可以将高16位放到低16位中,因为这时,低16位我们已经不关心它的值了。
好,看代码
shr eax, 16
这句代码就将eax向右移动16位,低位被抛弃,高位变成了低位。呵呵。。。
现在好办了,低16位又可以分为al,和 ah,那么现在我们就将al放到4位置,ah放到7位置吧 !
- 第8行
org 07c00h
; 告诉编译器程序加载到7c00处
org 会在编译期影响到内存寻址指令的编译(编译器会把所有程序用到的段内偏移地址自动加上org 后跟的数值),而其自身并不会被编译成机器码。就是为程序中所有的引用地址(需要计算的相对地址)增加一个段内偏移值。org 指令本身并不能决定程序将要加载到内存的什么位置,它只是告诉编译器,我的程序在编译好后需要加载到xxx 地址,所以请你在编译时帮我调整好数据访问时的地址。
如果没有org指令,那么编译器计算偏移量(虽然nasm中没有offset但编译器仍会进行这个运算)是从0000开始的,如果有了org,就会在最后加上org后面跟的数字。
-
第9行
9 jmp LABEL_BEGIN
先跳转到实模式代码,也即是第30行代码处! -
第20-21行, 首先要建立GDT起始线性地址,
20 GdtPtr dw GdtLen - 1 ; GDT界限
21 dd 0 ; GDT基地址
可以看出基地址是0开始!
- 第11-26行 [SECTION .gdt] 安装描述符请看X86 汇编! 注意有个Descriptor ,比如第14行
; 段基址, 段界限 , 属性
14 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
Descriptor 是一个宏,如下代码所示:
; 宏 ------------------------------------------------------------------------------------------------------
;
; 描述符
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro ; 共 8 字节
上面也有注释说明,可以看出是dd 双字,也就是8个字节。 还记得吗 在GDT 模式章节中 https://dbb4560.github.io/2019/10/24/x86%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80-%E4%BB%8E%E5%AE%9E%E6%A8%A1%E5%BC%8F%E5%88%B0%E4%BF%9D%E6%8A%A4%E6%A8%A1%E5%BC%8F%E7%AC%94%E8%AE%B0-%E5%85%A8%E5%B1%80%E6%8F%8F%E8%BF%B0%E7%AC%A6%E8%A1%A8-GDT/ 有个GDTR 表,再次附上:
再次补充详细说明:
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro ;
首先它有三个参数:段基址1% 段界限2% 属性3%,共8个字节。
现在开始填充这个宏(8个字节初始全为0):
1.取第二个参数的第1-16位(2个字节大小)作为宏第一、二个字节
2.取第一个参数的第1-16位(2个字节大小)作为宏第三、四个字节
3.取第一个参数的第17-24位(1个字节大小)作为宏第五个字节
4.1.取第三个参数的第1-8位(1个字节大小)作为宏第六个字节
4.2取第二个参数的第17-20位(半个字节大小)
4.3取第三个参数的第12-15位(半个字节大小)并上作为宏第七个字节
5.取第一个参数的第24-32位(1个字节大小)作为宏第八个字节
填充完毕
段基址参数有效位:0xFFFFFFFF
段界限参数有效位:0x000FFFFF
属性参数有效位: 0x0000F0FF
以上F代表有效位!
这里用到宏汇编知识, %x 为第x个参数!
dw %2 & 0FFFFh ; 段界限1
就是第二个参数取低16位,也就是0-15位,正好对应段界限1!
db (%1 >> 16) & 0FFh ; 段基址2
就是第一个参数位右移动16位后,取8位,也就是16 +8 -1 = 24位
就是16-23!正好对应段基址。
所以这个宏自动创建描述符!
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 结束
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]
这次我们再次复习一次:
$是当前行的汇编地址; $$是 NASM 编译器提供的另一个标记,代表当前汇编节(段)的起始汇编地址。
从代码可以看出建立三个段: LABEL_GDT 、 LABEL_DESC_CODE32 、 LABEL_DESC_VIDEO
LABEL_GDT是空操作符,cpu要求的!
LABEL_DESC_CODE32 是32位代码段,也就是LABEL_SEG_CODE32 位置处的地址!
LABEL_DESC_VIDEO 地址是0B8000h,是不是很熟悉,就是显示器存储器的地址!
现在已经知道GDT 每一个描述符定义一个段,就是段基址和段界限!
那么CS DS 等段寄存器是如何和这些段对应起来的?
在保护模式下,32位寄存器结构如图所示:
书上写的比较详细!
在[SECTION .s32] 第80-81行代码
[SECTION .s32]; 32 位代码段. 由实模式跳入.
77 [BITS 32]
78
79 LABEL_SEG_CODE32:
80 mov ax, SelectorVideo
81 mov gs, ax ; 视频段选择子(目的)
表面看上去gs 寄存器的值变成 SelectorVideo
我们查找上下文,找到SelectorVideo 的定义:
25 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
伪指令 equ 仅仅是允许我们用符号代替具体的数值,但声明的数值并不占用空间。
表面看是对GDT基址的偏移量。 其实这个就是选择子
复习一下 TI 是描述符表指示器(Table Indicator), TI=0 时,表示描述符在 GDT 中; TI=1 时,描述符在 LDT 中。 RPL 是请求特权级!一般都是00h! 这个权限最高!
所以当TI 和 RPL 都为零时,选择子就变成描述符相对于GDT基址的偏移!
自己计算一下,每个描述符占用8个字节,那么LABEL_DESC_VIDEO 相对于就是偏移16个字节,偏移量就是0x10h,转换成二进制就是0b 00000000000_10_000B 在描述符索引上面,就变成10b 也就是2,正好对应上LABEL_DESC_VIDEO 正好在描述符编号#2!
书上也画了图,在此附上:
需要注意的是,”段:偏移“ 形式的逻辑地址( logical Address )经过段机制转换成”线性地址“,而不是物理地址!
X86 汇编有提到 页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址! 线性地址还要经页部件转换后,才是物理地址。
目前程序中,线性地址就是物理地址!书上说后面也会解释线性地址和物理地址的关系,我们后面继续看!
书上也提到LDT 也可以包含描述符!
实模式跳转保护模式
- 第28行开始 [SECTION .s16] 是实模式状态! 这个是程序开头就直接跳转过来的!
28 [SECTION .s16]
29 [BITS 16]
30 LABEL_BEGIN:
- 书上说先从第37行开始看 初始化 32 位代码段描述符!
- 第38行
xor eax, eax
eax清零! - 第39行
mov ax, cs
还记得CS:IP 因为之前org 07c00h 可以通过lst文件看出,cs的值是00000000 E9(0000) E9 那个是机器码,IP 是0x7c00! 所以CS 是0!
为了能看出当前执行状态,我安装了bochs调试器能直接看出程序状态!
- 首先在0x07c00处打断点! 输入b 0x7c00,如图:
- 输入c,让程序直接运行到断点处停止! 可以输入trace-reg on 显示寄存器状态了!需要注意的是 dump_cpu 指令已经取消了,可以使用 r fp mmx sse dreg sreg creg 组合来看寄存器的值!
- 此时我们输入r r fp mmx sse dreg sreg creg
具体的调试指令如下图,该目的仅仅是为了看出cs的值是多少!
调试的时候,你会发现ecs是 0x0009 0000,但是实模式是16位的,所以cs 还是0x0000!
- 执行到第38行之前,我们都知道cs , ds , es, ss 的值都为0!
- shl 4 逻辑左移指令 ,左移4位,仍然还是0
- 执行到第42行 ` mov word [LABEL_DESC_CODE32 + 2], ax ` 如图所示:
然后把它分成三部分,分别赋值给描述符DESC_CODE32中相应的位置!
说白了LABEL_DESC_CODE32 的段界限 和 属性已经被指定好了,只有段基址没指定,之前是默认0!
- 第55行 `lgdt [GdtPtr]
可以看出GdtPtr 和 gdtr的结构完全一样!
- 第58行 关闭中断
- 第60-68 是执行打开A20 地址线和切换保护模式(CRO)!
CR0 是 32 位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。如图所示,它的第 1 位(位 0)是保护模式允许位(Protection Enable, PE),是开启保护模式大门的门把手,如果把该位置“1”,则处理器进入保护模式,按保护模式的规则开始运行。
mov cr0, eax
之后,系统就处于保护模式下了!
但是CS 的值还是实模式下!我们还是需要把代码段的选择子装入cs!
所以第71行的jmp指令
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs; 并跳转到 Code32Selector:0 处
PS :
描述符的补充:
AVL 位 保留位,可以被系统软件使用。
一致代码段的一致意思是: 当转移的目标是一个特权级更高的一致代码段,当前的特权级会被延续下去,如果向特权级更高的非一致代码段的转移会引起常规保护错误 General-protection exception #GP ), 除非是用调用门或者任务门!
如果系统代码不访问受保护的资源或者某些类型的异常处理(比如,除法错误或溢出错误),它可以被放在一致代码段中。
要是避免低特权级的程序访问而被保护起来的系统代码则应放到非一致代码中!
call 和 jmp的转移
这里提一下段属性设置!
从代码可得知:
第15行 ` LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段 `
属性的值使用宏 DA_C + DA_32 代替,这个宏是在头文件pm.inc中的定义的!
DA_C 在第205行 ` DA_C EQU 98h ; 存在的只执行代码段属性值 `
对应的二进制是1001 1000b,对照寄存器,得知,P 位是1 表示这个段在寄存器存在,S位也是1 ,表明这个段是代码段或者数据段,type = 8 表明这个段是代码段(TYPE 字段共 4 位,用于指示描述符的子类型,或者说是类别。对于数据段来说, 这 4 位分别是 X、 E、 W、 A 位;而对于代码段来说,这 4 位则分别是 X、 C、 R、 A 位。)
DA_32 在第194行 ` DA_32 EQU 4000h ; 32 位段 `
因为是代码段,4000,也就是 0b 0100 0000 ,所以 D 位置的值是1,再次复习一下:
对于代码段,此位称做“D”位,用于指示指令中默认的偏移地址和操作数尺寸。 D=0 表示指令中的偏移地址或者操作数是 16 位的; D=1,指示 32 位的偏移地址或者操作数!
如果代码段描述符的 D 位是 0,那么,当处理器在这个段上执行时,将使用 16 位的指令指针寄存器 IP 来取指令,否则使用 32 位的 EIP。
堆栈段来说,该位被叫做“B”位,用于在进行隐式的堆栈操作时,是使用 SP 寄存器还是ESP 寄存器。 B 位的值也决定了堆栈的上部边界。如果 B=0,那么堆栈段的上部边界(也就是 SP 寄存器的最大值)为 0xFFFF;如果 B=1,那么堆栈段的上部边界(也就是 ESP 寄存器的最大值)为 0xFFFFFFFF!
复习结束!
由此推论,VIDEO 段是存在的可读写数据数据段!