12 实模式到保护模式 下

参考

https://www.cnblogs.com/wanmeishenghuo/tag/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/

https://blog.51cto.com/13475106/category6.html

 

 

 

 

 

这一节,我们深入研究一下保护模式:定义显存段

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

显卡:

  1、为显示器提供需要显示的数据

  2、控制显示器的模式和状态

显示器:

  1、将目标数据以可见的方式呈现在屏幕上

显存的概念和意义:

1、显卡拥有自己内部的数据存储器,简称显存

2、显存在本质上和普通内存无差别,用于存储目标数据

3、操作显存中的数据将导致显示器上的内容改变

显存在本质上和内存没有差别,只不过它存在与显卡的内部

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

 

 

  在不同的模式下,显卡对显存内容的解释是不同的

  可以使用专属指令或者int 0x10中断改变显卡的工作模式

  在文本模式下:

    显存的地址范围映射为:[0xB8000,0xBFFFF],直接往这个地址写数据,显示器上就会显示数据

 

 

    一屏幕可以显示25行,每行80个字符,每个字符占两个字节,一个字节为实际的字符(低字节),一个字节为字符的属性(高字节)

 下面我们来完成在屏幕的指定位置上打印指定字符串的功能,在保护模式下打印指定内存中的字符串,步骤如下:

  定义全局堆栈段(.gs),用于保护模式下的函数调用

  定义全局数据段(.dat),用于定义只读数据(D.T.OS!)

  利用对显存段的操作定义字符串打印函数(PrintString)

汇编知识:

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

    被乘数放到AX寄存器

    乘数放到通用寄存器或内存单元(16位)

    相乘的结果放到EAX寄存器中

  再论$$和$:

    $表示当前行相对于代码起始位置处的偏移量

    $$表示当前代码节(section)的起始位置

下面直接给出打印字符串的程序:

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
162
163
164
165
166
167
168
169
170
171
172
173
174
%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
VIDEO_DESC      :     Descriptor 0xB8000,     0x07FFF,         DA_DRWA + DA_32
DATA32_DESC     :     Descriptor    0,    Data32SegLen - 1,    DA_DR + DA_32
STACK_DESC      :     Descriptor    0,     TopOfStackInit,     DA_DRW + DA_32
; GDT end
 
GdtLen    equ   $ - GDT_ENTRY
 
GdtPtr:
          dw   GdtLen - 1
          dd   0
           
           
; GDT Selector
 
Code32Selector    equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector     equ (0x0002 << 3) + SA_TIG + SA_RPL0
Data32Selector    equ (0x0003 << 3) + SA_TIG + SA_RPL0
StackSelector     equ (0x0004 << 3) + SA_TIG + SA_RPL0
 
; end of [section .gdt]
 
TopOfStackInit    equ 0x7c00
 
[section .dat]
[bits 32]
DATA32_SEGMENT:
    DTOS               db  "D.T.OS!", 0
    DTOS_OFFSET        equ DTOS - $$
    HELLO_WORLD        db  "Hello World!", 0
    HELLO_WORLD_OFFSET equ HELLO_WORLD - $$
 
Data32SegLen equ $ - DATA32_SEGMENT
 
[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
    mov ax, ds
    shl eax, 4
    add eax, GDT_ENTRY
    mov dword [GdtPtr + 2], eax
 
    ; 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
    mov ax, cs
    shl eax, 4
    add eax, esi
    mov word [edi + 2], ax
    shr eax, 16
    mov byte [edi + 4], al
    mov byte [edi + 7], ah
     
    pop eax
     
    ret
     
     
[section .s32]
[bits 32]
CODE32_SEGMENT:
    mov ax, VideoSelector
    mov gs, ax
     
    mov ax, StackSelector
    mov ss, ax
     
    mov ax, Data32Selector
    mov ds, ax
     
    mov ebp, DTOS_OFFSET
    mov bx, 0x0C
    mov dh, 12
    mov dl, 33
     
    call PrintString
     
    mov ebp, HELLO_WORLD_OFFSET
    mov bx, 0x0C
    mov dh, 13
    mov dl, 31
     
    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]
    cmp cl, 0
    je end
    mov eax, 80
    mul dh
    add al, dl
    shl eax, 1
    mov edi, eax
    mov ah, bl
    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

 

对上述程序做一个解析:

  第12行定义了显存段,这个段可以直接根据起始地址和段界限给出

  第13行定义了32位的数据段,其中的起始地址需要在运行时计算,因为具体的起始物理地址和段寄存器有关,而段界限可以在编译时期计算出来

  第14行定义了保护模式下的堆栈段

定义了段描述符后必然要定义相应的段选择子,第27、28、29分别定义了对应上述三个段的段选择子。

保护模式下的堆栈栈顶依然定义为0x7c00。

32位数据段中(35-43行)定义了一些需要打印的字符串。

16位实模式下的代码和上一节几乎一样,这一节我们需要在运行期初始化两个段描述符中的基地址,因此,我们将初始化的过程封装成了函数InitDescItem,这个函数需要两个参数,esi代表代码段或者数据段的标签,edi代表段描述符的标签。有了这个函数之后,55-63行就可以方便的调用这个函数来初始化段描述符中的基地址了。

  下面进入32位代码段,从111行开始,114-121行,分别将相应的段选择子存入相应的段寄存器中,显存段存入了gs,堆栈段存入了ss,数据段存入了ds。当需要用到堆栈时,CPU自动根据ss和sp的值计算真正的物理地址,sp在16位代码中初始化为了栈顶,虽然在16位代码中有函数调用,但是向32位代码跳转时这些函数已经全部返回了,因此,在32位代码的起始处,sp还是指向栈顶的。当取32数据段中的字符时,我们显式的使用ds作为段寄存器。当向显存写数据时,我们显式的使用gs作为段寄存器,这样可以保证地址计算不会出错。有些指令会有默认的段寄存器,但是为了保险我们显式的指定。这在打印函数中会看到。

  32位代码段设置完几个段寄存器后就开始调用打印函数了,先是将参数写入相应的寄存器,然后调用函数。我们分析一下130-135行的调用过程:

  第130行将字符串“Hello World!”的起始地址相对于32位数据段的偏移量存入ebp,然后在其他几个寄存器存入打印属性和打印的行和列,然后调用打印函数。

  下面进入139-172行打印字符串的函数,它是在32位代码段中的,这个函数接受三个参数,ds:ebp存放字符串地址,bx存放属性,dx存放行和列,也就是要打印在第几行第几列。

  我们看一下具体的打印过程:

  142-147行将一些寄存器先保存起来,第150行中将目标地址中的字符取出来,目标地址是根据 段+偏移 的方式算出来的,mov cl, [ds:ebp]指令中,我们显式的指明段寄存器为ds,ds中存放的是32位数据段的选择子,这也是我们在前面初始化好了的,这样可以保证计算出的字符的地址是正确的。151行判断要打印的字符是否为0,为0就不打印了,跳到end,不为零的话就继续根据行和列的值计算出地址(这个地址是相对于显存起始地址的偏移量),把这个偏移量存入edi寄存器, 第160行使用指令mov [gs:edi], ax将字符写入显存中,这里的显存物理地址是根据gs和edi计算出来的,我们显式的指明gs作为段选择子,其中存放的是显存段的选择子,这也是我们在前面初始化好了的,这样可以保证写入到正确的位置。 

打印字符串的效果如下:

 

  我们来看一下92-108行,这一段代码作用是计算某个段的物理基地址,并写入到段描述符中。esi存放段标签,edi存放的是段描述符标签。

假设现在esi存放了DATA32_SEGMENT的地址,edi存放的是DATA32_DESC。计算段的物理基地址时使用了eax(里面是cs的值),这样计算出了物理地址,向段描述符中写时使用的是mov byte [edi + 4], al 指令,edi中的值是相对于0x9000计算出来的值,而真正的物理地址还需要根据ds段寄存器的值计算出来(此时处于16位实模式,段寄存器存放的就是段基地址),此时的ds中的值必须和cs中的值一样才行,这在48-51行也保证了,如果ds的值不等于cs的话,程序会发生错误(已经实验验证)。

  在boot.asm程序中,我们也是将cs,ds,ss等段寄存器弄成了一样的值,在加载loader.asm时,我们只给出了偏移地址,不管段基址用哪一个计算,结果都是一样的,我们将loader加载到了0x9000处,如果段寄存器中的值是0的话,那就真正加载到了物理地址的0x9000处,boot.asm有一句跳转到指定地址的指令jnb 0x9000,如果段寄存器为0,它就可以跳到物理地址0x9000处,和实际的加载地址正好对应上。如果段寄存器不为0,它就要根据cs寄存器计算真正的物理地址,而如果cs不为0,则ds和ss等也不为0(它们三个是一样的值,代码中有赋值操作),而计算出来的物理加载地址也就不是0x9000,但是不管是多少,只要保证jnb跳转时的偏移地址和加载loader.asm时的偏移地址是一样的就行(程序中都是0x9000),这样就不会出错,因为段寄存器中的值都是一样的,计算出来的物理地址肯定也是一样的。

  到了loader.asm中,又对几个段寄存器进行了一次操作,还是要保证cs,ds,ss的值是一样的,这样就跟上面标红的一段对应起来了,使用cs计算一个段的真实物理起始地址可以得到正确的值(这个值本应该按ds计算,因为加载loader.asm是按ds计算的,但是cs和ds相等,所以结果一样),使用ds计算段描述符的起始地址也可以得到描述符正确的值,mov byte [edi+4], al中默认使用ds作为段寄存器。标签的值是相对于代码的起始地址0x9000计算出来的地址,如果段寄存器都为0,标签的值等于真实的物理地址。如果段寄存器不为0,只要保证它们在实模式下都相等也可以,这样它们也算有一个统一的“0”地址。

       将编译后生成的data.img放到windows下的虚拟机路径,选择从这个光驱文件移动,也可看到如下结果:

 

 

总结:

 

 

 

 

 

posted on   lh03061238  阅读(129)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)

导航

< 2025年3月 >
23 24 25 26 27 28 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 1 2 3 4 5
点击右上角即可分享
微信分享提示