从实模式到保护模式笔记7进入保护模式
从实模式到保护模式笔记7进入保护模式
0x00 代码
mov ax,cs
mov ss,ax
mov sp,0x7c00
mov ax,[cs:gdt_base+0x7c00];低16位
mov dx,[cs:gdt_base+0x7c00+0x02];高16位
mov bx,16
div bx
mov ds,ax;定位数据段
mov bx,dx
;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [bx+0x00],0x00
mov dword [bx+0x04],0x00
;#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800
;#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
mov dword [bx+0x10],0x8000ffff
mov dword [bx+0x14],0x0040920b
;#3描述符,保护模式下的栈段描述符
mov dword [bx+0x18],0x00007a00
mov dword [bx+0x1c],0x00409600
;初始化描述符寄存器GDTR
mov word [cs:gdt_size+0x7c00],31 ;描述符界限(4*8-1)
lgdt [cs:gdt_size+0x7c00] ;
in al,0x92
or al,00000010B
out 0x92,al;启用a20 第21根地址线
cli;屏蔽可屏蔽中断intr
mov eax,cr0
or eax,1
mov cr0,eax;进入保护模式
jmp dword 0x0008:flush;16位描述符选择子(0x0008即1#描述符),32位偏移
;jmp能清流水线并串行化处理器
[bits 32]
flush:
mov cx,00000000_000_10_000b ;10即2,即2#描述符
mov ds,cx;段选择子,段选择子是16位的在ds里 而不是用eds ecx
;"Protect mode OK."
mov byte [0x00],'P'
mov byte [0x02],'r'
mov byte [0x04],'o'
mov byte [0x06],'t'
mov byte [0x08],'e'
mov byte [0x0a],'c'
mov byte [0x0c],'t'
mov byte [0x0e],','
mov byte [0x10],'m'
mov byte [0x12],'o'
mov byte [0x14],'d'
mov byte [0x16],'e'
;堆栈操作
mov cx,00000000_000_11_000b
mov ss,cx
mov esp,0x7c00
mov ebp,esp
push byte '.';不会真的只压入一个字节的
sub ebp,4
cmp ebp,esp
jnz ghalt
pop eax
mov [0x1e],al
ghalt:
hlt;处理器进入低功耗模式,且由于中断被禁用了,无法被唤醒
;--------------------------------------------
gdt_size dw 0
gdt_base dd 0x00007e00;GDT的物理地址
times 510-($-$$) db 0
db 0x55,0xaa
上述代码运行正确,不放截图
0x01 代码讲解
全局描述符表:(Global Descriptor Table)
8086模式下,为了让程序能够自由浮动而不影响正确运行,处理器将内存划分成逻辑上的段,并在指令中使用偏移地址。在保护模式下,对内存的访问仍然使用段地址和偏移地址,但是每个段必须登记
登记的信息包括段的起始地址,段的界限(大小),以及各种访问属性,当访问的偏移地址超出段的界线时,会产生内部异常的中断。
段描述符:指描述段的信息的那8个字节,每个段都需要一个段描述符,存放在内存空间中,段描述符集中存放在一起,就成了段描述表,最主要的段描述表即使全局描述符表,为整个软硬件系统服务,进入保护模式前必须定义全局描述符表。
为了跟踪/描述 全局描述符表 本身,处理器内部有一个全局描述符表寄存器(Global Descriptor Table zxRegister,GDTR),该寄存器是48位,低16位表示全局描述符表的边界,高32位则是全局描述符表的起始线性地址(不是物理地址)
因为GDT的界限是16位的,那么表最大时2^16字节,最多定义8192个段描述符,由于进入保护模式前必须定义GDT,而实模式只有1MB空间可用,所以GDT一般定义在1MB以内的空间里。
段描述符详解:
之所以设置段描述符,而不让直接访问,是为了在多任务环境下保障内存安全。每个段描述符占8个字节,下面是其每个位的含意(下面是低32位,上面是高32位):
为什么低32位中的段基地址和段界限和高32位中的是分开而不是连续存放的?因为这是80286后遗症(80286是32位的段描述符,为了兼容80286就变成了上面这个样子)。段基地址是32位的,所以可以是0-4GB内的任意地址,不过16字节对齐的段基地址可以使得处理器性能最大化。20位的段界限是用来限制段的扩展范围,所以对于向上扩展的段(代码和数据段),偏移量从0到段界限;向下扩展的段(栈),段界限决定了偏移量的最小值。(也就是负偏移,段界限的偏移的绝对值最大,所以最小)
下面是每个位的含义:
G位是粒度(Granularity)位,当G位是0时,段界限以字节位单位。此时段的扩展范围是1字节-1MB,相反如果该位是1,则是以4KB位单位,范围是4KB-4GB
S位是指定描述符的类型,当该位是0时,表示是一个系统段,为1则是一个程序的代码段 数据段 或者栈段
DPL(Descriptor Privilege Level)表示描述符的特权级,由两位组成值为0,1,2,3,特权级别一次下降。刚进入保护模式时执行的代码具有0级通常是操作系统,操作系统装载用户程序时会指定一个稍低的特权级。而DPL是指定访问该段必须拥有的最低的特权级,比如设置为3,那么所有程序均可以访问该段,如果设置为0,则只有特权级为0的程序可以访问。
P位是段存在位(segment present),P位用于指示描述符所对应的段是否存在内存中。(因为内存紧张时,可能只建立了描述符,但是段本身不在内存,而被换到硬盘里了)P位是由处理器负责检查的,每当通过描述符访问段时,如果P位是0,就会产生一个异常中断,该中断一般由操作系统提供,会把该段从硬盘换回内存,并将P位置1
D/B位是 “默认操作数大小(Default Operation Size)”,设置该位是为了兼容16位保护模式,尽管已经没人用了。我们将在这本书中将他置1,表明是32位。
L位是64位代码标志段,保留给64位处理器使用,本书中置0
TYPE字段共4位,用于指示描述符的具体类型,对于数据段来说这4位分别是X\E\W\A 对于代码段则是X\C\R\A
X表示是否可执行,为0即不可执行(数据段),为1可执行(代码段)。E位是数据段用来决定段的扩展方向,如果为0,则是向上扩展,为1则是向下扩展(栈段)。W为决定是否可写,A位则是已访问(Accessed),每当该段被访问,处理器将其置1,置0则应该是由操作系统来完成,操作系统定期检查该位,可以统计出段的使用频率,从而决定哪些段应该被换到硬盘。
C表示是否特权级依从,C=0表示只允许特权级相同的代码段调用该段代码,或者通过门调用,C为1表示允许低特权级的程序转移到该代码段。R表示能否读出,能执行不代表能读出。
AVL位是可以使用的位(available),通常由操作系统使用,可以当作是处理器用不上多了的一位。
通过对上面段描述符的详解,我们可以知道我们写的三个段描述符的具体数值的含义:
;#1描述符,保护模式下的代码段描述符
mov dword [bx+0x08],0x7c0001ff
mov dword [bx+0x0c],0x00409800
低32位 0x7c00是段基址0-15;0x01ff 段界限 0-15;
高32位 0x00 段基地址24-31, 0x4(0100)表示G位0粒度为1字节;D/B为1 表示是32位保护模式;L位为0表示非64位保护模式;AVL为0不管他,多余的一位;0x0 段界限16-19;0x98 (1001_1000)表示P位1 该段存在内存空间中,DPL位00,访问该段需要特权级0;S位1该位是一个普通的代码段或者数据段;TYPE位1000 1表示 该位可执行 0是代码段 该位非特权级依从,必须要同特权级才能调用 0该段不能读出 0该位尚未访问过(由处理器负责置1 软件只负责置0)
最后lgdt m48(48位内存空间)指令,读48位数据,传入GDTR(初始状态GDTR 低16位全局描述符表界限是0XFFFF,表的基地址为0X00000000) lgdt指令不影响任何标志寄存器(和mov push inc等差不多),注意此时仍然没有进入到保护模式
第21条地址线A20:
8086时,只有16根地址线,但实现了1MB寻址,其最大物理地址0xfffff,当再+1时,进位被抛弃回到0x00000,很多老的8086程序利用了这个特性,然而到了80826的实模式时,由于其有24根地址线,导致0xfffff再+1变成了0x100000,回不到0,所以处理器厂商就让第21根地址线恒为0,需要使用时才打开正常使用第21根地址线。
开头代码中对0x92端口的第二位的操作就是打开a20引脚
保护模式下的内存访问
CR0(Control register)是处理器内部的控制寄存器0号,还是CR1 2 3…. CR0是32位寄存器,其第一位(位0)为PE位 保护模式允许位(Protection mode Enable),置1 则进入 保护模式。
由于保护模式下中断机制和实模式不同,因此原有的中断向量表不再使用,BIOS中断不能再用,所以我们在进入保护模式之前使用了cli禁止了可屏蔽中断。没有直接 or cr0,0x00000001 而是先移到eax再修改再移回去。
8086处理器有段寄存器 CS,DS,SS,ES 32位处理器又加了FS GS两个。8086处理器在实模式下(其也只有实模式),访问内存时,用的时逻辑地址,即 段地址 * 16+偏移地址
32位处理器中在实模式下,这6个段寄存器前16位和8086相同,可以接受例如 mov es,ax的指令 向段寄存器按的前16位传送段地址,但是具体的工作方式不一样,8086的es只有16位,所以使用es 里的内容和偏移地址在处理器内部地址生成的部件得出逻辑地址,而32位处理器的实模式则是 mov es,ax后将ax里的内容送到es的低16位,再让es左移4位(处理器自动进行),这样es的低20位保存的段地址就是段的物理地址的起始地址!
至于保护模式,访问段时,也需要指定一个段,但是传送到段选择器(段寄存器的低16位)不再是逻辑短地址,而是段描述符表中的索引号:
段描述符索引用来从段描述符表中选择一个段描述符,其占13位,所以最多有2^13个,和之前GDTR里有关GDT的界限占16位,但是因为每个描述符表是8字节,所以最多只能有2^13个段描述符对应上了。
TI则是描述符表指示器(Table Indicator),表示描述符在GDT中还是LDT(Local Descriptor Table)中,TI为0在GDT中,为1则在LDT中。
而RPL(Request Privilege Level)请求特权级,则是给出当前选择子的那个程序的特权级别,共两位0,1,2,3本章先设置为0 最高
mov cx,00000000_000_10_000b ;0x10即2#描述符
mov ds,cx;段选择子,段选择子是16位的在ds里 而不是用eds ecx
开头的代码中,进入保护模式32位指令环境后,我们用的是mov cx,00000000_000_10_000b,低16位段选择子就是cx,我们给出的13位段描述符索引是2对应 我们一开始在表里定义的2#数据段描述符,后面000则是表示在GDT中找,且设置请求访问特权级为0最高。
mov ds,cx等改变段选择器的指令在处理器中的作用过程:当这些指令执行时,将指令中的索引号*8+GDTR中高32位GDT的基地址相加,来访问GDT(如果没超过GDT的界限的话),然后将找到的段描述符加载到有关的段寄存器的不可见的描述符高速缓存部分,加载的部分包括段的线性基地址、段界限和段的访问属性
此后每当有访问ds:[mem1]的指令时,不再访问GDT,而是直接用当前段寄存器描述符高速缓存部分提供的线性基地址,jmp指令改变cs时也是一样的原理
清空流水线并串行化处理器
mov cr0,eax;进入保护模式
jmp dword 0x0008:flush;16位描述符选择子(0x0008即1#描述符),32位偏移
在我们设置了cr0的PE位进入了保护模式后,我们上面的代码里有一个jmp dword 0x0008:flush. 这是跳转到哪儿呢?由于我们已经进入了保护模式,0x0008不再时段地址,而是段选择子,对应00000000_0000_1000,对应的段选择子索引是1,也就是1#段描述符,而1#段描述符的段基地址是0x7c00,也就是我们代码开始的地方,实际上这是代码段,那么jmp dword 0x0008:flush 效果就是跳转到flush处执行,要注意,虽然我们进入了保护模式,但是由于cs里的描述符高速缓存部分的D位还是0(我们正准备跳到我们自己定义的D位1的32位的代码段,但是还没跳到呢,所以还是0),所以处理器会运行在16位的保护模式下,而且该jmp也在bits 16环境下编译。
但是我们要跳的代码段是个32位保护模式下的段,所以必须给出32位偏移地址,flush必须是双字的汇编地址,而且还必须让cpu工作于32位模式下(cpu工作在16位不认32位的偏移地址)所以我们加了dword 这个会让这条指令前面加上0x66 并且让flush也是双字编译的汇编地址(0x66前缀用于模式反转,工作在16位是,用就会让处理器暂时在这条指令处于32位,反之亦然,但是该指令是不是编译成了32位的那是bits改变的)
奇怪的是,jmp指令的下一句本来就是flush标号对应的地方,为什么还要jmp来跳转呢?原因是我们flush标号开始都是在32位保护模式下工作的指令,而flush前面则工作于16的实模式下,而16位的实模式由前面的可知用到了32位段寄存器的低20位,所以对段寄存器的描述符高速缓存部分也有影响,此外很多16位的指令进入了流水线,而现在到了32位模式,必须清除,所以用jmp或者call 清空流水线让处理器串行化执行(即重新按照指令的自然顺序在执行)。
保护模式下的栈
保护模式下的栈,其线性基地址定于于段描述符里,而其段界限和粒度的乘积,是ESP偏移量的最小值,也就是说,因为栈段是向下扩展的,所以段界限和粒度的乘积是栈段的下限,所以有ESP>段界限*粒度值,那么上限在哪儿呢?我们开头代码定义的栈段下界限是0x07a00,没有上界限吗?确实没有,ESP即使为0xffffffff都行,因为我们这里定义段的方式有问题,下一章有更好的方法。
图上是我们的3#描述符栈段的初始化和界限以及线性基地址,可以看到其并没有上界
push byte '1';. ascii 31
对于进入32位模式下的push byte 指令,我们可以用bochs查看其编译和运行后的结果:
可以看到,byte前缀让其编译时变成8位只占1个字节,但是处理器压入栈的还是32位4个字节。
如果我们把byte变成word 会怎么样?
可以看到加上word后,编译出来的是2字节的0x0031,而且前面有0x66前缀,这条push指令被处理器当都在16位模式下,可以从print-stack 看到,栈顶地址是0x00007bfe相比push之前的0x00007c00减小小2,而不是4(而用byte是4,因为处理器工作在32位)
16位与32位指令的根本区别在于操作数的位数不同,实模式下通过66前缀是可以用32位数据的,所以也可以一次push 32位数据
0x02 保护模式下的调试
1段寄存器观察
在实模式下执行
mov ax,0x0031
mov ss,ax
用sreg命令观察段寄存器ss的值,发现ss一共分两部分,保护模式下的段选择子保存了段地址,描述符高速缓存部分的低32位,仍然在工作,低16位是段界限0xffff,所以段最大64k,段基地址是0x00310 是0x0031 * 16,也是正确的段的基地址,所以32位处理器即使在实模式下仍然可以用 段寄存器的描述符高速缓存部分 里的 段基地址+给的偏移地址 正确的得出线性地址!
同时可以看到,上面sreg命令还给出了段的读写属性,以及是否被访问
sreg命令还可以看到gdtr寄存器的值
2设置PE位后的段寄存器状态
这是我们在设置cr0的pe为之前的cs段寄存器的状态,下面是设置之后的状态
可以发现,即使我们设置的cr0但是相关段寄存器的值(包括描述符高速缓存部分都并没有改变) 段基地址仍然是0x00000000 限制长度是0xffff 64kb dh部分0x00009300-> 00000000_0000_0000_1001_0011_00000000
G位0 粒度为1byte D/B位为0,操作数为16位,即让处理器工作在16位保护模式 L位0 非64位 AVL位0(多余位)
P位1 当前位在内存 DPL位00 变时访问该段需要特权级至少是0 S位1 表示当前为是一个程序段(非系统段) TYPE段 0011表示是数据段 向上扩展 可读可写 最近访问过
可以看到cs寄存器的描述符高速缓存部分被标识位数据段,所以我们必须要刷新cs寄存器 ,所以我们在进入保护模式后要jmp
执行jmp后 cs寄存器里的描述符高速缓存部分,才是正确的段基地址和界限(我们规定的0x000001ff)和属性
仔细观察jmp dword 0x0008:flush 这个指令及编译后的结果,编译后以66开头,且flush的汇编地址(因为段基地址是0x7c00所以偏移地址和汇编地址相同)以32位表示。
这里由于是在保护模式下的jmp 所以实际上是先带着段选择子0x0008去GDT找到合法的描述符刷新cs 由此进入32位保护模式,这是就要求32位的偏移地址再进行跳转,所以这就是为什么我们加上dword。
查看GDT
当我们用lgdt mem48 加载gdt后,就可以在bochs里用info gdt来查看gdt里的内容了
查看控制寄存器
使用creg可以查看控制寄存器cr0到cr8的内容,注意标志位大写是1小写是0