不打算按别人的思路来,因为在我学的过程中上网查,发现网上的博客都是互相抄的,最终还是抄书的。
Intel 64 和 IA-32架构处理器在进入保护模式之后,就会有一些列保护机制。其中出现了三个特别重要的东西:CPL、DPL、RPL。
CPL表示当前正在执行程序的特权级,它保存在cs段寄存器里面;
DPL表示某个段的特权级,保存在这个段对应的段描述符中;
RPL表示请求访问特权级,保存在选择子中(表示程序希望通过这个级别的特权级区访问另一个段)。
所谓的特权级一共有4个,ring0,ring1,ring2,ring3,数字越小,特权级越高。对于数据段和代码段的访问都要满足特权级规则。
对于数据段(数据段都是非一致的),只允许高特权级访低特权级,或者同级之间访问,不允许低特权级程序访问高特权级数据段;
对于一致代码段,只允许低特权级访问高特权级代码段,或者同级访问;
对于非一致代码段,只允许同一特权级访问。
(这里的一致不一致大致是指这个代码段在系统中是否是拿出来共享的,也就是描述符中的一位,不必特别在意,因为大部分段都是非一致的)
我们用Intel手册中的例子来说:
在这里面代码段C是非一致代码段,D是一致代码段。代码段A能够访问到代码段C,因为它们处在同一个特权级,使用的选择子的RPL也是2;
代码段B就不能访问C,因为它的特权级比C要低;但是它却可以访问D,因为D是一致代码段,只能被低特权级的程序访问。
大致就是这么一个过程,我们现在正在执行某一个代码段的程序,我们的CPL一般就等于这个代码段的DPL,然后我们忽然想要访问另外一个段,则在保护模式下,我们必须使用一个叫段选择子的东西去访问另一个段,而这个选择子又包含一个RPL,它相当于我们给出一张一卡通,用这张一卡通去访问目标段,看看这个一卡通能不能满足目标段的DPL要求。
我们在访问代码段的过程中,我们的特权级也可能发生变化,每当我们的特权级CPL发生变化,处理器就要求我们切换堆栈,所以一个程序按道理需要配上4个备用堆栈,但是实际上如果我们能保证我们不会用到某个特权级,就可以不设对应的堆栈,例如在linux里面,只用到了ring0和ring3,则就不需要设置特权级为1和2的堆栈,关于怎么设置堆栈,下面等一下在讲。
现在我们先说说特权级切换,我打算先从ring0切换到ring2,在从ring2切换到ring0。但是我们知道,非一致代码段是不能从低特权级切换到高特权级的,这里我们会看到一个叫调用门的东西,它可以帮我们从低特权级代码段切换到高特权级代码段。
首先,从ring0切换到ring2,一开始我以为可以直接jmp Selector:Offset就可以了,但是出错了。我发现这样做不行,找Intel手册,上网查,都找不到办法,后来还是参考了于渊老师的办法,我觉得这个方法特变巧妙,就是利用ret/retf指令。我们都知道汇编语言里的call,是函数调用,它分为两种,近的和远的,所谓近的就是被call的程序在同一个段里面,所谓远的么就是被call的程序再另一个段里面,对应两种返回就是ret和retf。在近call中,处理器先把当前(准确的说是下一条)eip推入堆栈,然后把目标指令地址存入eip寄存器,开始执行新的程序,在ret时,就是把栈顶的值(也就是eip)弹出到eip寄存器里面,如果有参数传递,则把esp加上参数占用地址的大小。如果是远call和retf的话,则在推入和弹出eip时会多加一个cs,其它一样。
利用retf的这个特点,我们就可以实现向低特权级的代码段的跳转。因为retf的本质就是把栈顶的两个地址里的内容分别弹出到cs:eip里面,假设我们的目标代码段(低特权级)为SelectorRing2:0则我们可以这么做:
push SelectorStack2
push StackTop2
push SelectorRing2
push 0
retf
这里面多推入了堆栈段内容,是因为当特权级发生改变时,需要切换堆栈,而当检测到特权级变化时,处理器就会从堆栈段里取堆栈段选择子和栈顶指针来创建新的堆栈段。
这样我们就可以进入到[SelectorRing2:0]指向的程序了,注意32位对其和标注,我曾今在这里没有对其和标注,而出错过。
接下来就是在ring2里面显示写什么东西,再切换回ring0,第特权级程序不能访问高特权级非一致代码段,所以用retf也不行了,这里就必须使用调用门。在Intel手册里面有一条非常特别,就是调用门只能从低特权级到高特权级,不能反过来。
首先我们创建调用门的门描述符
LABEL_CALL_GATE: Gate SelectorRing0, 0, 0, DA_386CGate + DA_DPL2
表示目标代码段的段选择子为SelectorRing0,偏移为0,传递参数为0,特权级为2
然后建立门选择子
SelectorGate equ LABEL_CALL_GATE - LABEL_GDT + SA_RPL2
这里也涉及到堆栈的切换,但是却没之前那么简单了,因为Intel为操作系统想的比较多。在x86里面有一个叫TSS的东西,Task State Segment,本质上是一个段,是专门用来记录程序运行状态的。它能实现的功能是这样的:一开始我们都会处在ring0的特权级,当我们特权级改变时,如果我们加载了这个TSS,处理器就会到这里面来找对应的在堆栈,比如我们要跳转到ring1的代码段,则需要一个ring1特权级的堆栈,处理器就会到TSS里面找到对应的ring1特权级的段选择子和栈顶指针,来构建新的堆栈,当程序返回的时候,又会恢复为原来的堆栈。
构建TSS段
[section .tss]
align 32
[bits 32]
LABEL_TSS:
dd 0
dd STACK0_TOP
dd SelectorStack0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dd 0
dw 0
dw $ - LABEL_TSS + 2
db 0x0FF
TSS_LEN equ $ - LABEL_TSS - 1
...
LABEL_DESC_TSS: Descriptor 0, TSS_LEN, DA_386TSS
SelectorTss equ LABEL_DESC_TSS - LABEL_GDT
然后在retf之前,加载TSS
mov ax, SelectorTss
ltr ax
在ring2的程序中,我们就可以使用call这个调用门就可以又切换到ring0了
[section .ring2]
align 32
[bits 32]
SEG_RING2:
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 10 + 18) * 2
mov ah, 0x0B
mov al, 'R'
mov [gs:edi], ax
add edi, 2
mov al, '2'
mov [gs:edi], ax
call SelectorGate:0
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 10 + 26) * 2
mov ah, 0x0B
mov al, 'R'
mov [gs:edi], ax
add edi, 2
mov al, '2'
mov [gs:edi], ax
jmp $
RING2_LEN equ $ - SEG_RING2 - 1
[section .ring0]
align 32
[bits 32]
SEG_RING0:
mov ax, SelectorVideo
mov gs, ax
mov edi, (80 * 10 + 22) * 2
mov ah, 0x0B
mov al, 'R'
mov [gs:edi], ax
add edi, 2
mov al, '3'
mov [gs:edi], ax
retf
RING0_LEN equ $ - SEG_RING0 - 1
setup.asm的全部代码