鱼C论坛

 找回密码
 立即注册
查看: 2702|回复: 0

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

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

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

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

x
【14.4.6 创建0、1和2特权级的栈】
任务在运行时,需要调用内核或者操作系统的例程。这可以认为是从同一个任务的局部地址空间转移到全局地址工作。而且,在这个过程中涉及特权级的变化,需要通过调用门。
通过调用门的控制转移通常会改变当前特权级CPL,同时还要切换到与目标代码段特权级相同的栈。为此,必须为每个任务定义额外的栈。对于当前的3特权级任务来说,应当创建特权级0、1和2的栈。而且,应当将它们定义在每个任务自己的LDT中。
这些额外的栈是动态创建的,而且需要登记在任务状态段TSS中,以便处理器固件能够自动访问到它们。但是,现在的问题是还没有创建TSS,有必要先将栈信息登记在任务控制块TCB中暂时保存。
第622行,从栈中取得TCB的基地址,执行后,寄存器ESI中存放着TCB的基地址的值;第625行~第628行,申请创建0特权级栈所需要的4KB内存,并在TCB中登记该栈的尺寸。登记到TCB中的尺寸值要求是以4KB为单位的,所以,还要逻辑右移12次,相当于除以4096,得到一个4KB的倍数。
第629、630行,先申请内存,然后用申请到的内存基地址加上栈的尺寸,得到栈的高端地址,并将此地址登记到TCB中。一般来说,栈应当使用高端地址作为其线性基地址。
第631~634行,用给定的段界限和段属性调用公共例程段内的过程make_seg_descriptor创建描述符。段属性表明这是一个栈段,4KB粒度。我们创建的是0特权级栈,故要求描述符的DPL为0。
第635、636行,调用内核代码段内的近过程fill_descriptor_in_ldt将刚创建的描述符安装到LDT中。该过程要求使用EBX作为参数提供TCB的线性基地址,故在调用该过程前先将该地址传送到EBX寄存器。
第637~639行,将安装描述符后返回的段选择子登记在TCB中。相应地,应当将该选择子的请求特权级RPL设置为0。注意,过程返回的选择子本来就是RPL为0的,所以那条指令作为注释存在的。同时登记的还有0特权级栈指针的初始值。按老规矩,这个初始值应当为0。
第642~673行是创建1、2特权级的栈,并将它们的信息登记在TCB中,并使用了和上面相同的方法,要注意,为它们分配的特权级别是各不相同的。

【14.4.7 安装LDT描述符到GDT中】
尽管局部描述符表LDT和全局描述符表GDT都用来存放各种描述符,比如段描述符,但这掩盖不了它们也是内存段的事实。简单地说,它们也是段。但是,因为它们用于系统管理,故称为系统的段或系统段。
全局描述符表(GDT)是唯一的,整个系统中只有一个,所以只需要用GDTR寄存器存放其线性基地址和段界限即可;但LDT不同,每个任务一个,所以,为了追踪它们,处理器要求在GDT中安装每个LDT的描述符。当要使用这些LDT时,可以用它们的选择子来访问GDT,将LDT描述符加载LDTR寄存器。在一些人看来,这个理由很牵强,这么做也很别扭。但是,如果不这样,处理器将没有机会来做存储器和特权级的保护工作。
第676~679行,调用公共例程段的过程make_seg_descriptor创建LDT描述符。作为传入的参数,EAX寄存器的内容是从TCB中取出的LDT基地址,EBX寄存器的内容是从TCB中取出的LDT长度,ECX寄存器的内容是描述符的属性,各属性位与它们在描述符高32位中相同,无关的位要清零。如图14-016所示,这是LDT描述符的格式。
14-016.png

LDT本身也是一种特殊的段,最大尺寸是64KB。段基地址指示LDT在内存中的起始地址,段界限指示LDT的范围;描述符的G位是粒度位,适用于LDT描述符,以表示LDT的界限值是以字节为单位,还是以4KB为单位。即使是以4KB为单位,它也不能超过64KB的大小。
D位(或者叫B位)和L位对LDT描述符来说没有意义,固定为0。
AVL和P位的含义和存储器的段描述符相同。
LDT描述符中的S位固定为0,表示系统的段描述符或者门描述符,以相对于存储器的段描述符(S=1),因为LDT描述符属于系统的段描述符。
在描述符为系统的段描述符时,即,在S=0的前提下,TYPE字段为0010(二进制)表明这是一个LDT描述符。
因此,传送到ECX寄存器的属性值0x00408200表示这是一个LDT描述符,描述符特权级DPL为0,其他无关的位都已清零。
过程返回后,创建的描述符在EDX:EAX中。第680、681行,立即调用过程set_up_gdt_descriptor安装此描述符到全局描述符表GDT中。然后,将返回的描述符选择子写入任务控制块TCB的相应位置。

【14.4.8 任务状态段TSS】
到目前为止,任务的所有内存段都已创建完毕,除了任务状态段(TSS)。现在就来创建TSS。在此之前,先来全面了解一下TSS的各个组成部分。
TSS内偏移0处是前一个任务的TSS描述符选择子。和LDT一样,必须在全局描述符表GDT中创建每个TSS的描述符。当系统中有多个任务同时存在时,可以从一个任务切换到另一个任务执行,此时称任务是嵌套的。被嵌套的任务用这个指针指向前一个任务,即嵌套它的那个任务,当控制返回前一个任务时,处理器需要这个指针来识别前一个任务。创建TSS时,可以为0。
SS0、SS1和SS2分别是0、1和2特权级的栈段选择子,ESP0、ESP1和ESP2分别是0、1和2特权级栈的栈顶指针。这些内容应当由任务的创建者填写,且属于填写后一般不变的静态部分,当通过门进行特权级之间的控制转移时,处理器用这些信息来切换栈。
CR3和分页有关,有关分页的知识将在第16章讲述。此处一般由任务的创建者填写,如果没有使用分页,可以为0。
偏移为32~92的区域是处理器各个寄存器的快照部分,用于在进行任务切换时,保存处理器的状态以便将来恢复现场。在一个多任务环境中,每个创建一个任务时,操作系统或者内核至少要填写EIP、EFLAGS、ESP、CS、SS、DS、ES、FS和GS,当该任务第一次获得执行时,处理器从这里加载初始执行环境,并从CS:EIP处开始执行任务的第一条指令。在此之后的任务运行期间,该区域的内容由处理器固件进行更改。在本章中,只有一个任务,而且自进入保护模式时就开始运行了,只不过一开始是在0特权级的全局空间执行。所以,这部分内容不需要填写。
LDT段选择子是当前任务的LDT描述符选择子。由内核或者操作系统填写,以指向当前任务的LDT。该信息由处理器在任务切换时使用,在任务运行期间保持不变。
T位用于软件调试。在多任务的环境中,如果T位是“1”,每次切换到该任务时,将引发一个调试异常中断。这是有益的,调试程序可以接管该中断以显示任务的状态,并执行一些调试操作。现在只需要将这一位清零即可。
I/O映射基地址用于决定当前任务是否可以访问特定的硬件端口,对它的解释说来话长。我们知道,特权指令是只有0特权级的程序才可以执行的指令,执行这些指令会影响整个机器的状态。现有的特权指令也许是处理器的设计者精心挑选的,因为即使较低特权级的程序不使用它们,这些程序也能运行得很好,简直是非常好。不过,另外一些候选的指令就没那么幸运了,尽管它们也适合作为特权指令,但其他特权级的程序同样需要它们。
一个典型的例子就是硬件端口的输入输出指令in和out,它们应该对特权级别为1的程序开放,因为设备驱动程序就工作在这个特权级别。不过,这样做依然是不合理的,因为即使是特权级为3的程序,在需要快速反应的场合,也需要直接访问某些硬件端口。所以,如果需要,它们也可以向2、3特权级的程序开放。
处理器可以访问65536个硬件端口。如果只对应用程序开放那些它们需要的端口,而禁止它们访问另一些敏感的端口,操作系统肯定会对此持欢迎态度,因为这有利于设备的统一管理,同时也很安全。
每个任务都有EFLAGS寄存器的副本,其内容在任务创建的时候由内核或者操作系统初始化,在多任务系统中,每次当任务恢复运行时,就由处理器固件自动从TSS中恢复。EFLAGS寄存器的IOPL位决定了当前任务的I/O特权级别。如果当前特权级CPL高于,或者和任务的I/O特权级IOPL相同时,即,在数值上,CPL≤IOPL时,所有I/O操作都是允许的,针对任何硬件端口的访问都可以通过。相反,如果当前特权级CPL低于任务的I/O特权级IOPL,也并不意味着所有的硬件端口都对当前任务关上了大门。事实上,处理器的意思是总体上不允许,但个别端口除外。至于个别端口是哪些端口,要找到当前任务的TSS,并检索I/O许可串位。
14-017.png

如图14-017所示,I/O许可位串(I/O Permission Bit String)是一个比特序列,或者说是一个比特串,最多允许65536比特,即8KB。从第1比特开始,各比特用它在串中的位置代表一个端口号。因此,第1个比特代表0号端口,第2个比特代表1号端口,第3个比特代表2号端口,…,第65536比特代表第65535端口。
每个比特的取值决定了相应的端口是否允许访问。为1时,禁止访问;为0时,允许访问。
处理器检查I/O许可位的方式是先计算它在I/O许可位映射区的字节编号,并读取该字节,然后进行测试。比如,当执行指令out0x09,al时,处理器通过计算就可以知道,该端口对应着I/O许可位映射区第2个字节的第2个比特(位1)。于是,它读取该字节,并测试那一位。
同其他和任务相关的信息一样,I/O许可位串位位于任务的TSS中。如图14-18所示,任务状态段TSS的最小长度是104字节,保存着最基本的任务信息,但这并不是它的最大长度。
14-018.png

事实上,整个TSS还可以包括一个I/O许可位串,它所占用的区域称为I/O许可位映射区。如上图所示,在TSS内偏移为102的那个字单元,保存着I/O许可位串(I/O许可位映射区)的起始位置,从TSS的起始处(0)算起。(此处注意:32位系统中,基地址、偏移量什么的大部分都是32位的,为何这里的I/O映射基地址只有16位呢?因为它是相对TSS开始处的16位偏移量,不是一个32位的数值)因此,如果该字单元的内容大于(书上为小于,有误)或者等于TSS的段界限(在TSS描述符中),则表明没有I/O许可位串。在这种情况下,如果当前特权级CPL低于当前的I/O特权级IOPL,执行任何硬件I/O指令都会引发处理器异常中断。说明一下,和LDT一样,必须在GDT中创建TSS的描述符,TSS描述符中包括了TSS的基地址和界限,该界限值包括I/O许可位映射区在内。
非常重要的一点是,I/O端口是按字节编址的。这句话的意思是,每个端口仅被设计用来读写一个字节的数据,当以字或者双字访问的时,实际上是访问连续的2个或者4个端口。比如,当从端口n读取一个字时,相当于同时从端口n和端口n+1各读取一个字节。即,inax,0x3f8语句相当于同时执行
in al,0x3f8
in ah,0x3f9     ;仅为示例,x86处理器不允许使用AH寄存器
由于这个原因,当处理器执行一个字或者双字I/O指令时,会检查许可位串中的2个,或者4个连续位,而且要求它们必须都是“0”,否则引发异常中断。麻烦在于,这些连续的位可能是跨字节的。即,一些位于前一字节,另一些位于后一字节。为此,处理器每次都要从I/O许可位映射区读两个连续的字节。
这种操作方式直接导致了另一个问题。即,如果要检查的比特在最后一字节中,那么,这个两字节的读操作将会越界。为防止这种情况,处理器要求I/O许可位映射区的最后必须附加一个额外的字节,并要求它的所有比特都是“1”,即0xFF。当然,它必须位于TSS的界限之内。
处理器并不要求为每一个I/O端口都提供位映射。对于那些没有在该区域内映射的位,处理器假定它对应的比特是“1”。例如,要是I/O许可位映射区的长度是11字节,那么,除去最后一个所有比特都是“1”的字节,前10字节映射了80个端口,分别是端口0到端口79,访问更高地址的端口将引发异常中断。
显然,EFLAGS寄存器中的IOPL位对于控制任务的I/O特权来说是很重要的。通常,IOPL位由内核或者操作系统根据任务的实际需要进行初始化。尽管不存在对EFLAGS寄存器整体写入或者读出的指令,但存在将标志寄存器入栈和出栈的指令:
pushf/pushfd
popf/popfd
pushf并不是一条新指令。事实上,早在8086处理器的时代就已经有了,用于将16位的标志寄存器FLAGS压栈,机器指令码为9C。在8086处理器上执行时,SP寄存器的内容减去2,然后将FLAGS的内容保存到栈段,操作数的大小是一个字。同样地,popf指令把当前栈中的栈顶内容弹出到FLAGS寄存器。
到了32位处理器时代,pushf指令既可以工作在16位模式下,也可以工作在32位模式下。在16位模式下,pushf压入的是EFLAGS的低16位。如果要压入整个32位的EFLAGS,需要指令前缀66,即669C。在32位模式下,pushf压入的是整个32位的EFLAGS,即使有指令前缀,也不会只压入低16位,多总比少好,只压入低16位没有太大意义,徒增处理器的负担。
为了区分EFLAGS寄存器在16位模式下的两种压栈方式,编译器引入了符号pushfd。本质上,它们对应着同一条指令,当你使用pushf时,编译器就知道,应当编译成无前缀的机器码9C;当使用pushfd时,编译器会编译成66 9C。可见,在32位模式下,pushf和pushfd是相同的。上面的讨论同样适用于popf和popfd。
通过将EFLAGS寄存器的内容压入栈,局部修改后,再弹出到EFLAGS,可以间接地改变它的各种标志。对多数标志位的修改不会威胁到整个系统的安全,比如,你修改了ZF标志,这有什么用呢?唯一的后果可能是搬石头砸自己程序的脚。
但是,如果修改了IOPL位和IF位,就不同了。能够修改这两个标志的指令是popf、iret、cli、sti。注意,没有包括pushf指令,原因来自一个阴险的想法:你可以执行pushf指令,但我不允许你执行popf和iret指令。另外,中断是由操作系统或者内核统一管理的,cli和sti指令不能由低特权级的程序随便执行。遗憾的是,这些指令并不是特权指令,原因很简单,其他特权级的程序也离不开它们。
最好的办法是用IOPL本身来控制它们。如果当前特权级CPL高于,或者和当前I/O特权级IOPL相同,即,在数值上CPL≤IOPL,则允许执行以上4条指令,也允许访问所有的硬件端口。否则,如果当前特权级CPL低于当前的I/O特权级IOPL,则执行popf和iret指令时,会引发处理器异常中断;执行cli和sti时,不会引发异常中断,但不改变标志寄存器的IF位。同时,是否能访问特定的I/O端口,要参考TSS中的I/O许可位映射串。

本帖被以下淘专辑推荐:

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

使用道具 举报

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

本版积分规则

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

GMT+8, 2024-11-24 23:17

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

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