文章目录
01、特权级保护的必要性和机制
用描述符实施段与段之间的隔离和保护,建立在程序之间分工协作的基础上,首先用户程序不能独立运行需要在内核的支持下运行。
内核需要加载和重定位用户程序、为用户程序的每个段创建描述符,将段选择子回填到用户程序的头部中,因为这个原因用户程序只能访问自己的代码段、数据段和栈段。
当然这样并不能有效的防止用户程序访问GDT,如下程序:
。。。
mov eax, 0x28 ;00101_0_00
mov ds, eax
mov dword [0], 012345678
。。。
用户程序虽然不知道5号描述符指向哪个段,但是仍然可以破坏段中的数据,甚至用户程序重新定义一个GDT
来替换内核建立的GDT
,从而达到破坏的目的。
还有用户程序只要知道内核中例程的段选择子
和段内偏移
就可以调用例程执行,这可以破坏内核。
系统的多任务如下:
使用特权级来划分内核和用户程序、任务的共有部分和私有部分之间的隔离,0高3低
。
特权指令:只有0特权级
的程序能够执行
02、当前特权级CPL
特权级是以处理器的工作特点和工作方法进行划分的。
处理器不知道当前执行的是哪个程序,但是可以知道是哪个段:
程序的特权级就是:组成这个程序的所有代码段的特权级。
处理器正在执行哪个代码段,其特权级就是当前特权级CPL
(Current Priviledge Level
)。也即是当前正在执行的程序的特权级。
哪么在哪里体现CPL呢?段寄存器如下:
段选择器:低两位保存CPL
。
也有例外,如下:
。。。
mov cr0, eax ;设置PE位
jmp 0x000:flush ;进入保护模式
。。。
当设置CR0
的PE
位进入保护模式之后,处理器自动处于0特权级,但是这个特权级无法使用CS
的段选择器来指示,因为CS的段选择器中仍然保存着实模式下的逻辑段地址,而不是段选择子。只有执行了jmp
指令之后,CS
才会被刷新,用来指示当前特权级。
在引入保护模式和特权级之后,实模式被赋予新的内涵,实模式下的程序始终是0特权级
的,在进入保护模式之后,处理器只不过是继承额实模式的0特权级
。
03、描述符特权级DPL
描述符特权级DPL(Descriptor Privilege Level)
:
描述符特权级用来描述指定的实体
的特权级,即描述符描述的是一个段,那么DPL就是这个段特权级。对于一个正在执行的代码段,其CPL
和当前的DPL
一致。
程序是在不同代码段内执行,那么CPL
就是目标代码段的DPL
。即控制转移只能发生在两个特权级相同的代码段之间。如下,CS
指向代码段A
、CPL=0
;代码段B
的DPL
必须是0才能从代码段A
跳转到代码段B
内执行。
当然也可以从一个低特权级的代码段
转移到一个高特权级的代码段
内执行,需要特殊的方法。但是无论如何都不能从一个高特权级的代码段
转移到一个低特权级的代码段
内执行。
访问数据时的特权级:
在当前代码段内访问数据段,那么当前代码段的特权级
必须大于等于
那个目标数据段的特权级
,即CPL <= DPL
。代码段特权级别低的话表示其可靠性和安全性不高,不能允许其访问高特权级的数据。
上一章代码中,从引导程序跳转执行内核程序
时,两者特权级均为0,可以跳转。从内核程序跳转执行用户程序
时,两者特权级均为0,可以跳转。其中执行ltr、lldt
这些只能在0特权级执行的指令也是合法的。
04、任务公共部分和私有部分的特权级划分
一个任务由内核
与用户程序
共同组成,当内核为用户程序创建描述符时,将用户程序的特权级设置为3,而内核部分特权级为0。
在代码中:原先建立描述符时,特权级为0。
。。。
;建立程序头部段描述符
mov eax,edi ;程序头部起始线性地址
mov ebx,[edi+0x04] ;段长度
dec ebx ;段界限
;mov ecx,0x00409200 ;字节粒度的数据段描述符,特权级0
mov ecx,0x0040F200 ;字节粒度的数据段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
。。。
同上,将代码段、数据段栈段均改为特权级3。
但是只修改特权级之后程序将不能正常运行,因为当前内核CPL是0,用户程序DPL是3,不允许使用jmp
指令从高特权级向低特权级执行。就算能进入用户程序,则当前CPL为3,在用户程序中调用内核中的例程也不被允许,因为内核DPL为0,不能从低特权级代码段向高特权级代码段执行。
调试程序,在内核跳转执行用户程序时停下:
显示特权级检查没有通过。
05、依从的代码段
处理器原则上不允许在两个不同特权级代码段之间转移。但是符合一定条件是可以的。
方法一:将目标代码段设置为依从的代码段,依从的代码段
中即使其特权级较高,也可从低特权级代码段进入。
S位
:为1
表示寄存器的段描述符X位
:为1
表示代码段C位
:为0
表示普通的代码段、为1
表示依从的代码段,一个代码段是依从的表示可以从低特权级代码段进入、但是不能从高特权级代码段进入。从数值上有当前代码段CPL
>=依从代码段DPL
。
上图中右边CPL=3
可以转移到左边DPL=2
的代码段执行。转移之后程序是在CPL=3的特权级上执行,而不是在依从的DPL=2上执行。
06、门描述符和调用门
接上一节。
方法 二:通过门实施转移
门描述符描述的是一些系统管理单元,比如描述一个任务、描述一个例程(子程序)。
门描述符描述的是一个例程,称为调用门
。
如果高特权级是依从的,可以从低特权级
向高特权级
代码段执行;
若不是依从的,则通过调用门也可从低特权级
向高特权级
代码段执行;
调用门格式:
S位
:为0
表示系统描述符;TYPE位
:为1100
表示调用门、描述的是一个例程(子程序);P位
:为0
表示调用门无效、为1表示有效(调用这样的门会导致处理器产生异常中断);DPL位
:表示调用门本身的特权级,表示谁有权通过调用门实施控制转移;高32位的4~0
:保存用栈传递的参数个数,最多2^5 - 1 = 31
个。
调用门涉及三个部分:
一是:当前代码段特权级CPL
二是:调用门描述符的DPL
三是:目标代码段描述符的DPL
要想穿过调用门,则在数值上
有目标代码段描述符的DPL
<=当前代码段特权级CPL
<= 调用门描述符的DPL
。
上图,当前代码段特权级CPL为3
时不能通过调用门,为1和2
时能通过,为0
时不能通过。
- 通过
CALL
指令通过调用门之后还可以返回
,通过之后处理器在目标代码段的特权级上执行,即CPL
由低到高。 - 通过
JMP
指令通过调用门之后不可以返回
,通过之后处理器在原来的特权级上执行,即CPL
不变。
调用一个例程时,可以使用寄存器和栈传递参数,调用者通过调用门之后将参数压栈,返回时从栈中返回参数。
但是通过调用门之后特权级可能改变当前特权级CPL
,从低特权级变为高特权级。此时处理器要求栈也必须切换,从低特权级的栈切换到高特权级的栈,还要复制参数,为了防止栈的原因出错。为了知道需要复制几个参数,调用门描述符中需要保存参数的数量,保存在高32位的位4~0
中,即2^5 - 1 = 31个
。
07、本章程序说明和特权级检查的时机
本章程序有:
主引导程序:c13_mbr0.asm
内核程序:c14_core2.asm
用户程序:c13_app1.asm
进入保护模式,之后就要进行特权级指令检查,jmp far
指令进入内核执行,需要进行特权级别检查。
1、上图中下面两行不需要进行特权级检查,虽然需要访问段中的数据,但是在访问之前需要指定段的位置。
2、就如前两行代码,将一个段选择子传入段寄存器DS
时,要检查当前特权级CPL
是否高于等于目标数据段描述符的DPL
,即数值上当前CPL<=目标数据段DPL
。
3、若通过检查表示DS
会被加载,那么后续的内存访问指令都是合法的,若没有通过检查则DS不可能成功加载,后续指令就没有机会成功执行。
4、特权级检查的典型时机如下:其中特权指令只能在0特权级
下执行。
本章程序可以从0特权级
的内核进入3特权级
的用户程序执行,也可以在3特权级
的用户程序中使用0特权级
的接口例程。
08、请求特权级RPL
本章程序:
1、用户程序中CPL=3
,调用门的DPL=3
,内核代码段得DPL=0
,满足条件,可以执行转移。转移只有处理器以CPL=0
执行。
2、在内核的硬盘读写例程中 将数据段的选择子传送给DS
,需要进行特权级检查,由高特权级的代码段可以访问同级或低级的数据段,从数值上即当前代码段CPL<=目标数据段DPL
。在本程序中硬盘读写例程中CPL=0
<= 用户程序数据段的DPL=3
,满足条件通过检查。
若用户程序通过调用门执行内核的硬盘读写例程时传递的数据段选择子
是内核的数据段选择子
,那么在将数据段选择子传递给DS
时也可以通过检查,那么用户程序可以通过调用门破坏内核数据段了。
表明只是依靠当前代码段特权级CPL
和目标数据段描述符特权级DPL
进行特权级检查是不充分的。
在这里要访问数据段的是用户程序,用户程序自己不能访问外部设备,它请求内核硬盘读写例程代替自己访问一个数据段,在内核硬盘读写例程中当前CPL=0
,之前是3,说明真实请求者的信息被隐藏了。
如果能够恢复请求者的身份,知道他是3特权级的用户程序,自然就知道它不能访问0特权级的内核数据段,将数据段选择子的传送到DS
的指令也就不可能执行。这个问题处理器不能解决。
在访问一个数据段之前,需要将段选择子传送到DS
的段选择器,在进行这个操作时选哟特权级检查。段选择子:
其中RPL
(Request Privilege Level
)表示请求者的特权级,那么检查如下:
对于RPL
的检查已经内置到处理器中,由处理器固件完成的,是一个例行的操作。因此当程序员意识到请求者不是当前程序或当前代码段,而是一个低特权级的程序,那么在硬盘读写例程中有:
此时就可检查出请求者的特权级RPL
低于内核数据段特权级DPL
,在数值上有RPL=3 >= DPL=0
,不满足条件,指令终止,处理器产生一个异常中断。
09、请求特权级调整指令ARPL
在程序中:
;-------------------------------------------------------------------------------
;此例程用于说明如何通过请求特权级RPL解决因请求者身份与CPL不同而带来的安全问题
read_hard_disk_with_gate: ;从硬盘读取一个逻辑扇区
;输入:PUSH 逻辑扇区号
; PUSH 目标缓冲区所在段的选择子
; PUSH 目标缓冲区在段内的偏移量
;返回:无
push eax
push ebx
push ecx
mov ax,[esp+0x10] ;获取调用者的CS
arpl [esp+0x18],ax ;将数据段选择子调整到真实的请求特权级别
mov ds,[esp+0x18] ;用真实的段选择子加载段寄存器DS
mov eax,[esp+0x1c] ;从栈中取得逻辑扇区号
mov ebx,[esp+0x14] ;从栈中取得缓冲区在段内的偏移量
;此部分的功能是读硬盘,并传送到缓冲区,予以省略。
retf 12
假定已经为这个例程创建调用门,调用门的特权级是3,这个例程可以从特权级3的用户程序调用。
在CS
的低2位
就是进入当前例程前请求者的特权级,将其取出传送给数据段选择子的RPL
字段,即可修改请求者的特权级。使用ARPL
指令修改。
ARPL
指令比较两个操作数的低2位,若目的操作数RPL < 源操作数RPL
,则修改目的操作数RPL
,使其与源操作数RPL
保持一致,同时标志寄存器的0标志位ZF置1
。否则不改变目的操作数的RPL
,标志寄存器的0标志位ZF清0
。
程序最后,指令retf 12
表示由被调用者来调整栈平衡,传入3个参数,一个参数4个字节,共12个字节。
10、一般情况下的请求特权级设置
绝大多数时刻,请求者就是当前代码段或者当前程序,此时只需要将段选择子
的RPL
字段设置成当前特权级CPL
就可以了。在c13_mbr0.asm
程序中:
。。。
;保护模式选择子为:0000000000010_0_00
; 15 3: 2 : 10
; 描述符索引号: TI位: 特权级RPL
;CS的值就会被改变,指向代码段
;使用CS,将'10'号选择子送入CS,TI为0表示在GDT中,使用索引号 X 8到GDT表中取出对应的描述符
;将其中的段基地址部分送入 CS的描述符高速缓存寄存器中,之后使用这个 基地址 + 偏移地址(flush) = 逻辑地址
;处理器跳转到这个 目标的逻辑(物理)地址 处执行程序
;同时执行完这条指令,处理器会清空流水线,使得之前使用16位操作指令执行译码的指令清空,接下来使用32位
jmp 0x0010:flush ;清流水线并串行化处理器,段间直接绝对跳转
;进入保护模式就要进行特权级别检查
。。。
1、此时当前特权级CPL为0
,是从实模式继承来的;
2、请求特权级RPL
位与选择子0x0010 = 0000000000010_0_00
中,其中设置的请求特权级RPL
为00
;
3、转移的目标位置是初始代码段,其中DPL
为00
。
因此在此条指令执行时,CPL = RPL = DPL
,可以通过特权级检查。
进入保护模式设置数据段:
。。。
mov eax, 0x0008 ;0x0008 = 0000000000001_0_00
mov ds, eax
。。。
1、此时CPL = 0
;
2、请求特权级在选择子0x0008
,即请求特权级RPL = 0
;
3、目标数据段是4G字节数据段,其描述符中DPL = 0
;
能够通过特权级检查。
接下来设置栈段:
。。。
mov eax,0x0018 ;加载堆栈段选择子 11:00011_0_00
mov ss,eax
xor esp,esp ;堆栈指针 <- 0
。。。
1、此时CPL = 0
;
2、请求特权级在选择子0x0018
,即请求特权级RPL = 0
;
3、目标数据段是栈段,其描述符中DPL = 0
;
能够通过特权级检查。
11、为内核接口例程创建调用门
进入内核之后使用指令call sys_routing_seg_sel:put_string
在内核中转移是允许的,因为内核公共例程段的特权级
和内核代码段的特权级
相同,都是0特权级
,可以直接调用。但是用户程序的特权级是3
,所以要为那些供用户程序使用的例程创建调用门。
在内核的核心数据段中定义了符号地址检索表,其中有例程的名字、例程所在段内偏移、例程所在段的选择子。现在分别为这些例程创建调用门,并且把他们的例程所在段选择子
改为调用门选择子
。
创建调用门的代码如下:
。。。
;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
mov edi,salt ;C-SALT表的起始位置
mov ecx,salt_items ;C-SALT表的条目数量
.b3:
push ecx
mov eax,[edi+256] ;该条目入口点的32位偏移地址
mov bx,[edi+260] ;该条目入口点的段选择子
mov cx,1_11_0_1100_000_00000B ;特权级3的调用门(3以上的特权级才
;允许访问),0个参数(因为用寄存器
;传递参数,而没有用栈)
call sys_routine_seg_sel:make_gate_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+260],cx ;将返回的门描述符选择子回填,此时默认RPL=0
add edi,salt_item_len ;指向下一个C-SALT条目
pop ecx
loop .b3
。。。
其中调用们的描述符:
安装调用门之后GDT布局:
12、调用门测试和调用门转移过程
接上一节,本节对代码段进行测试:
...
;对门进行测试
mov ebx,message_2
call far [salt_1+256] ;通过门显示信息(偏移量将被忽略)
;此时DS指向内核数据段
mov ebx,message_3
call sys_routine_seg_sel:put_string ;在内核中调用例程不需要通过门
...
- 1、DS指向内内核数据段,偏移
salt_1+256
指向一个地址,地址处存放的是一个偏移和一个选择子; - 2、处理器使用选择子到GDT中取出描述符,发现是一个调用门描述符,包含一个代码段选择子和段内偏移;
- 3、处理器将代码段选择子传送到CS的选择器部分,将段内偏移传送到EIP;
- 4、处理器使用CS选择器中的选择子访问GDT,将取出的描述符存放到CS描述符高速缓存器;
- 5、处理器使用CS描述符高速缓存器 加上 EIP中的偏移 转移到目标例程开始执行。
在这里salt_1+256
地址处指定的偏移量和选择子中,只是使用了选择子部分。即在通过调用门实施控制转移时,在指令中提供的偏移量会被忽略。 但是在指令中偏移量还是要加上。如下:
若0x0030
指定的选择子是一个调用门,那么指令中指定的偏移量0x0000C000
不会被使用,但是在书写时不能不写,但是可以写一个任意值。
通过调用门实施控制转移时的特权级检查。
13、通过调用门实施由低到高特权级的转移
接上一节,本节具体看代码c14_core2.asm
。
其中用户程序调用内核例程和过程:
特权级检查:
14、通过调用门转移控制时的栈切换过程
用CALL
指令通过调用门实施控制转移可以改变程序的当前特权级CPL
。比如从特权级3的用户程序用CALL
指令通过调用门进入特权级0的内核之后,当前特权级CPL
也会送3变成0。
问题在于通过调用门转移的时候,栈的切换时处理器自动进行的,处理器如何知道该切换到哪一个栈呢,如何知道栈在哪里呢?
因为通过调用门转移时,是从低特权级转移到高特权级中,特权级3是最低的,不可能从更低的特权级转移到特权级3中,所以TSS中也就不需要存储特权级3的栈段选择子和栈指针。
在用户程序中:
。。。
mov eax,100 ;逻辑扇区号100
mov ebx,buffer ;缓冲区偏移地址
call far [fs:ReadDiskData] ;段间调用
。。。
call far
指令执行时,当前CPL=3
,目标例程位于内核的公共例程段,内核的公共例程段的描述符特权级DPL是0
,因此进入目标例程时需要对栈进行切换。
切换之前的栈是用户程序自己的栈。若转移之前需要通过栈传递参数则存在用户程序自己的栈中,此处并未使用栈传递参数,而是通过寄存器传递参数。
1、一旦处理器发现要对栈进行切换,而且目标代码段的特权级是0;
2、就立即到当前任务的TSS
中,取出特权级0的栈段选择子SS0
、和栈指针EIP0
;
3、并分别传送到栈寄存器SS
、栈指针寄存器ESP
。
4、SS
选择器的选择子发生改变,处理器立即到描述符表(GDT/LDT
)中取出描述符;
5、取出后传送到SS
描述符高速缓存器中,至此新的栈段就可以使用了。
切换到新栈之后,处理器立即压入旧栈的SS
和ESP
,这样做是为了将来从调用门返回时,可以返回到用户程序原来的栈中。其中段选择子是16位的,压入时用0扩展至32位。接着处理器从旧栈中将传递的参数(在调用门的描述符中记录了参数的数量)传递到新栈中。在这个程序中使用寄存器传递参数,所以这里的参数部分是没有的。
复制完参数之后,处理器再将控制转移前的CS
和EIP
压栈,这样做是为了能够返回到原来的程序,也就是调用者那里。
栈切换过程是由处理器自动进行的,现在处理器就可以执行目标例程了。
当例程返回时:
从被调用程序的栈中返回SS、ESP、CS、EIP
,如此一来就可以返回到原来的调用者哪里执行原来的程序,并切换会原来的旧栈。
15、通过调用门转移控制并返回的完整描述
通过调用门转移控制并返回的全过程:
使用call far
指令通过调用门转移控制时,如果改变了当前的特权级别则必须切换栈,即从当前任务的固有栈切换到与目标代码段特权级相同的栈上。
栈的切换是由处理器固件自动进行的,当前栈是由SS
和ESP
的当前内容指示的,要切换到的新栈位于当前任务的TSS
中,处理器知道如何找到它。在栈切换前处理器与要检查新栈是否有足够的空间完成本次控制转移。
控制转移和栈切换的过程如下:
如果通过调用门的控制转移是使用jmp far
指令发起的,则转移后不在返回,而且没有特权级的变化,也不需要切换栈;
相反如果通过调用门的控制转移是使用call far
指令发起的,那么可以使用远转移指令retf
返回到调用者。
返回时,处理器从栈中弹出调用者的代码段选择子
和指令指针
,不管是从相同的特权级返回还是从不同的特权级返回,为了安全处理器都会进行特权级检查,控制返回的全部过程如下:
特权级检查不是在实际访问时进行的,而是在将选择子带入段寄存器时进行的,因此当控制从低特权级的程序通过调用门进入高特权级的程序之后,加入高特权级的程序使用指令mov ds,...
将一个高特权级的数据段选择子带入ds
时,如果能够通过特权级检查是没有问题的。
在返回低特权级的程序之后,低特权级的程序依然能够使用指令mov [0],...
来访问高特权级的数据段,儿不会进行特权级检查,这是很危险的。为了解决这个问题在执行retf
指令时,处理器要检查数据段寄存器,根据他们找到相应的段描述符,要是有任意一个段描述符的DPL高于调用者的特权级也就是返回后的新CPL,那么处理器将会把数值0传送到该段寄存器(0是一个特殊的段选择子,处理器允许带入而且不会引发异常),但是后续使用这样的段寄存器访问内存一定会引发处理器异常中断。
TSS
中的SS0、ESP0
等是静态的,除非软件修改它们,处理是不会修改它们的。当处理器通过调用门进入特权级0
的代码段时,处理器会使用SS0
和ESP0
切换到特权级0
的栈段,在此之后栈指针可能会因为压栈出栈而改变,但是返回时并不会更新到TSS
中的ESP0
,下次通过调用门进入特权级0
的代码段时,用的依然是静态的ESP0
的值。
16、创建0、1、2特权级的栈并登记在TSS中
上一节介绍了通过调用门转移控制和返回的全过程,据此知道必须要创建特权级为0、1、2的栈段
,并登记在当前任务的任务状态段TSS
中,对于每一个栈包括栈的线性基地址、栈段选择子、初始栈指针。先把这些信息保存到TCB最后再来填写TSS。
在load_relocate_program
程序中,处理完SALT之后,从栈中取得TCB的线性基地址,之后就可以创建特权级0、1、2的栈段了:
。。。
mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址
;创建0特权级栈
mov ecx,0 ;以4KB为单位的栈段界限值
mov [es:esi+0x1a],ecx ;登记0特权级栈界限到TCB
inc ecx
shl ecx,12 ;乘以4096,得到段大小
push ecx
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x1e],ecx ;登记0特权级栈基地址到TCB
mov eax,ecx
mov ebx,[es:esi+0x1a] ;段长度(界限)
mov ecx,0x00c09200 ;4KB粒度,读写,特权级0
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
;or cx,0000_0000_0000_0000 ;设置选择子的特权级为0
mov [es:esi+0x22],cx ;登记0特权级堆栈选择子到TCB
pop dword [es:esi+0x24] ;登记0特权级堆栈初始ESP到TCB
;对于一个向上扩展的栈;来说,初始栈指针
;应该设置成栈的总大小
。。。
创建0、1、2特权级栈
之后任务控制块TCB
的结构如下:
创建完3个特权级栈
之后,先创建LDT
描述符,并将其安装在GDT
中,之后创建任务状态段TSS
:
。。。
;在GDT中登记LDT描述符
mov eax,[es:esi+0x0c] ;LDT的起始线性地址
movzx ebx,word [es:esi+0x0a] ;LDT段界限
mov ecx,0x00008200 ;LDT描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x10],cx ;登记LDT选择子到TCB中
;创建用户程序的TSS
mov ecx,104 ;tss的基本尺寸
mov [es:esi+0x12],cx
dec word [es:esi+0x12] ;登记TSS界限值到TCB
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx ;登记TSS基地址到TCB
。。。
之后从TCB中取出3个特权级栈的选择子以及初始栈指针,并填写到TSS
中:
。。。
;登记基本的TSS表格内容
mov edx,[es:esi+0x24] ;登记0特权级栈初始ESP
mov [es:ecx+4],edx ;到TSS中
mov dx,[es:esi+0x22] ;登记0特权级栈段选择子
mov [es:ecx+8],dx ;到TSS中
mov edx,[es:esi+0x32] ;登记1特权级栈初始ESP
mov [es:ecx+12],edx ;到TSS中
mov dx,[es:esi+0x30] ;登记1特权级栈段选择子
mov [es:ecx+16],dx ;到TSS中
mov edx,[es:esi+0x40] ;登记2特权级栈初始ESP
mov [es:ecx+20],edx ;到TSS中
mov dx,[es:esi+0x3e] ;登记2特权级栈段选择子
mov [es:ecx+24],dx ;到TSS中
mov dx,[es:esi+0x10] ;登记任务的LDT选择子
mov [es:ecx+96],dx ;到TSS中
mov word [es:ecx+100],0 ;T=0
。。。
之后就是创建TSS
描述符,并在GDT
中等级TSS
描述符:
。。。
;在GDT中登记TSS描述符
mov eax,[es:esi+0x14] ;TSS的起始线性地址
movzx ebx,word [es:esi+0x12] ;段长度(界限)
mov ecx,0x00008900 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x18],cx ;登记TSS选择子到TCB
。。。
之后就是ret 8
指令返回到调用者。
17、通过模拟调用门返回进入用户程序执行
接上一节,返回到内核start
的调用者之后,加载任务寄存器TR
以及局部描述符表寄存器LDTR
:
...
ltr [ecx+0x18] ;加载任务状态段
lldt [ecx+0x10] ;加载LDT
...
这两条指令执行之后表明当前正在一个任务中执行。现在是在任务的全局部分执行,并且应该转移到任务的私有部分(用户程序)执行。
在用户程序中每个段的描述符特权级DPL=3
,以前使用jmp
指令完成转移,但是当前程序特权级CPL=0,从高特权级使用jmp
或call
指令转移到低特权级是不允许的。
通过模拟从调用门返会已进入低特权级的用户程序中执行,在栈中压入以下部分:
代码如下:
...
mov ds,[ecx+0x44] ;切换到用户程序头部段
;使用DS取得数据,之后再回头修改DS
;以下假装是从调用门返回。摹仿处理器压入返回参数
push dword [0x1c] ;调用前的堆栈段选择子
push dword 0 ;调用前的esp
push dword [0x0c] ;调用前的代码段选择子
push dword [0x08] ;调用前的eip
retf
...
之后进入用户程序执行,执行完之后返回;
...
jmp far [fs:TerminateProgram] ;将控制权返回到系统
...
但是jmp
执行时,CPL=3,DPL=0
,条件不成立,不能使用jmp
指令返回,这里改成call far
即可,不过call
指令会在栈中压入返回地址。不过在用户程序终止后,其占用的资源都会被收回,包括栈段,这种情况下,压入数据和不压入数据是一样的。
Virtual Box运行:
Bochs虚拟机: