前几节课我们演示了从实模式进入到保护模式,那么从保护模式返回到实模式具体怎么操作呢?
先将上一节的程序列出:
1 %include "inc.asm" 2 3 org 0x9000 4 5 jmp CODE16_SEGMENT 6 7 [section .gdt] 8 ; GDT definition 9 ; 段基址, 段界限, 段属性 10 GDT_ENTRY : Descriptor 0, 0, 0 11 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 12 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 13 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 14 STACK_DESC : Descriptor 0, TopOfStackInit, DA_DRW + DA_32 15 ; GDT end 16 17 GdtLen equ $ - GDT_ENTRY 18 19 GdtPtr: 20 dw GdtLen - 1 21 dd 0 22 23 24 ; GDT Selector 25 26 Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 27 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 28 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 29 StackSelector equ (0x0004 << 3) + SA_TIG + SA_RPL0 30 31 ; end of [section .gdt] 32 33 TopOfStackInit equ 0x7c00 34 35 [section .dat] 36 [bits 32] 37 DATA32_SEGMENT: 38 DTOS db "D.T.OS!", 0 39 DTOS_OFFSET equ DTOS - $$ 40 HELLO_WORLD db "Hello World!", 0 41 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ 42 43 Data32SegLen equ $ - DATA32_SEGMENT 44 45 [section .s16] 46 [bits 16] 47 CODE16_SEGMENT: 48 mov ax, cs 49 mov ds, ax 50 mov es, ax 51 mov ss, ax 52 mov sp, TopOfStackInit 53 54 ; initialize GDT for 32 bits code segment 55 mov esi, CODE32_SEGMENT 56 mov edi, CODE32_DESC 57 58 call InitDescItem 59 60 mov esi, DATA32_SEGMENT 61 mov edi, DATA32_DESC 62 63 call InitDescItem 64 65 ; initialize GDT pointer struct 66 mov eax, 0 67 mov ax, ds 68 shl eax, 4 69 add eax, GDT_ENTRY 70 mov dword [GdtPtr + 2], eax 71 72 ; 1. load GDT 73 lgdt [GdtPtr] 74 75 ; 2. close interrupt 76 cli 77 78 ; 3. open A20 79 in al, 0x92 80 or al, 00000010b 81 out 0x92, al 82 83 ; 4. enter protect mode 84 mov eax, cr0 85 or eax, 0x01 86 mov cr0, eax 87 88 ; 5. jump to 32 bits code 89 jmp dword Code32Selector : 0 90 91 92 ; esi --> code segment label 93 ; edi --> descriptor label 94 InitDescItem: 95 push eax 96 97 mov eax, 0 98 mov ax, cs 99 shl eax, 4 100 add eax, esi 101 mov word [edi + 2], ax 102 shr eax, 16 103 mov byte [edi + 4], al 104 mov byte [edi + 7], ah 105 106 pop eax 107 108 ret 109 110 111 [section .s32] 112 [bits 32] 113 CODE32_SEGMENT: 114 mov ax, VideoSelector 115 mov gs, ax 116 117 mov ax, StackSelector 118 mov ss, ax 119 120 mov ax, Data32Selector 121 mov ds, ax 122 123 mov ebp, DTOS_OFFSET 124 mov bx, 0x0C 125 mov dh, 12 126 mov dl, 33 127 128 call PrintString 129 130 mov ebp, HELLO_WORLD_OFFSET 131 mov bx, 0x0C 132 mov dh, 13 133 mov dl, 30 134 135 call PrintString 136 137 jmp $ 138 139 ; ds:ebp --> string address 140 ; bx --> attribute 141 ; dx --> dh : row, dl : col 142 PrintString: 143 push ebp 144 push eax 145 push edi 146 push cx 147 push dx 148 149 print: 150 mov cl, [ds:ebp] 151 cmp cl, 0 152 je end 153 mov eax, 80 154 mul dh 155 add al, dl 156 shl eax, 1 157 mov edi, eax 158 mov ah, bl 159 mov al, cl 160 mov [gs:edi], ax 161 inc ebp 162 inc dl 163 jmp print 164 165 end: 166 pop dx 167 pop cx 168 pop edi 169 pop eax 170 pop ebp 171 172 ret 173 174 Code32SegLen equ $ - CODE32_SEGMENT
上一节中,我们跳到32位保护模式后,并没有设置栈顶指针esp,但是程序依然可以正常运行,这时怎么回事呢?原因是我们在第52行设置了栈顶指针,而我们的程序中,16位的实模式和32位的保护模式使用的栈是一样的,因此,无需重新设置程序也可以正常运行。第14行的段描述符描述了32位保护模式下的栈的信息,在保护模式下即使我们将这个段的选择子,赋值给ss,那么由于段基址是0,得到最终的栈顶指针依然是 段基址+esp=0+esp,所以不给ss赋值和给ss赋值的结果是一样的。如果在32位保护时使用的栈和16位实模式使用的栈不一样的话,就不能这样操作了,而必须在进入32位保护模式后设置ss段寄存和esp栈顶指针。
保护模式下的栈段,我们一般要进行以下步骤的设置:
1、指定一段空间,并为其定义段描述符
2、根据段描述表中的位置定义段选择子
3、初始化栈段寄存器(ss <- StackSelector)
4、初始化栈顶指针(esp <- TopOfStack )
下面定义32位保护模式下的专用栈:
1 %include "inc.asm" 2 3 org 0x9000 4 5 jmp CODE16_SEGMENT 6 7 [section .gdt] 8 ; GDT definition 9 ; 段基址, 段界限, 段属性 10 GDT_ENTRY : Descriptor 0, 0, 0 11 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 12 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 13 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 14 STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32 15 ; GDT end 16 17 GdtLen equ $ - GDT_ENTRY 18 19 GdtPtr: 20 dw GdtLen - 1 21 dd 0 22 23 24 ; GDT Selector 25 26 Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 27 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 28 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 29 Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 30 31 ; end of [section .gdt] 32 33 TopOfStack16 equ 0x7c00 34 35 [section .dat] 36 [bits 32] 37 DATA32_SEGMENT: 38 DTOS db "D.T.OS!", 0 39 DTOS_OFFSET equ DTOS - $$ 40 HELLO_WORLD db "Hello World!", 0 41 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ 42 43 Data32SegLen equ $ - DATA32_SEGMENT 44 45 [section .s16] 46 [bits 16] 47 CODE16_SEGMENT: 48 mov ax, cs 49 mov ds, ax 50 mov es, ax 51 mov ss, ax 52 mov sp, TopOfStack16 53 54 ; initialize GDT for 32 bits code segment 55 mov esi, CODE32_SEGMENT 56 mov edi, CODE32_DESC 57 58 call InitDescItem 59 60 mov esi, DATA32_SEGMENT 61 mov edi, DATA32_DESC 62 63 call InitDescItem 64 65 mov esi, DATA32_SEGMENT 66 mov edi, STACK32_DESC 67 68 call InitDescItem 69 70 ; initialize GDT pointer struct 71 mov eax, 0 72 mov ax, ds 73 shl eax, 4 74 add eax, GDT_ENTRY 75 mov dword [GdtPtr + 2], eax 76 77 ; 1. load GDT 78 lgdt [GdtPtr] 79 80 ; 2. close interrupt 81 cli 82 83 ; 3. open A20 84 in al, 0x92 85 or al, 00000010b 86 out 0x92, al 87 88 ; 4. enter protect mode 89 mov eax, cr0 90 or eax, 0x01 91 mov cr0, eax 92 93 ; 5. jump to 32 bits code 94 jmp dword Code32Selector : 0 95 96 97 ; esi --> code segment label 98 ; edi --> descriptor label 99 InitDescItem: 100 push eax 101 102 mov eax, 0 103 mov ax, cs 104 shl eax, 4 105 add eax, esi 106 mov word [edi + 2], ax 107 shr eax, 16 108 mov byte [edi + 4], al 109 mov byte [edi + 7], ah 110 111 pop eax 112 113 ret 114 115 116 [section .s32] 117 [bits 32] 118 CODE32_SEGMENT: 119 mov ax, VideoSelector 120 mov gs, ax 121 122 mov ax, Stack32Selector 123 mov ss, ax 124 125 mov eax, TopOfStack32 126 mov esp, eax 127 128 mov ax, Data32Selector 129 mov ds, ax 130 131 mov ebp, DTOS_OFFSET 132 mov bx, 0x0C 133 mov dh, 12 134 mov dl, 33 135 136 call PrintString 137 138 mov ebp, HELLO_WORLD_OFFSET 139 mov bx, 0x0C 140 mov dh, 13 141 mov dl, 30 142 143 call PrintString 144 145 jmp $ 146 147 ; ds:ebp --> string address 148 ; bx --> attribute 149 ; dx --> dh : row, dl : col 150 PrintString: 151 push ebp 152 push eax 153 push edi 154 push cx 155 push dx 156 157 print: 158 mov cl, [ds:ebp] 159 cmp cl, 0 160 je end 161 mov eax, 80 162 mul dh 163 add al, dl 164 shl eax, 1 165 mov edi, eax 166 mov ah, bl 167 mov al, cl 168 mov [gs:edi], ax 169 inc ebp 170 inc dl 171 jmp print 172 173 end: 174 pop dx 175 pop cx 176 pop edi 177 pop eax 178 pop ebp 179 180 ret 181 182 Code32SegLen equ $ - CODE32_SEGMENT 183 184 [section .gs] 185 [bits 32] 186 STACK32_SEGMENT: 187 times 1024 * 4 db 0 188 189 Stack32SegLen equ $ - STACK32_SEGMENT 190 TopOfStack32 equ Stack32SegLen - 1
184-190行我们重新定义了32位保护模式下的栈段。并在14行和19行为其填充了段描述符表项和段选择子。我们在94行打上断点,看看程序执行到这里时栈顶指针寄存器的值是多少。启动bochs开始运行,结果如下:
可以看到这时的esp是0x7c00。
122-126行,我们在32位保护模式中设置了栈的段基址和栈顶指针。继续单步执行程序,如下:
图中可以看出,我们将段选择子赋值给了ss,将栈的段界限赋值给了esp,因为栈是向下生长的,所以就应该将段界限赋值给esp。
继续执行程序,最终结果如下:
从保护模式返回时模式:
8086中的一个神秘限制:
无法直接从32位代码段回到实模式
只能从16位代码段间接返回实模式
在返回前必须用合适的选择子对段寄存器赋值
可以从16位实模式代码段跳到32位保护模式代码段,但是返回的话不能直接进行。
返回流程:先从32位保护模式的代码段返回16位保护模式的代码段(保护模式下也可以定义16位的代码段),然后从16位保护模式代码段跳到16位实模式代码段。
16位保护模式的代码段在这里作为一个中间过渡过程,我们在这个段只干一件事,就是用合适的段选择子对段寄存器进行赋值。除此之外不做其他的逻辑上的操作。
在操作之前,我们先介绍一下处理器中的设计:
80286之后的处理器都提供兼容8086的实模式
然而,绝大多数时候处理器都运行于保护模式
因此,保护模式的运行效率至关重要
那么,处理器如何高效的访问内存中的段描述符呢?
运行于保护模式时,性能瓶颈在于:段描述符定义在内存中,如果每次都要访问内存,效率会比较低。如何快速高效的访问内存中的段描述符呢?解决方案如下:
使用高速缓冲存储器
当使用选择子设置段寄存器时,会触发处理器的内部操作:
根据选择子访问内存中的段描述符
将段描述符加载到段寄存器的高速缓冲存储器
需要段描述符信息时,直接从高速缓冲器中获得
处于实模式时也会用到这个段寄存器高速缓冲存储器。会用到其中的段基地址和段界限。
注意事项:
在实模式 下,高速缓冲存储器仍然发挥着作用
段基址是32位,其值是相应段寄存器的值乘以16
实模式下段基址有效位为20位(高速缓存中的32段基址足以容纳),段界限固定为0xFFFF(64K)
段属性的值不可设置,只能继续沿用保护方式下所设置的值
高速缓冲存储器不可以直接访问设置值。只能通过特殊的方法:
通过加载一个合适的描述符选择子到有关段寄存器,以使得对应的段描述符高速缓冲寄存器中含有合适的段界限和段属性。
跳到16位实模式的具体流程:
32位保护模式代码段 -> 16位保护模式代码段(刷新段寄存器,退出保护模式) -> 16位实模式代码段(设置段寄存器的值,关闭A20地址线,启用硬件中断)
汇编小知识:深入理解jmp指令
段内跳转: 指令是三个字节,操作码(E9)为1个字节(低地址),操作数是两个字节(高地址)(也就是段内偏移地址)。
段间跳转:指令时5个字节,操作码(EA)为1个字节(低地址),操作数是四个字节(偏移地址、段基址)(高地址)
段间跳转时,我们可以修改指令中的偏移地址和段基址就可以跳转到另一个期望的段中去了。修改指令是运行时修改内存中的指令,而不是在源程序中修改。
从保护模式返回到实模式的程序如下:
1 %include "inc.asm" 2 3 org 0x9000 4 5 jmp ENTRY_SEGMENT 6 7 [section .gdt] 8 ; GDT definition 9 ; 段基址, 段界限, 段属性 10 GDT_ENTRY : Descriptor 0, 0, 0 11 CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 12 VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 13 DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 14 STACK32_DESC : Descriptor 0, TopOfStack32, DA_DRW + DA_32 15 CODE16_DESC : Descriptor 0, 0xFFFF, DA_C 16 UPDATE_DESC : Descriptor 0, 0xFFFF, DA_DRW 17 ; GDT end 18 19 GdtLen equ $ - GDT_ENTRY 20 21 GdtPtr: 22 dw GdtLen - 1 23 dd 0 24 25 26 ; GDT Selector 27 28 Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 29 VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 30 Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 31 Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 32 Code16Selector equ (0x0005 << 3) + SA_TIG + SA_RPL0 33 UpdateSelector equ (0x0006 << 3) + SA_TIG + SA_RPL0 34 ; end of [section .gdt] 35 36 TopOfStack16 equ 0x7c00 37 38 [section .dat] 39 [bits 32] 40 DATA32_SEGMENT: 41 DTOS db "D.T.OS!", 0 42 DTOS_OFFSET equ DTOS - $$ 43 HELLO_WORLD db "Hello World!", 0 44 HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ 45 46 Data32SegLen equ $ - DATA32_SEGMENT 47 48 [section .s16] 49 [bits 16] 50 ENTRY_SEGMENT: 51 mov ax, cs 52 mov ds, ax 53 mov es, ax 54 mov ss, ax 55 mov sp, TopOfStack16 56 57 mov [BACK_TO_REAL_MODE + 3], ax 58 59 ; initialize GDT for 32 bits code segment 60 mov esi, CODE32_SEGMENT 61 mov edi, CODE32_DESC 62 63 call InitDescItem 64 65 mov esi, DATA32_SEGMENT 66 mov edi, DATA32_DESC 67 68 call InitDescItem 69 70 mov esi, DATA32_SEGMENT 71 mov edi, STACK32_DESC 72 73 call InitDescItem 74 75 mov esi, CODE16_SEGMENT 76 mov edi, CODE16_DESC 77 78 call InitDescItem 79 80 ; initialize GDT pointer struct 81 mov eax, 0 82 mov ax, ds 83 shl eax, 4 84 add eax, GDT_ENTRY 85 mov dword [GdtPtr + 2], eax 86 87 ; 1. load GDT 88 lgdt [GdtPtr] 89 90 ; 2. close interrupt 91 cli 92 93 ; 3. open A20 94 in al, 0x92 95 or al, 00000010b 96 out 0x92, al 97 98 ; 4. enter protect mode 99 mov eax, cr0 100 or eax, 0x01 101 mov cr0, eax 102 103 ; 5. jump to 32 bits code 104 jmp dword Code32Selector : 0 105 106 BACK_ENTRY_SEGMENT: 107 mov ax, cs 108 mov ds, ax 109 mov es, ax 110 mov ss, ax 111 mov sp, TopOfStack16 112 113 in al, 0x92 114 and al, 11111101b 115 out 0x92, al 116 117 sti 118 119 mov bp, HELLO_WORLD 120 mov cx, 12 121 mov dx, 0 122 mov ax, 0x1301 123 mov bx, 0x0007 124 int 0x10 125 126 jmp $ 127 128 ; esi --> code segment label 129 ; edi --> descriptor label 130 InitDescItem: 131 push eax 132 133 mov eax, 0 134 mov ax, cs 135 shl eax, 4 136 add eax, esi 137 mov word [edi + 2], ax 138 shr eax, 16 139 mov byte [edi + 4], al 140 mov byte [edi + 7], ah 141 142 pop eax 143 144 ret 145 146 147 [section .16] 148 [bits 16] 149 CODE16_SEGMENT: 150 mov ax, UpdateSelector 151 mov ds, ax 152 mov es, ax 153 mov fs, ax 154 mov gs, ax 155 mov ss, ax 156 157 mov eax, cr0 158 and al, 11111110b 159 mov cr0, eax 160 161 BACK_TO_REAL_MODE: 162 jmp 0 : BACK_ENTRY_SEGMENT 163 164 Code16SegLen equ $ - CODE16_SEGMENT 165 166 167 [section .s32] 168 [bits 32] 169 CODE32_SEGMENT: 170 mov ax, VideoSelector 171 mov gs, ax 172 173 mov ax, Stack32Selector 174 mov ss, ax 175 176 mov eax, TopOfStack32 177 mov esp, eax 178 179 mov ax, Data32Selector 180 mov ds, ax 181 182 mov ebp, DTOS_OFFSET 183 mov bx, 0x0C 184 mov dh, 12 185 mov dl, 33 186 187 call PrintString 188 189 mov ebp, HELLO_WORLD_OFFSET 190 mov bx, 0x0C 191 mov dh, 13 192 mov dl, 30 193 194 call PrintString 195 196 jmp Code16Selector : 0 197 198 ; ds:ebp --> string address 199 ; bx --> attribute 200 ; dx --> dh : row, dl : col 201 PrintString: 202 push ebp 203 push eax 204 push edi 205 push cx 206 push dx 207 208 print: 209 mov cl, [ds:ebp] 210 cmp cl, 0 211 je end 212 mov eax, 80 213 mul dh 214 add al, dl 215 shl eax, 1 216 mov edi, eax 217 mov ah, bl 218 mov al, cl 219 mov [gs:edi], ax 220 inc ebp 221 inc dl 222 jmp print 223 224 end: 225 pop dx 226 pop cx 227 pop edi 228 pop eax 229 pop ebp 230 231 ret 232 233 Code32SegLen equ $ - CODE32_SEGMENT 234 235 [section .gs] 236 [bits 32] 237 STACK32_SEGMENT: 238 times 1014 * 4 db 0 239 240 Stack32SegLen equ $ - STACK32_SEGMENT 241 TopOfStack32 equ Stack32SegLen - 1
147-164行定义了16位的保护模式代码,106-126行定义了另一个16位实模式代码段。15、16行定义了新的段描述符,15行的段描述符是描述16位保护模式的代码段的。16行的段描述符是描述16位实模式的代码段的。75-78行我们初始化了16位保护模式下的段描述符。程序从32位保护模式的196行跳转到16位保护模式的代码段,然后将16位实模式代码段的段选择子分别赋给ds、es、fs、gs、ss段寄存器(赋值的意义就是刷新对应的段描述符对应的高速缓冲存储器),赋值的同时,处理器的内部机制会读取内存,并初始化段寄存器高速缓存。这样这些寄存器高速缓存中保存的就是16位实模式代码段的信息了。然后,157-159行使处理器进入实模式。当执行162行跳转时,处理器已经处于16位实模式。注意,在16位保护模式的代码中,我们没有给cs赋值,因为这时程序还处于16位保护模式,如果这时候我们给cs赋值16位实模式,那么程序会出错,因为这时代码还在16位保护模式执行中。
162行的跳转我们要跳到16位的实模式代码段处,这是一个段间跳转,因此使用jmp 0 : BACK_ENTRY_SEGMENT(按照16位实模式进行跳转,偏移地址是16位的,寻址范围是64kb),这里的0我们应该填入cs的值,这个值是程序执行到第50行处cs的值,这个cs的值是代表16位实模式代码的基地址,因此我们在57行加了mov [BACK_TO_REAL_MODE + 3], ax,标签BACK_TO_REAL_MODE是在161行定义的,这句代码的意思是,我们直接修改内存中的指令,使得跳转指令中的基地址变为cs的值。这样就实现了运行时动态的修改指令。因此,当第162行我们执行跳转时,jmp 0 : BACK_ENTRY_SEGMENT指令完成跳转的同时,也会改变cs的值和ip的值,程序可以正确的跳转到16位实模式 BACK_ENTRY_SEGMENT代码处,在BACK_ENTRY_SEGMENT处,cs已经处于16位实模式下正确的值。程序执行到第162行时,cs段寄存器对应的高速缓存中存储的还是16位保护模式的信息,也就是在第15行的16位保护模式代码段描述符中如果我们填入的段界限是Code16SegLen - 1,那么执行到162行时,cs高速缓存中的段界限就是这个值,而这个值比较小(因为16位保护模式的代码我们写的比较小),因此,执行第162行时,会发生越界访问异常(虽然这时候是处于实模式,但是跳转时段界限依旧起作用),因为BACK_ENTRY_SEGMENT一般是大于Code16SegLen - 1那个值的,因此,15行中的段界限我们要按16位实模式的段界限64k来填,也就是0xFFFF。
最终进入到16位实模式代码段BACK_ENTRY_SEGMENT处,在这个段中,cs的值已经是16位实模式代码段的地址了,将cs的值一次赋值给其他的段寄存器,在这里已经进入了16位实模式代码段,对段寄存器的赋值只会改变相应高速缓冲存储器中的段基址,段界限和段属性沿用UPDATE_DESC中定义的值。然后设置栈,然后打开A20地址线,然后开中断,最后执行一个打印。
执行结果如下: