x86

从计算机的历史谈起

  • 远古时期的程序开发 : 直接操作物理内存
  • CPU 指令的操作数直接使用实地址( 实际内存地址 )
  • 程序员拥有绝对的权利( 利用 CPU 指哪打哪 )

绝对的权利带来的问题

  • 难以重定位
    程序每次都需要同样地址的内存执行,某一程序能在A电脑上运行,到了B电脑上就不一定能运行了(B电脑内存不一定够)
  • 给多道程序设计带来障碍
    不管内存多大,但凡一个字节被其它程序占用都无法执行,对于A程序和外设备交互,不再需要处理机了,这个时候处理机去调度B程序,如果A程序和B程序有重合那么问题就很大了

CPU 历史的里程碑- 8086

  • 地址线宽度为 20 位 , 可访问 1M(2^20)内存空间
  • 引入 [ 段地址 : 偏移地址 ] 的内存访问方式
    8086的段寄存器和通用寄存器为16位
    单个寄存器寻址最多访问64K 的内存空间
    需要两个寄存器配合 , 完成所有内存空间的访问

这个时候重定位就非常简单了,只需要写死偏移地址,如果有冲突就修改段基址即可

深入解析 [ 段地址 : 偏移地址 ]
- 硬件所做的工作
段地址左移4 位 , 构成20 位的基地址( 起始地址 )
基地址 + 偏移地址 = 实地址
- 对于开发者的意义
更有效的划分内存的功能( 数据段 , 代码段 , 等 )
当出现程序地址冲突时 , 通过修改段地址解决冲突

1
2
mov ax, [0x1234]	; 实地址 : (ds << 4) + 0x1234 如果不指定段地址,硬件会选择默认段地址,默认段地址存储在寄存器ds
mov ax, [es:0xl234] ; 实地址 : (es << 4) + 0x1234

一个问题:[ 段地址 : 偏移地址 ] 能访问的最大地址为 0xFFFF : 0xFFFF即 : 10FFEF; 超过了(0xFFFFF)1MB 的空间 , CPU 如何处理 ?

8086 中的高端地址区( High Memory Area )

1
2
3
4
5
0XFFFF : 0XFFFF
= 0XFFFF0 + 0XFFFF
= 0XFFFF0 + (0XF + 0XFFF0)
= (0XFFFF0 + 0xF) + 0XFFF0
= 0xFFFFF + 0xFFF0

8086的处理方式

由于 8086 只有 20 位地址线 , 因此最高位被丟弃( 溢出 ) !

1
2
3
4
5
0XFFFF : 0XFFFF
= 10FFEF #16进制
= 100001111111111101111 #2进制 总共21位,超出一位
-> 00001111111111101111 #2进制 舍弃最高位变为20位 内存回卷
=0XFFEF

再谈 8086 历史

  • 8086 在当时是非常成功的一款产品
  • 因此 , 拥有一大批的开发者和应用程序
  • 各种基于 8086 程序设计的技术得到了发展
  • 不幸的是 , 各种奇技淫巧也应运而生

8086 时期应用程序中的问题

  1. 1MB 内存完全不够用( 内存在任何时期都不够用 )
  2. 开发者在程序中大量使用内存回卷技术( HMA 地址被使用
  3. 应用程序之间没有界限,相互之间随意干扰
    • A 程序可以随意访问 B 程序中的数据
    • C 程序可以修改系统调度程序的指令

80286的登场

  • 8086已经有那么多应用程序了 , 所以必须兼容再兼容
  • 加大内存容量 , 増加地址线数量( 24位 )
  • [段地址:偏移地址] 的方式可以强化一下
    为每个段提供更多属性( 如:范围,特权级,等 )
    为每个段的定义提供固定方式

80286的兼容性

  • 默认情况下完全兼容 8086 的运行方式( 实模式
  • 默认可直接访问1MB 的内存空间
  • 通过保护模式访问1MB+ 的内存空间 (地址线24位)

保护模式

image-20220508175754093

初识保护模式

  • 每一段内存的拥有一个属性定义 描述符,类似于变量( 描述符 Descriptor )
  • 所有段的属性定义构成一张表,类似于数组描述符表 Descriptor Table )
  • 段寄存器保存的是属性定义在表中的索引,访问数组需要“下标”( 选择子 Selector )

描述符的内存结构

image-20220508175925665

段基址为什么放三个段然后拼接起来?这是个历史原因,硬件会帮我们拼接起来

描述符表

image-20220508183358674

选择子

image-20220508183446585

进入保护模式的方法

  1. 定义描述符表
  2. 打开 A20 地址线
    80286默认情况下兼容8086运行方式,只使用0-19这20根地址线,访问超过1MB内存时,会自动回卷,
    但是打开了A20地址线就可以使用更多地址线,访问的地址范围更大了
  3. 加载描述表
    将描述表的地址放入一个特殊寄存器
  4. 通知 CPU 进入保护模式
    将一个特殊寄存器的值0变为1

80286 的光荣退场

  • 历史意义
    引入了保护模式 , 为现代操作系统和应用程序奠定了基础
  • 奇葩设计
    段寄存器为 24 位 , 通用寄存器为 16 位( 不伦不类 ),实模式下只有16位可以使用
        理论上 , 段寄存器中的数值可以直接作为段基址
        16 位通用寄存器最多**访问 64K 的内存**
        为了访问16M 内存,**必须不停切换段基址**  
    

80386的登场( 计算机新时期的标志)

  • 32 位地址总线( 可支持 4G 的内存空间 )
  • 段寄存器和通用寄存器都为 32 位
    任何一个寄存器都能访问到内存的任意角落
    开启了平坦内存模式的新时代
    段基址为 0 , 使用通用寄存器访问4G 内存空间

新时期的内存使用方式

  1. 实模式:兼容 8086的内存使用方式( 指哪打哪 )
  2. 分段模式:通过[段地址:偏移地址]的方式将内存从功能上分段(数据段,代码段 )
  3. 平坦模式:所有内存就是一个段[0:32位偏移地址]

一个问题:x86指的究竟是什么处理器?
8086,80286,80386…

GDT表初始化

GDT表项

段属性定义:

image-20220508201154545

选择子属性定义:

image-20220508201346654

保护模式的段定义
借助宏来实现

image-20220508201835202

GDT的定义

这样借助宏只需要传入三个参数即可

image-20220508202140787

汇编小贴士

  • section 关键字用于”逻辑的“定义一段代码集合
  • section 定义的代码段不同于 [ 段地址 : 偏移地址 ] 的代码段(内存中可执行的代码)
    section定义的代码段仅限于源码中的代码段( 代码节 ),未经过编译以文本形式存在的代码段,只存在于源码,不会加载到物理内存中
    [ 段地址 : 偏移地址]的代码段指内存中的代码段
    image-20220508202550629

汇编程序中以.开头的名称并不是指令的助记符,不会被翻译成机器指令,而是给汇编器一些特殊指示,称为汇编指示(Assembler Directive)或伪操作(Pseudo-operation),由于它不是真正的指令所以加个“伪”字。.section指示把代码划分成若干个段(Section),程序被操作系统加载执行时,每个段被加载到不同的地址,操作系统对不同的页面设置不同的读、写、执行权限。.data段保存程序的数据,是可读可写的,相当于C程序的全局变量。

.s1代码段逻辑上是连续的,代码节之间存在内存对齐,必须四字节对齐,因此.s1补了两个字节的0

  • [bits16] 用于指示编译器将代码按照16 位方式进行编译
  • [bits32]用于指示编译器将代码按照32 位方式进行编译

初始化GDT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
; inc.asm
; Segment Attribute
DA_32 equ 0x4000
DA_DR equ 0x90
DA_DRW equ 0x92
DA_DRWA equ 0x93
DA_C equ 0x98
DA_CR equ 0x9A
DA_CCO equ 0x9C
DA_CCOR equ 0x9E

; Selector Attribute
SA_RPL0 equ 0
SA_RPL1 equ 1
SA_RPL2 equ 2
SA_RPL3 equ 3

SA_TIG equ 0
SA_TIL equ 4

; 描述符
; 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 & 0xFFFF ; 段界限1
dw %1 & 0xFFFF ; 段基址1
db (%1 >> 16) & 0xFF ; 段基址2
dw ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0xFF ; 段基址3
%endmacro ; 共 8 字节


这里实模式实现的功能非常的简单,将32位只执行代码段的GDT表项初始好了,然后jmp强制跳转到了32位保护模式下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
%include "inc.asm"

org 0x9000

jmp CODE16_SEGMENT

[section .gdt]
; GDT definition
; 段基址, 段界限, 段属性
GDT_ENTRY : Descriptor 0, 0, 0 ;段描述表第一个不使用,仅作为占位符
CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 ;保护模式下的32位只执行代码段
; GDT end

GdtLen equ $ - GDT_ENTRY ;全局描述符表的长度

GdtPtr:
dw GdtLen - 1 ;结构体界限
dd 0 ;GDT的起始地址,4字节


; GDT Selector

Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 ;选择子 索引1 + 属性GDT + 特权级0

; end of [section .gdt]

;实模式16位代码段
[section .s16]
[bits 16]
CODE16_SEGMENT:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00

; initialize GDT for 32 bits code segment 获取CODE32的真实段基地址
mov eax, 0 ;eax寄存器值清0
mov ax, cs ;eax = cs
shl eax, 4 ;eax = cs<<4
add eax, CODE32_SEGMENT ;得到32位代码段的真实段基地址[cs << 4 + CODE32_SEGMENT]

;对段描述符的段基址部分进行初始化设置
mov word [CODE32_DESC + 2], ax ;32位代码段的段基地址低16位放到偏移2处
shr eax, 16 ;低16位被移出去了,ax变为了高16位
mov byte [CODE32_DESC + 4], al ;将段基地址17-23位放到偏移4处
mov byte [CODE32_DESC + 7], ah ;将段基地址24-31位放到偏移7处

; initialize GDT pointer struct
mov eax, 0 ;eax寄存器值清0
mov ax, ds ;eax = cs
shl eax, 4 ;eax = cs<<4
add eax, GDT_ENTRY ;得到全局GDT段表的起始地址
mov dword [GdtPtr + 2], eax ;放入到结构体GdtPtr起始地址处

; 1. load GDT
lgdt [GdtPtr]

; 2. close interrupt
cli

; 3. open A20
in al, 0x92
or al, 00000010b
out 0x92, al

; 4. enter protect mode
mov eax, cr0
or eax, 0x01
mov cr0, eax

; 5. jump to 32 bits code
jmp dword Code32Selector : 0 ;这里就已经使用的是选择子

[section .s32]
[bits 32]
CODE32_SEGMENT:
mov eax, 0
jmp CODE32_SEGMENT

Code32SegLen equ $ - CODE32_SEGMENT

注意事项:

  • 段描述表中的第 0 个描述符不使用( 仅用于占位 )
  • 代码中必须显示的指明16 位代码段和 32 位代码段
  • 必须使用jmp指令从 16 位代码段跳转到 32 位代码段

为什么不直接使用标签定义描述符中的段基地址,换种说法为什么CODE32_DESC在定义的时候设置段基地址为0,然后实模式下才去初始化好端基地址 ?

  • NASM 将汇编文件当成一个独立的代码段编译
  • 汇编代码中的标签( Label ) 代表的是段内偏移地址(逻辑地址)
  • 实模式下需要配合段寄存器中的值才能计算得到标签的真实物理地址

为什么16 位代码段到 32 位代码段必须无条件换种说法32位代码段和16位代码段相连,为什么需要跳转 ?

  • 处理器为了提高效率将当前指令和后缴旨令预取到流水线
  • 因此 , 可能同时预取的指令中既有16 位代码又有 32 位代码
  • 为了避免将 32 位代码用16 位的方式运行 , 需要刷新流水线
  • 无条件jmp 能强制刷新流水

小结

  • 80386 处理器是计算机发展史上的里程碑
  • 32 位的寄存器和地址总线能够直接访问 4G 内存的任意角落
  • 需要在16 位实模式中对 GDT 中的数据进行初始化
  • 代码中需要为 **GDT 定义一个标识数据结构( GdtPtr )**,用于加载段描述符表
  • 需要使用jmp 指令从 16 位代码跳转到 32 位代码

显存段

为了显示数据 , 必须存在两大硬件 : 显卡 + 显示器

  • 显卡
    为显示器提供需要显示的数据
    控制显示器的模式和状态
  • 显示器
    将目标数据以可见的方式呈现在屏幕上

显存的概念和意义

  • 显卡拥有自己内部的数据存储器 , 简称显存
  • 显存在本质上和普通内存无差别 , 用于存储目标数据
  • 操作显存中的数据将导致显示器上内容的改变

显卡的工作模式 : 文本模式 & 图形模式

  • 在不同的模式下 , 显卡对显存内容的解释是不同的
  • 可以使用专属指令或 int 0x10 中断改变显卡工作模式
  • 文本模式下 :
    显存的地址范围映射为 : [ 0XB8000,0xBFFFF ]
    屏幕可以显示 25 行,每行 80 个字符(1个字符占用2字节,第一个字节存字符,第二个字节存属性,恰好可以用一个ax寄存器存下)

image-20220510100350498

PrintString

1
2
3
4
5
6
7
8
CODE32_SEGMENT:
mov ax, VideoSelector
mov gs, ax ;将显存段的选择子放入gs寄存器
mov edi, (80 * 12 + 37) * 2 ;每个字符占用两个单元字符和属性
mov ah, 0x0C ;黑底红字方式显示
mov al, 'P' ;显示P字符
mov [gs:edi], ax ;放到显存段中
jmp CODE32_SEGMENT

image-20220510102036694

在保护模式下 , 打印指定内存中的字符串

  • 定义全局堆栈段( .gs ) ,用于保护模式下的函数调用
  • 定义全局数据段( .dat ) , 用于定义只读数据 ( D.T.OS! )
  • 利用对显存段的操作定义字符串打印函数( Printstring )

image-20220510112728498

32 位保护模式下的乘法操作(mul)

  • 被乘数放到 AX 寄存器
  • 乘数放到通用寄存器或内存单元( 16位 )
  • 相乘的结果放到 EAX 寄存器中

再 论 $ 和 $$

  • $表示当前行相对于代码起始位置处的偏移量
  • $$表示当前代码节( section )的起始位置
1
2
3
4
5
[section .dat]
[bits 32]
FYOS db "FYOS!",0
; 计算FYOS在当前代码节的偏移位置
FYOS_OFFSET equ FYOS -$$
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
; ds:ebp    --> string address
; bx --> attribute
; dx --> dh : row, dl : col
PrintString:
push ebp
push eax
push edi
push cx
push dx
print:
mov cl, [ds : ebp] ;cl 取得当前字符
cmp cl, 0
je end
mov eax, 80
mul dh ;80*row -->eax
add al, dl
shl eax, 1
mov edi, eax ;mov edi, (80 * 12 + 37) * 2
mov ah, bl ;字符属性
mov al, cl ;打印字符放入al
mov [gs:edi], ax ;放入显存
inc ebp ;下一个的位置
inc dl ;显存中下一个字符的打印位置

jmp print

全局栈段

32位代码段下调用一个函数,我们也需给32位代码段也应设置一个栈空间

1
2
3
4
5
6
7
8
9
STACK_DESC      :     Descriptor    0,      TopOfStackInit,    DA_DRW + DA_32   ;GDT描述符32位 可读可写

Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 ;选择子 索引4 + 属性GDT + 特权级0
;....
mov ax, Stack32Selector
mov ss, ax
;....
call PrintString

这里并没有对esp指针进行初始化,似乎有bug?那么调用函数前esp的寄存器的值是多少?

这边可以反编译ndisasm -o 0x9000 loader > loader.txt看一下对应文件然后设置断点调试

image-20220513214306791

可以看到调用printstring函数前esp寄存器的值就是栈顶,是我们所预期的,那么这是为什么呢?

因为在16位实模式代码段就已经赋值0x7c000了,只要16位代码段的操作正确不乱修改栈,那么到32位下初始值也为0x7c00

保护模式下的栈段( Stack Segment )

  1. 指定一段空间 , 并为其定义段描述符
  2. 根据段描述符表中的位置定义筆子
  3. 初始化栈段寄存器( ss <- StackSelector )
  4. 初始化栈顶指针( esp <- TopOfStack )

因此对栈段进行修改,添加一个section

1
2
3
4
5
6
7
[section .gs]
[bits 32]
STACK32_SEGMENT:
times 1024 * 4 db 0

Stack32SegLen equ $ - STACK32_SEGMENT
TopOfStack32 equ Stack32SegLen - 1

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
%include "inc.asm"

org 0x9000

jmp CODE16_SEGMENT

[section .gdt]
; GDT definition
; 段基址, 段界限, 段属性
GDT_ENTRY : Descriptor 0, 0, 0 ;段描述表第一个不使用,仅作为占位符
CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 ;保护模式下的32位只执行代码段
VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 ;显存段描述符 可读可写且显存已经被访问过了
DATA32_DESC : Descriptor 0, Data32SegLen - 1, DA_DR + DA_32 ;32位 只读段
STACK_DESC : Descriptor 0, TopOfStackInit, DA_DRW + DA_32 ;32位 可读可写,注意这个栈空间段基址无需初始化,从0~0x7c00作为我们的全局栈空间

; GDT end

GdtLen equ $ - GDT_ENTRY ;全局描述符表的长度

GdtPtr:
dw GdtLen - 1 ;结构体界限
dd 0 ;GDT的起始地址,4字节


; GDT Selector

Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0 ;选择子 索引1 + 属性GDT + 特权级0
VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0 ;选择子 索引2 + 属性GDT + 特权级0
Data32Selector equ (0x0003 << 3) + SA_TIG + SA_RPL0 ;选择子 索引3 + 属性GDT + 特权级0
Stack32Selector equ (0x0004 << 3) + SA_TIG + SA_RPL0 ;选择子 索引4 + 属性GDT + 特权级0


; end of [section .gdt]

TopOfStackInit equ 0x7c00
[section .dat]
[bits 32]
DATA32_SEGMENT:
FYOS db "F.Y.OS!", 0
FYOS_OFFSET equ FYOS - $$ ;数据段内偏移地址
Data32SegLen equ $ - DATA32_SEGMENT


;实模式16位代码段
[section .s16]
[bits 16]
CODE16_SEGMENT:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, TopOfStackInit

; initialize GDT for 32 bits code segment
mov esi, CODE32_SEGMENT
mov edi, CODE32_DESC

call InitDescItem

mov esi, DATA32_SEGMENT
mov edi, DATA32_DESC
call InitDescItem

; initialize GDT pointer struct
mov eax, 0 ;eax寄存器值清0
mov ax, ds ;eax = cs
shl eax, 4 ;eax = cs<<4
add eax, GDT_ENTRY ;得到全局GDT段表的起始地址
mov dword [GdtPtr + 2], eax ;放入到结构体GdtPtr起始地址处

; 1. load GDT
lgdt [GdtPtr]

; 2. close interrupt
cli

; 3. open A20
in al, 0x92
or al, 00000010b
out 0x92, al

; 4. enter protect mode
mov eax, cr0
or eax, 0x01
mov cr0, eax

; 5. jump to 32 bits code
jmp dword Code32Selector : 0 ;这里就已经使用的是选择子

; esi --> code segment label
; edi --> descriptor label
InitDescItem:
push eax
mov eax, 0 ;eax寄存器值清0
mov ax, cs ;eax = cs
shl eax, 4 ;eax = cs<<4
add eax, esi ;得到32位代码段的真实段基地址[cs << 4 + CODE32_SEGMENT]

;对段描述符的段基址部分进行初始化设置
mov word [edi + 2], ax ;32位代码段的段基地址低16位放到偏移2处
shr eax, 16 ;低16位被移出去了,ax变为了高16位
mov byte [edi + 4], al ;将段基地址17-23位放到偏移4处
mov byte [edi + 7], ah ;将段基地址24-31位放到偏移7处
pop eax
ret

[section .s32]
[bits 32]
CODE32_SEGMENT:
mov ax, VideoSelector
mov gs, ax ;将显存段的选择子放入gs寄存器

mov ax, Stack32Selector
mov ss, ax

mov ax, Data32Selector
mov ds, ax

mov ebp, FYOS_OFFSET ;都是32位段下面的,因此使用FYOS_OFFSET偏移
mov bx, 0x0C
mov dh, 12
mov dl, 33

call PrintString

jmp $

; ds:ebp --> string address
; bx --> attribute
; dx --> dh : row, dl : col
PrintString:
push ebp
push eax
push edi
push cx
push dx
print:
mov cl, [ds : ebp] ;cl保存需要打印的字符
cmp cl, 0 ;判断是否为0
je end
mov eax, 80 ;
mul dh ;80*row -->eax
add al, dl
shl eax, 1 ;*
mov edi, eax ;mov edi, (80 * 12 + 37) * 2
mov al, cl
mov [gs:edi], ax
inc ebp
inc dl

jmp print

end:
pop dx
pop cx
pop edi
pop eax
pop ebp
ret

Code32SegLen equ $ - CODE32_SEGMENT

小结

  • 实模式下可以使用32位寄存器和32位地址
  • 显存是显卡内部的存储单元,本质和普通内存无差别
  • 显卡有两种工作模式:文本模式&图形模式
  • 文本模式下操作显存单元中的数据能够立即反应到显示器上

保护模式返回实模式

本质上是处理器的两种工作模式,因此应该是允许相互跳转的。

处理器中的设计简介

  • 80286 之后的处理器都提供兼容 8086 的实模式
  • 然而,绝大多时候处理器都运行于保护模式
  • 因此,保护模式的运行效率至关重要,性能瓶颈在于段描述符位于内存,必须读内存
  • 处理器的速度远远快于内存读取速度,那么,处理器如何高效的访问内存中的段描述符 ???

解决方案:高速缓冲存储器

当使用选择子设置段寄存器时(mov操作)
• 根据选择子访问内存中的段描述符
• 将段描述符加载到段寄存器的高速缓冲存储器
• 需要段描述符信息时 , 直接从高速缓冲存储器中获得

一个问题:当处理器运行于实模式时 , 段寄存器的高速缓冲存储器是否会用到 ?

  • 在实模式下,高速缓冲存储器仍然发挥作用
  • 高速缓冲区的段基址是 32 位 , 实模式下其值是相应段寄存器的值乘以16
  • 实模式下段基址有效位为 20 位 (32位的空间完全容纳的下),段界限固定为0xFFFF( 64K )
  • 段属性的值不可设置(实模式下都是指哪打哪内存属性都是可读可写) , 只能继续沿用保护方式下所设置的值

image-20220513222332686

高速缓冲存储器依然发挥作用,但是进入实模式时必须保证高速缓冲存储器段界限和段属性是正确的

然而高速缓冲存储器不允许直接访问但是又需要设置它的值
因此需要借助段描述符(mov)修改高速缓冲存储器的值,具体就是通过加载一个合适的描述符选择子到有关段寄存器 , 以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性 ! !

image-20220513223200672

80x86 中的一个神秘限制

  • 无法直接从 32 位代码段回到实模式
  • 只能从 16 位代码段间接返回实模式
  • 在返回前必须用合适的选择子对段寄存器赋值

保护模式下也是可以定义16位代码段,并且16位代码段也是存在运行于保护模式中,中转代码段不干任何其他操作,只用合适选择子给段寄存器赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CODE16_DESC     :     Descriptor    0,          0xFFFF,        DA_C          
UPDATE_DESC : Descriptor 0, 0xFFFF, DA_DRW ;16位 可读可写


Code16Selector equ (0x0005 << 3) + SA_TIG + SA_RPL0
UPDATESelector equ (0x0006 << 3) + SA_TIG + SA_RPL0

;16位保护模式代码段,保护模式下jmp Code16Selector : 0即可跳转到这
[section .s16]
[bits 16]
CODE16_SEGMENT:
mov ax, UPDATESelector ;定义一个特殊的段描述符用于刷新高速缓冲存储器
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax ;注意不能刷新cs寄存器,因为这段代码还在保护模式下面


;退出保护模式
mov eax, cr0
and al, 11111110b
mov cr0, eax

BACK_TO_REAL_MODE:
jmp 0 : BACK_ENTRY_SEGMENT ;段间跳转,cs是未知的,因此应该在前面动态填入

深入讲解jmp指令:段内跳转和段间跳转是不同的,因此在实模式下可以考虑动态修改操作数,可以跳转到另一个段中期望的地址

image-20220513223426970

因此在刚开始最先进入实模式时就可以mov [BACK_TO_REAL_MODE + 3], ax动态的修改了这个段基址jmp 0 : BACK_ENTRY_SEGMENT--->jmp cs : BACK_ENTRY_SEGMENT

另外注意jmp 0 : BACK_ENTRY_SEGMENT这里已经是在保护模式下运行了,但是我们并没有刷新cs寄存器对应的高速缓冲存储器,段界限段属性无法刷新,而如果设置的段界限太小了会直接报错,因为实模式下寻址范围是64k

因此在开始初始化的时候就应该将cs寄存器对应的高速缓冲存储器的偏移地址设置为0xFFFF

跳转到16位实模式后,对段寄存器的赋值只会改变相应高速缓冲区的段基地址,段界限和段属性沿用UPDATE_DESC中的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BACK_ENTRY_SEGMENT:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, TopOfStack16

in al, 0x92 ;关闭A20地址线
and al, 11111110b
out 0x92, al

sti ;启用硬件中断
mov bp, HELLO
mov cx, 12
mov dx, 0
mov ax, 0x1301
mov bx, 0x0007
int 0x10


jmp $