鱼C论坛

 找回密码
 立即注册
查看: 3663|回复: 3

[学习笔记] X86汇编语言-从实模式到保护模式—笔记(44)-第14章 任务和特权级保护(10)

[复制链接]
发表于 2017-12-24 19:51:06 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 兰陵月 于 2018-1-14 10:02 编辑

【14.4.9 创建任务状态段TSS】
下面创建任务状态段TSS。第684~688行,申请104字节的内存用于创建TSS。很显然,我们是要创建一个标准大小的TSS。照例,要把TSS的基地址和界限登记到任务控制块TCB中,将来创建TSS描述符时用得着。TSS的界限值是16位的,是它的大小(总字节数减1),这就是第686行的目的。第687行,调用过程allocate_memory分配一个104字节长的内存空间,给TSS使用,过程返回TSS的线性基地址;第688行,将TSS的线性基地址登记到TCB中。注意,界限值必须至少是103,任何小于该值的TSS,在执行任务切换时,都会引发处理器异常中断。
第691行开始,填充登记TSS中的内容。第691行,寄存器ECX中的值为TSS的线性基地址,ES:ECX为TSS在0~4GB段中的起始地址,ES:ECX+0表示位移0处,填充0到此处,长度为16位,该处为前一个任务的指针,这里填入0,表明这是唯一的任务。【为什么这个链接域只有16位?链接字段安排在TSS内偏移0开始的双字中,其高16位未用。在起链接作用时,低16位保存前一任务的TSS描述符的选择子。附外:如果当前的任务由段间调用指令CALL或中断/异常而激活,那么链接字段保存被挂起任务的TSS的选择子,并且标志寄存器EFLAGS中的NT位被置1,使链接字段有效。在返回时,由于NT标志位为1,返回指令RET或中断返回指令IRET将使得控制沿链接字段所指恢复到链上的前一个任务。】
第693~709行,登记0、1和2特权级栈的段选择子,以及它们的初始栈指针。所有的栈信息都在TCB中,先从TCB中取出,然后填写到TSS中的相应位置。第711、712行,登记当前任务的LDT描述符选择子。在任务切换时,处理器需要用这里的信息找到当前任务的LDT。LDT对任务来说并不是必需的,如果高兴,也可以把属于某个任务的段定义在GDT中。如果没有LDT,这里应该填写0。第714、715行,填写I/O许可位映射区的地址。在这里,填写的是TSS段界限(103),这意味着不存在该区域。第717行,置调试位T位值为“0”。

【14.4.10 安装TSS描述符到GDT中】
和局部描述符表(LDT)一样,也必须在GDT中安装TSS的描述符。这样做,一方面是为了对TSS进行段和特权级的检查;另一方面,也是执行任务切换的需要。当call far和jmp far指令的操作数是TSS描述符选择子时,处理器执行任务切换操作。图14-19所示,这是TSS描述符的格式,和LDT描述符差不多,除了TYPR位。
14-019.png
TSS描述符中的B位是“忙”位(Busy)。在任务刚刚创建的时候,它应该为二进制的1001,即,B位是“0”,表明任务不忙。当任务开始执行时,或者处于挂起状态(临时被中断执行)时,由处理器固件把B位置“1”。
任务是不可重入的。就是说,在多任务环境中,如果一个任务是当前任务,它可以切换到其他任务,但不能从自己切换到自己。在TSS描述符中设置B位,并由处理器固件进行管理,可以防止这种情况的发生。
第720~725行,先调用公共例程段内的过程make_seg_descriptor创建TSS描述符,它需要传入三个参数。先从TCB中取出TSS的基地址,传送到EAX寄存器;EBX寄存器的内容是TSS的界限;ECX寄存器的内容是描述符属性值,0x00408900表明这是一个DPL为0的TSS描述符,字节粒度。接着,调用公共例程段内的另一个过程set_up_gdt_descriptor安装此描述符到GDT中,并将返回的描述符选择子登记在TCB中。TSS描述符选择子的RPL字段为0。

【14.4.11 带参数的过程返回指令】
至此,任务创建完毕,可以从过程load_relocate_program返回了。
在过程返回之前,即,在执行ret指令之前,需要恢复现场,也就是按相反的顺序将刚进入过程时压入栈的内容出栈。这是第727~730行的工作。
如图14-20所示,当执行ret指令时,栈恢复到刚进入过程时的状态,即,只有返回地址和调用者传递给过程的参数。因为当初是采用32位相对近调用进入过程load_relocate_program的,故仅将EIP压栈,没有压入段寄存器CS的内容。
14-020.png
再来看,一旦ret指令执行完毕,控制将返回到调用者,且栈中只剩下两个参数。按道理,这两个参数是由调用者压入的,应该再由调用者弹出即可。以本程序为例,在call后加一句“add esp,8”,这样就可以平衡参数。
不过,最好的解决办法是在过程返回时,顺便弹出参数。这样做是可行的,过程的编写者最清楚栈中有几个参数。如果希望过程在返回时弹出参数,使ESP寄存器指向调用过程前的栈位置(使栈平衡),可以使用带操作数的过程返回指令:
ret imm16
retf imm16
这两条指令都允许16位的立即数作为操作数,不同之处仅仅在于,前者是仅返回,后者是远返回。立即数是16位的,而且一般总是偶数,原因是栈操作总是以字或者双字进行,它指示在将控制返回到调用者之前,应当从栈中弹出多少字节的数据。
因此,第732行,当该指令执行时,除了将控制返回到过程的调用者之外,还要调整栈的指针,即ESP←ESP+8,之所以指令的操作数是8,是因为要弹出2个双字。
这条指令给高级语言带来的好处是增加了它们的复杂性。比如这样一个C语言函数:
Void func(int i,char *c){
/* 这里是函数体 */
}
因为一般是通过栈传递参数,所以,哪个参数先入栈,哪个后入栈,栈平衡的事情由调用者来做,还是由过程来做,就需要一个标准,即所谓的调用转换规则。特别是在开发一些大软件时,需要用不同的高级语言来开发各个独立的、但能够协同工作的模块,尤其需要注意这个问题。
一个典型的调用转换标准是stdcall,它规定,参数从右往左进栈,且由过程在返回前出栈。

14.5  用户程序的执行

【14.5.1 通过调用门转移控制的完整过程】
现在我们转到代码清单14-1的第845、846行,在调用过程load_relocate_program创建任务之后,显示一条成功的信息。第848、849行,令DS指向4GB数据段。
接下来的工作是将控制转移到用户程序那里。我们创建的是一个3特权级的任务,所以这是一个从0特权级到3特权级的控制转移。或者,换一种更体面的说法,是从任务自己的0特权级全局空间转移到3特权级局部空间执行。通常情况下,这既不允许,也不太可能。
办法总还是有的,只不过稍微有一点曲折,那就是假装从调用门返回。先来看看完整的调用门控制转移和返回过程是怎样的。
首先,通过调用门实施控制转移,可以使用jmp far和call far指令。指令执行时,描述符选择子必须指向调用门,32位偏移量被忽略。但,无论采用哪种控制转移指令,都使用表14-1的特权检查规则。注意,表中的比较关系都是数值上的。
14-020-1.png
从上表中可以看出,当使用jmp far指令通过调用门转移控制时,要求当前特权级和目标代码段的特权级相同。原因是jmp far指令通过调用门转移控制时,不改变当前特权级CPL。(如果是转移到依从的代码段,则可以从低特权级转移到高特权级,但是同样也不改变当前特权级CPL,也就是说,高特权级的代码在调用者的特权级下运行,同样也因为这个原因,所以才叫“依从的”代码段。)
相反,使用call far指令可以通过调用门将控制转移到较高特权级别的代码段。之所以说“可以”,是因为,如果目标代码段是依从的,则和jmp far指令一样,不改变当前特权级别;否则,如果目标代码是非依从的,则在目标代码段的特权级别上执行。
其次,当使用call far指令通过调用门转移控制时,如果改变了当前的特权级别,则必须切换栈。即,从当前任务的固有栈切换到与目标代码特权级相同的栈上。栈的切换是由处理器固件自动进行的。
当前栈是由段寄存器SS和栈指针寄存器ESP的当前内容指示的;要切换到的新栈位于当期任务的TSS中,处理器知道如何找到它。在栈切换前,处理器要检查新栈是否有足够的空间完成本次控制转移。栈切换过程如下:
(1)使用目标代码段的DPL(也就是新的CPL)到当前任务的TSS中选择一个栈,包括栈段选择子和栈指针。
(2)从TSS中读取所选择的段选择子和栈指针,并用该选择子读取栈段描述符。在此期间,任何违反段界限检查的行为都将引发处理器异常中断(无效TSS)。
(3)检查栈段描述符的特权级和类型,并可能引发处理器异常中断(无效TSS)。
(4)临时保存当前栈段寄存器SS和栈指针ESP的内容。
(5)把新的栈段选择子和栈指针代入SS和ESP寄存器,切换到新栈。
(6)将刚才临时保存的SS和ESP的内容压入当前栈,如下图图14-21。
14-021.png
(7)依据调用门描述符“参数个数”字段的指示,从旧栈中将所有参数都复制到新栈中。如果参数个数为0,不复制参数,如上图。
(8)将当前段寄存器CS和指令指针寄存器EIP的内容压入新栈,如上图。通过调用门实施的控制转移一定是远转移,所以要压入CS和EIP。
(9)从调用门描述符中依次将目标代码段选择子和段内偏移传送到CS和EIP寄存器,开始执行被调用过程。
相反,如果没有改变特权级别,则不切换栈,继续使用调用者的当前栈,只在原来的基础上压入当前段寄存器CS和指令指针寄存器EIP的内容,如下图图14-22所示。
14-022.png
在该例程内,操作系统可能会访问自己的0特权级数据段以进行某些内部操作。当然,它也必须先执行将选择子代入段寄存器的操作:
mov ds,ax                                ;操作系统自己的选择子
按道理,安全的做法是先将旧的DS值压栈,用完后再出栈。像这样:
push ds
mov ds,ax
……
pop ds
retf
但是,如果操作系统例程没有这么做,一定有它的道理,而处理器也无权干涉。唯一可以预料的是,当控制返回到应用程序时,段寄存器DS依然指向操作系统数据段,因此,应用程序就可以直接在3特权级下访问操作系统的数据段:
mov edx,[0x000C]
这是因为,特权级检查只在引用一个段的时候进行。即,只在将选择子传送到段寄存器的时候进行。只要通过了这一关,后面那些使用这个段寄存器的内存访问就都是合法的。
为了解决这个问题,在执行retf指令时,要检查数据段寄存器,根据它们找到相应的段描述符。要是有任何一个段描述符的DPL高于调用者的特权级(返回后的新CPL),那么,处理器将把数值0传送到该段寄存器。使用这样的段寄存器访问内存,会引发处理器异常中断。
特别需要注意的是,任务状态段TSS中的SS0、EIP0、SS1、EIP1、SS2、EIP2域是静态的,除非软件进行修改,否则处理器从来不会改变它们。举个例子,当处理器通过调用门进入0特权级的代码段时,会切换到0特权级栈。返回时,并不把0特权级栈指针的内容更新到TSS中的ESP0域。下次再次通过调用门进入0特权级代码段时,使用的依然是ESP0的静态值,从来不会改变。这就是说,如果你希望通过0特权级栈返回数据,就必须自己来做这件事,比如,在返回到低特权级别的代码段之前,手工改写TSS中的ESP0域。
【14.5.2 进入3特权级的用户程序的执行】
回到代码清单14-1中。任务寄存器TR总是指向当前的任务状态段(TSS),而LDTR寄存器也总是指向当前任务的LDT。TSS是任务的主要标志,因此要使TR寄存器指向任务;而使用LDTR的原因是可以在任务执行期间加速段的访问。
在多任务环境中,随着任务的切换,每当一个任务开始运行时(称为前台活动任务),TR和LDT寄存器的内容都会更新,以指向新的当前任务。
现在的问题是,我们只有一个任务,而且是个3特权级的任务,不能用任务切换的方法使它开始运行。这个问题可以表述为:如何从任务的0特权级全局空间转移到它自己的3特权级空间正常执行?
答案是先确立身份,即,使TR和LDTR寄存器指向这个任务,然后假装从调用门返回。和当前任务有关的信息都在它的任务控制块(TCB)中。因此,第848、849行,先令段寄存器DS指向4GB的内存段。第851、852行,加载任务寄存器TR和局部描述符表寄存器(LDTR)。
14-023.png
如图14-23所示,TR和LDTR寄存器都包括了16位的选择器部分,以及段描述符高速缓存器部分。选择器部分的内容是TR和LDT描述符的选择子;描述符高速缓存器部分的内容则指向当前任务的TSS和LDT,以加速这两个段(表)的访问。
加载任务寄存器TR需要使用ltr指令。这条指令的格式为
ltr r/m16
这条指令的操作数可以是16位通用寄存器,也可以是指向一个16位单元的内存地址。但不管是寄存器还是内存单元,其内容都是16位的TSS选择子。
在将TSS选择子加载到TR寄存器之后,处理器用该选择子访问GDT中对应的TSS描述符,将段界限和段基址加载到任务寄存器TR的描述符高速缓存器部分。同时,处理器将该TSS描述符中的B位置“1”,也就是标志为“忙”,但并不执行任务切换。
该指令不影响EFLAGS寄存器的任何标志,但属于只能在0特权级下执行的特权指令。
加载局部描述符表寄存器LDTR使用的是lldt指令,其格式和ltr是一样的:
lldt r/m16
其操作数也和ltr指令一样,但是,指向的是16位LDT选择子。ltr和lldt指令执行时,处理器首先要检查描述符的有效性,包括审查它是不是TSS或者LDT描述符。在将LDT选择子加载到LDTR寄存器之后,处理器用该选择子访问GDT中对应的LDT描述符,将段界限和段基地址加载到LDTR的描述符高速缓存器部分。CS、SS、DS、ES、FS和GS寄存器的当前内容不受该指令的影响,包括TSS中的LDT选择子字段。
如果执行这条指令时,代入LDTR选择器的选择子,其高14位是全零,LDTR寄存器的内容被标记为无效,而该指令的执行也将不声不响地结束(即不会引发异常中断)。当然,后续那些引用LDT的指令都将引发处理器异常中断(对描述符进行校验的指令除外),例如,将一个指向LDT的段选择子代入段寄存器。
最后,如图14-24所示,这是一个任务的全景图,给出了与一个任务相关的各个组成部分。
14-024.png
注意了,现在,局部描述符表(LDT)已经生效,可以通过它访问用户程序的私有内存段了。
第854、855行,访问任务的TCB,从中取出用户程序头部段选择子,并传送到段寄存器DS。该选择子RPL字段的值为3,即,请求特权级为3;TI位是“1”,指向任务自己的LDT。这两条指令执行后,段寄存器DS就指向用户程序头部段。
第858~862行,从用户程序头部内取出栈段选择子和栈指针,以及代码段选择子和入口点,并将它们顺序压入当前的0特权级栈中。这部分内容要结合第13章的用户程序头部来分析。
第864行,执行一个远返回指令retf,假装从调用门返回。于是控制转移到用户程序的3特权级代码开始执行。注意,这里所用的0特权级并非是来自于TSS。不过,处理器不会在意这个。下次,从3特权级的段再次来到0特权级执行时,就会用到TSS中的0特权级栈了。
用户程序现在是工作在它的局部空间里。它可以通过调用门请求系统服务来显示字符串,或者读取硬盘数据,这都没有问题。
创建任务的基本过程:
1、主引导程序能够正常运行
(1)设置好与主引导程序有关的段描述符,创建GDT表,并加载它。
(2)加载内核,并初始化内核
(3)转到内核执行
2、运行到内核程序,做好相关准备工作
(1)在内核数据段的相关位置安装好调用门
(2)安装好其他数据
3、加载用户程序
(1)为了记录任务的相关信息,先设计一个任务控制块TCB,用于保存创建任务前配置好的相关信息
(2)当用户程序被读入内存,并处于运行或者等待运行的状态时,就视为一个任务。任务有自己的代码段和数据段(包括栈),这些段必须通过描述符来引用,而这些描述符可以放在GDT中,也可以放在任务自己的LDT中,但最好是放在LDT中。GDT用于存放各个任务公有的描述符,比如公共的数据段和公共例程。
(3)分配一块内存,作为LDT来用,为创建用户程序各个段的描述符做准备;
(4)将LDT的大小和起始线性地址登记在任务控制块TCB中;
(5)分配内存并加载用户程序,并将它的大小和起始线性地址登记到TCB中;
(6)创建局部描述符表
(7)重定位用户程序中的U-SALT表
(8)创建0、1、2特权级的栈
(9)安装LDT描述符到GDT中
(10)创建任务状态段TSS
(11)安装TSS描述符到GDT中
(12)假装从调用门返回;
假装从调用门返回之后,处理器就进入了用户程序局部空间里了。

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2017-12-29 15:06:59 | 显示全部楼层
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2017-12-29 15:08:09 | 显示全部楼层
强烈支持朋友i㎎...
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2017-12-29 18:57:34 | 显示全部楼层
mmdn 发表于 2017-12-29 15:08
强烈支持朋友i㎎...

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-11-22 06:06

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表