作者:K_Linux_Man 转载请注明出处
我们之前一直都工作在16位寄存器的环境下,我们即将要飞跃到32位寄存器 的平台。从16位寄存器操作到32位寄存器操作就是我们今天要讲到的从实模式到保护模式的转变。
这节内容晦涩难懂,为什么呢?因为都是intel们的大叔在设计芯片的时候没有考虑的非常长远,并且必须按照intel大叔们设计出的架构去实现代码。
此节内容废话连篇,请提前拿好板砖!
1.实模式的历史:
实模式,顾名思义,就是按照真实的地址去访问物理空间。我们大致先了解一下intel大叔们设计cpu的历史,有几个重要的节点,那就是intel8086 ,intel80286,intel80386的诞生
Intel8086是第一个16位的cpu,拥有16位寄存器,16的数据总线,和20位的地址总线,所以最大寻址达到1MB的空间,这1MB空间包括物理内存空间和BIOS-ROM空间。编写的汇编程序可以直接使用这些真实地址去访问BIOS程序或者外围设备或内存空间。后续又出了intel80286(80286,16位主理器,主频6/8/10/12~25MHZ,运算速度最高2.66MIPs,集成晶体管134,000个,3微米制造工艺,最大寻址内存16MB,生产曰期1982年.)这时是24位地址总线,寻址能力达到16MB,为了能够向下兼容,所以在80286及以后的x86系列兼容处理器仍然是开机启动时工作在实模式下。并且物理地址由段和偏移两部分组成,遵循公式: 物理地址=段地址左移4位+偏移地址,其中段地址和偏移地址都是16位的。
对于8086/8088来说计算实际地址是用真实地址对1M求模。8086/8088的地址线的物理结构:20根,也就是它可以物理寻址的内存范围为2^20个字节,即1 M空间,但由于8086/8088所使用的寄存器都是16位,能够表示的地址范围只有0-64K,这和1M地址空间来比较也太小了,所以为了在8086/8088下能够访问1M内存,Intel采取了分段寻址的模式:16位段基地址:16位偏移。其真实地址计算方法为:16位基地址左移4位+16位偏移=20位地址。
比如:DS=1000H EA=FFFFH 那么绝对地址就为:10000H + 0FFFFH = 1FFFFH 地址单元。 通过这种方法来实现使用16位寄存器访问1M的地址空间, 这种技术是处理器内部实现的,通过上述分段技术模式,能够表示的最大内存为:FFFFh: FFFFh=FFFF0h+FFFFh=10FFEFh=1M+64K-16Bytes(1M多余出来的部分被称做高端内存区HMA)。但8086/8088只有20位地址线,只能够访问1M地址范围的数据,所以如果访问100000h~10FFEFh之间的内存(大于1M空间),则必须有第21根地址线来参与寻址(8086/8088没有)。因此,当程序员给出超过1M(100000H-10FFEFH)的地址时,因为逻辑上正常,系统并不认为其访问越界而产生异常,而是自动从0开始计算,也就是说系统计算实际地址的时候是按照对1M求模的方式进行的。
技术发展到了80286,虽然系统的地址总线由原来的20根发展为24根,这样能够访问的内存可以达到2^24=16M,但是Intel在设计80286时是向下兼容的,所以在实模式下,系统所表现的行为应该和8086/8088所表现的完全一样。如果程序员访问100000H-10FFEFH之间的内存,系统将实际访问这块内存,而不是象8086/8088一样从0开始,这样将导致8086/8088的程序代码不能在80286上很好的运行,为此intel和IBM的大叔设计出了A20 Gate.
2.A20 Gate的出现:
A20 Gate其实是为了解决以上兼容问题而提出的。使用键盘控制器上剩余的一些输出线来管理第21根地址线(地址从A0-A19,这里又多出来个A20,哎!)
如果A20 Gate被置为有效,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;
如果A20 Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式。绝大多数IBM PC兼容机默认的A20 Gate是被禁止的。现在许多新型PC上存在直接通过BIOS功能调用来控制A20 Gate的功能.
即使A20 Gate在有效的状态下,当前所在的模式也还是实模式,80286以及后续的PC中,即使A20 Gate被打开,在实模式下所能够访问的内存最大也只能为10FFEFH,尽管它们的地址总线所能够访问的能力都大大超过这个限制。为了能够访问10FFEFH以上的内存,则必须进入保护模式。
3.犹抱琵琶半遮面的“保护模式”(虚拟地址模式)
保护模式又称位虚拟地址模式,说到保护模式咱们不能不提一下intel80386,保护模式真正的在intel80386上运行。虽然在intel80286就提及保护模式,但是也没有真正实现。
从intel80386开始,intel cpu家族进入了32位处理器的时代,80386拥有32位地址总线,寻址能力能够达到4GB.
为了允许80386上的pc机器能够运行起来8086/8088上的操作系统或者程序代码,必须考虑到兼容问题,为此,在80386以及以后的x86体系的cpu上电的一瞬间,还是让其工作在实模式下,如果是运行在8086/8088上的系统,无需跳转到保护模式,直接访问真实地址即可。如果运行在80386及以后的x86体系的cpu上,则由实模式直接跳转到保护模式。
intel8086/8088 是运行在实模式下的,需要强调一点的是:实模式下,处理器中的寄存器是16位的,数据线是16位的,地址线默认是20位的,而intel80386处理器在开机的一瞬间还是运行在16位的实模式下,要发挥它强大的寻址能力,我们必须让cpu进去32位的世界,这32位的世界就是“保护模式”。
在实模式下,20位的地址需要段左移4位+偏移地址才能凑够20位,从而才达到1MB的访问空间。而进入intel80386的时代,我们有了32位的寄存器,一个寄存器就可以寻址4GB的空间,即使这样,在保护模式下,寻址方式发生了重大的变化,采用段式存储机制。
4.保护模式下的段存储机制
在保护模式下,寻址方式发生了变化,正如上图所述,段偏移地址为32位,段选择器是16位,有时段选择器又称为段选择子。寻址方式已经变为--- “段选择器:偏移”的形式。
我们称这种形式位逻辑地址,最终通过保护模式下的段存储机制转换成“真正访问“的线性地址。
从图中可以看出,段存储机制有好几部分组成,我们先介绍一下段描述符表。段描述符表又称为GDT(Global Descriptor Table).GDT由许多个段描述符组成,每个段描述符里面记录了每个段的基地址,段的长度等其他基本信息。另外,段描述符表基地址寄存器Gdtr,gdtr寄存器存放段描述符表的基地址和段描述符表的长度。段选择器里面存放段描述符表的偏移,通过与Gdtr基地址寄存器进行逻辑运算后得到段描述符的地址,通过段描述符里的信息得到段地址,段地址最后与段偏移地址逻辑运算后得到最终的线性地址。上述的过程完全由cpu自动完成,我们要做的是构建段描述符和段描述符表,将段描述符表的基地址和长度赋值给基地址寄存器gdtr,最后提供段选择器和偏移地址就间接地访问线性地址了。
4.1 用段描述符(Descriptor)构造GDT
Gdt由段描述符(Descriptor)组成,所以我们需要了解段描述符的结构。
上图所述位段描述符的组成机构。一个段描述符共64位,由32位的段基址 和 20位的段界限 以及其他一些属性位组成。由于历史原因,段基址和段界限都被分成了两部分。
其中第5字节和第6字节中有一些属性位,我们暂且先不管它,用到时了解一下即可。段描述符主要存放段基址和段界限的。由多个这种结构的Descriptor(段描述符)组成的类似于表结构的内容称之为“GDT”.
我们将用nasm汇编语言去填充段描述符,并构造出GDT
nasm 提供宏函数,可带参数,假如有3个参数,定义时,函数名后跟着参数的个数,对变量进行使用时,第一个变量为%1,第二个变量为%2,第三个变量为%3,以此类推.
格式:
%macro 函数名 参数个数
%endmacro
我们将要编写一个填充描述符的宏函数,起名:pm.inc,这个宏函数的作用是在数据段中占用了8字节的空间来存放段描述符.
; 填充描述符
; usage Descriptor Base, Limit, Atrribute
; Base: dd 基地址为32位.
; Limit : dd 界限为20位.
; Attribute: dw 属性为12位.
%macro Descriptor 3
dw %2 & 0xFFFF ;段界限的低16位 (BYTE0 + BYTE1)
dd %1 & 0xFFFFFF ;段基址低24位 (BYTE2 + BYTE3+ BYTE4)
dw (%2 >> 8) & 0xF00 | (%3 & 0xF0FF) ; (BYTE5 + BYTE6)
db (%1 >>24) ;(BYTE7)
%endmacro
;构造GDT
LABLE_GDT: Descriptor 0, 0, 0 ;空描述符
LABLE_DESC_CODE32: Descriptor xx, xx, xx
LABLE_DESC_VIDEO: Descriptor xx, xx, xx
4.2 (段描述符)基地址寄存器Gdtr 与段选择器
通过我们对保护模式下段存储机制的了解,通过Gdtr寄存器得到GDT的基地址,通过与段选择器中的偏移(或称为索引)相加,从而进一步得到段的基地址。
所以,Gdtr主要存放GDT的基地址,段选择器里存放你要使用哪个段描述符的偏移(或索引).
下图为gdtr寄存器结构图
32位基地址 | 16位界限 |
------------------高地址-------------------------低地址---------------
gdtr 寄存器结构
将我们构造好的GDT的基地址和长度 存放到相应字段即可。
段选择器的结构也非常的简单。由16位组成,结构如图所示,段选择器的第2位是引用描述符表指示位,标记为TI(Table Indicator),TI=0指示从全局描述符表GDT中读取描述符;TI=1指示从局部描述符表LDT中读取描述符,RPL为请求特权级, 至此我们应将TI 和 RPL设置为0,这样做是非常简便的,对于后面我们将会解释这一点。索引值指描述符在描述符表中的序号,例如我们定义了三个描述符, LABEL_GDT序号为0,LABEL_DESC_CODE32序号为1 ,LABEL_DESC_VIDEO序号为2,以此类推。
4.3 保护模式的关键寄存器CR0
CR0寄存器的第0位为PE位,此位为0时,CPU运行在实模式下,为1时,运行在保护模式下。
4.4 有一种代码叫“经典”
org 0x7c00 ;告诉汇编器我们将这段代码加载到内存的0x7c00处
%include "pm.inc" ;常量/宏函数/
jmp LABEL_BEGIN
;GDT存放在数据段中,一个描述符占8字节,共声明了3个描述符
;构造GDT
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符,只是个标识
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C+DA_32 ;代码段,32位,声明时基地址暂且为0x00
LABEL_DESC_VIDEO: Descriptor 0xB8000, 0xFFFF, DA_DRW ; 0xB8000为显存首地址,段界限为64K
;构造GDT结束
;构造Gdtr寄存器内容
GdtLen equ $-LABLE_GDT ;GdtLen为16位
GdtPtr:
dw GdtLen ; 低16位为gdt长度(界限)
dd 0 ; GDT基地址,声明时暂且设置为0
;构造Gdtr寄存器内容结束
;创建段选择器(GDT选择子)
SelectorCode32 equ LABEL_DESC_CODE - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
;将32位代码段的首地址填充到CODE32描述符表的基地址中
xor eax, eax ;将eax清空
mov ax, cs
shl eax, 4 ;左移4位
add eax, LABEL_SEG_CODE32 ;eax存放32位代码段的物理地址
mov word [LABEL_DESC_CODE32 +2], ax ;设置CODE32段描述符的段基址的低16位
shr eax, 16 ;右移16位
mov byte [LABEL_DESC_CODE32 +4 ], al ;设置段基址的16-23位
mov byte [LABEL_DESC_CODE32 +7], ah ; 设置段基址的24-31位
;设置Gdt基地址, gdt基地址存放于gdtr基址寄存器中
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ;eax存放gdt的基地址
mov dword [GdtPtr+2], eax ;设置32位的Gdt基地址
;gdtr寄存器赋值
lgdt [GdtPtr]
;关中断
cli
;打开地址线A20
in al, 92h
or al, 0x2
out 92h, al
;准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
;跳转进入保护模式
jmp dword Selector32:0 ;将SelectorCode32 装入cs,并跳转到CODE32段偏移为0的位置,这个位置正好是LABEL_SEG_CODE32处
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax
mov edi, (80*10 + 0) * 2;屏幕第10行,第0列
mov ah, 0ch ;0000黑底 1100:红字
mov al, ‘P’
mov [gs:edi], ax
jmp $
SegCode32Len equ $ -LABEL_SEG _CODE32