★PART1:32位保护模式下任务的隔离和特权级保护

   这一章是全书的重点之一,这一张必须要理解特权级(包括CPL,RPL和DPL的含义)是什么,调用门的使用,还有LDT和TSS的工作原理(15章着重讲TSS如何进行任务切换)。

1. 任务,任务的LDT和TSS

  程序是记录在载体上的指令和数据,其正在执行的一个副本,叫做任务(Task)。如果一个程序有多个副本正在内存中运行,那么他对应多个任务,每一个副本都是一个任务。为了有效地在任务之间进行隔离,处理器建议每个任务都应该具有他自己的描述符表,称为局部描述符表LDT(Local Descriptor Table)。LDT和GDT一样也是用来储存描述符的,但是LDT是只属于某个任务的。每个任务是有的段,都应该在LDT中进行描述,和GDT不同的是,LDT的0位也是有效的,也可以使用。

  LDT可以有很多个(有多少个任务就有多少个LDT),处理器使用局部描述符寄存器(LDT Register: LDTR)。在一个多任务的系统中,会有很多任务在轮流执行,正在执行中的那个任务,称为当前任务(Current Task)。因为LDTR只有一个,所以他用于指向当前任务的LDT,当发生任务切换(会在15章讲),LDTR会被自动更新成新的任务的LDT,和GDTR一样,LDTR包含了32位线性基地址字段和16位段界限。以指示当前LDT的位置和大小。如果要访问LDT中的一个描述符,和访问GDT的时候是差不多的,也是要向段寄存器传输一个16位的段选择子,只是和指向GDT的选择子不同,指向LDT的选择子的TI位是1。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  因为索引号只能是13位的,所以每个LDT所能容纳的描述符个数为2,也就是8192个。又因为每个描述符是8个字节,所以LDT最大长度是64KB。

  同时,为了保存任务的状态,并且在下次重新执行的时候恢复他们,每个任务都应该用一个额外的内存区域保存相关信息,这就叫做任务状态段(Task State Segment: TSS)。如图:

    ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  任务状态段TSS是具有上图的固定格式的,最小尺寸是104(这就是为什么图上的I/O映射基地址只有16位的原因,其实是可以32位的)。图中标注的偏移量都是10进制的。和LDT一样,处理器用TR(Task Register: TR)寄存器来指向当前的任务TSS。TR也是只有一个。当任务进行切换的时候,处理器将当前任务的现场信息保存到TR指向的TSS中,然后,再使TR寄存器指向新的任务TSS,并从新任务的TSS中恢复现场。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

2. 任务的全局空间和局部空间

每个任务都包含两个部分:全局部分和私有部分。全局部分是所有任务共有的,含有操作系统的软件和库程序,以及可以调用的系统服务和数据。私有部分则是每个任务自己的数据和代码,与任务要解决的具体问题有关,彼此各不相同。每个任务的LDT可以登记8192个段,GDT可以登记8191个段(0不能用),这样的话每个用户程序可以有64TB的总空间。在操作系统中,允许程序使用逻辑地址来访问内存,而不是实际地址,所以这64TB内存是虚拟地址空间(要开启页功能,16章讲)(全局地址空间可以有32TB,一个任务的局部空间为32TB,也就是一个任务的总空间可以是64TB,但是操作系统允许程序的编写者使用虚拟地址(逻辑地址)来访问内存。同一块内存,可以让多任务,或者是每个任务的不同段来使用。当执行或者访问一个新的段的时候,如果它不在物理内存中,而且也没有空闲的物理内存来加载它的时候,操作系统会挑出一个暂时不用的段,把它换到磁盘中,并把空间腾出来分配给马上要访问的段。并修改段描述符,使之指向这一段内存空间。当要使用这个段的时候再把段置换回物理内存中。)操作系统本身要进行虚拟内存管理。

3. 任务和特权级保护,调用门

  X86架构下,Intel引进了4个特权级,分别是0-3,权限从0到3逐次递减。操作系统处于0特权级,系统服务程序一般在0-2特权级,普通的应用程序一般在3特权级。这里要特别注意的是:特权级不是指的任务的特权级,而是指的组成任务的各个部分的特权级。比如任务的全局部分一般是0,1和2特权级别的,任务的私有部分一般是3特权级的。

处理器给每个可管理的对象都赋予一个特权级,以决定谁能访问他,确保各种操作的相对安全性。比如系统的一些敏感指令(如hlt,对控制寄存器的读写指令,lgdt,ltr等),必须通过具有0特权级的对象来操作。除了敏感指令,I/O端口的读写操作也是通过特权管理来进行的,这里所说的特权管理,通常是指的I/O端口访问许可权。由EFLAGS中的13位和12位决定(I/O Privilege Level:  IOPL),它代表着当前任务的I/O特权级别,I/O端口的访问权限控制等下再讲。

接下来我们要分清楚DPL,RPL和CPL之间的联系。

每个在GDT或者在LDT中的描述符,都有一个DPL位,这就是这个描述符所指的段的特权级(又叫做描述符特权级Descriptor Privilege Level: DPL)。

每个段的选择子的0和1位是一个RPL(Request Privilege Level: RPL)位,对应着当前操作请求的特权级。

当处理器正在一个代码段中取指令和执行指令时,那个代码段的特权级是当前特权级(Current Privilege Level: CPL)。正在执行的这个代码段的选择子位于段寄存器CS中,其最低两位就是当前特权级的值。

对于数据段,如果一个数据段,其描述符的DPL位2,那么只有特权级为0,1和2的程序才能访问他,如果特权级为3的程序访问这个数据段,那么处理器会阻止并引发异常中断。也就是在数值上要有:

CPL<=DPL(数值上比较,目标数据段DPL)

RPL<=DPL(数值上比较,目标数据段DPL)

对于代码段,处理器对代码段的检查是非常严格的,一般控制转移只允许发生在两个特权级相同的代码段之间。但是处理器允许通过将权限转移到依从的代码段或者通过调用门将当前权限变高。但是除了通过门切换或者从门返回,处理器不允许从特权级高的代码段转移到特权级低的代码段(可以理解为处理器不相信可靠性低的代码)。

如果当前程序想通过直接转移从特权级低的代码段到依从的特权级高的代码段,则必须满足:

CPL>=DPL(数值上比较,目标代码段DPL,代码段必须是依从的代码段)

RPL>=DPL(数值上比较,目标代码段DPL,代码段必须是依从的代码段)

程序还可以通过门来进行代码段的转移,14章讲的是调用门切换,但是总感觉讲的好啰嗦。说白了调用门其实也是一个描述符。如下图:

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  通常一些内核例程都是特权0级的(特别是那些需要访问硬盘的程序),所以调用门可以给用户程序便捷的调用例程的手段(用户程序在特权3级,用户程序只要知道例程函数名就可以了,不需要知道例程实现细节,而且可以做一些特权3级做不到的东西)。

  调用门描述符给出了例程所在的代码段的选择子,通过这个选择子就可以访问到描述符表相应的代码段,然后通过门调用实施代码段描述符的有效性,段界限和特权级的检查。例程开始偏移是直接在调用门描述符中指定的。

描述符的TYPE位用于表示门的类型,‘1100’表示的是调用门(这里注意S位,门的S位固定是0,说明这个是一个系统段,当检查了S为为0后处理器会继续检查TYPE为看是什么系统段(门或者LDT描述符,TSS描述符等))。

  描述符P位是有效位,正常来讲应该是‘1’,当它是0的时候,调用这样的门,会导致处理器产生中断。但是P=0这对于处理器来说是属于故障中断,从中断处理过程返回时,处理器还会重新执行引起故障的指令。对于操作系统来说,可以利用这个特性来统计门的调用次数,在中断程序中,每当某个门调用失败,就把该门的调用次数+1。

  通过调用门实施特权级之间控制转移时,可以使用jmp far指令,也可以使用call far指令,这两种方式的调用有差别。具体看下表:

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  如果使用了call执行调用门,则可能会改变CPL,因为栈段的特权级必须同当前的特权级保持一致,因此执行调用门时还需要对栈进行切换。也就是从低特权级的栈转移到高特权级的栈,比如一个特权级为3的程序通过调用门执行特权级为0的代码段,则栈段要从特权级3转移到特权级0。这是为了防止因为栈空间不足而产生不可预料的问题,同时也是为了防止数据的交叉引用。为了切换栈,每个任务除了自己固有的栈,还必须额外定义几个栈,具体数量去决定于当前任务的特权级,要补充比当前特权级高的栈段。这些额外创建的栈段必须放在任务自己的LDT中。同时,还要在TSS中进行登记。

  通过调用门使用高级权限的例程时,调用者会传递一些参数给例程。一般的方法是通过栈段来传递,但是因为切换栈段后栈段的指针会被初始化为一个固定的值,也就是如果你不说,处理器其实并不知道你通过栈传递了多少参数给例程,这个时候需要在调用门上说明传递了多少个参数,而参数的复制是通过处理器固件完成的,然后根据参数个数来把调用者栈中的数据复制到切换的栈段中去(程序员不需要管栈究竟切换到哪了,只要知道某个被传递的参数在调用门之前是什么时候压入栈的,用push和pop指令可以像没有经过切换栈一样得到传递那个参数,尽管栈指针和栈段已经改变)。(这个非常重要,课后习题第二题有体现)

  用call far指令通过调用门转移控制时,如果改变当前特权级,则完整的切换栈段的过程书上讲的很明白了,如下:

  S1:使用目标代码段的DPL到当前任务的TSS中选择一个栈,包括栈段的选择子和栈指针。

  S2:从TSS中独缺所选择的段的选择子和栈指针,并用该选择子和栈指针,并用该选择子读取栈段描述符,在此期间,任何违反段界限的行为都会引起处理器引发异常中断(无效TSS)。

  S3:检查栈段描述符的特权级和类型,并可能引发处理器异常中断(无效TSS)。

  S4:临时保存当前栈段寄存器SS和栈指针ESP的内容。

  S5:把新的栈段选择子和栈指针带入SS和ESP寄存器,切换到新栈。

  S6:将刚才临时保存的SS和ESP内容压入当前栈

  S7:依据调用门描述符“参数个数”字段的指示,从旧栈中所有的参数都复制到新栈中,如果参数为0则不复制。

  S8:将当前段寄存器CS和指针寄存器EIP压入新栈(因为是远调用)。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  S9:从调用门描述符中依次将目标代码选择子和段内偏移传送到CS和ESP寄存器,开始执行调用过程。

  注:如果不进行段切换,那么SS不会变,直接压入CS和EIP就好

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  使用jmp far来通过调用门转移控制,则没有特权级的变化,所以也不会进行栈段的切换,而且不能使用retf来返回控制到调用者。

  使用call far来通过调用门转移控制,可以使用retf来将控制返回,retf或者ret指令都是可以带参数的,带参数的返回其实就是高级语言的_stdcall的原型。具体可以看这篇文章:

http://www.cnblogs.com/Philip-Tell-Truth/articles/5294369.html

  带参数的返回是用于解决通过栈传递参数的过程最后维持栈平衡的问题,过程编写者知道进入过程前栈中有多少个元素,如果需要把这些元素在结束过程后弹出,则可以使用ret imm16或者retf imm16,这两条指令都带16位立即数为操作数,前者为近返回,后者为远返回,而且数值总是为2的倍数或者4的倍数(使用SP则位2的倍数,使用ESP则为4的倍数),代表将控制返回调用者之前,应当从栈中弹出多少字节的数据。假如弹出的数值为k,则过程结束后会执行ESP<-ESP+k,相当于弹出了k/4个参数。

  要求特权级变化的远返回,只能返回到较低特权级别上。控制返回的全部过程如下:

  S1:检查占中保存的CS寄存器的内容,根据其RPL字段决定返回时是否需要改变特权级别。

  S2:从当前栈中读取CS和EIP的内容,并针对代码段描述符和代码段选择子的RPL段进行特权检查。从同一特权级返回时,处理器任然会进行一次特权检查。

  S3:如果远返回指令是带参数的,则将参数和ESP寄存器的当前值相加,以跳过栈中的参数部分。最后的结果是ESP寄存器指向调用者SS和ESP的压栈值。注意,retf指令的字节计数值必须等于调用门的参数个数乘以参数长度。最后恢复调用者调用过程前(包括压入参数)的栈。

  S4:如果返回时需要改变特权级,则从栈中将SS和ESP的压栈值带入段寄存器SS和指令指针寄存器ESP,切换到调用者的栈,在此期间,一旦检测到有任何界限违反情况都会引发处理器异常中断。

  S5:如果返回时需要改变特权级,检查DS,ES,FS和GS寄存器的内容,根据他们找到相应的段描述符,要是有任何一个段描述符的DPL高于调用者的特权级(返回时新的CPL,数值上段描述符DPL<返回后的CPL),处理器将会把数值0传入该段寄存器(因为数据段的特权级检查只会在把段寄存器的选择子传入段寄存器时检查的,在这之后任何访问数据段指向的内存都不会进行特权级检查,如果当前的数据段特权级高于调用者,不在返回的时候把0传给段寄存器而任然不变的话,那么调用者将有权利使用高特权级的数据段,可能会引发不安全的操作)。

特别需要注意的是,TSS中的SS0,EP0,SS1,EP1,SS2和EP2域都是静态的,除非软件进行修改,否则处理器不会改变他们。

好了讲了那么多,我们来看下如何创建一个调用门

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

首先符号表每个项下面要加多一个东西,就是参数个数(教材上没有写这个东西,默认参数个数为0)。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  这就是安装门的过程,其实和安装GDT描述符差不多(调用门一般为公用例程,所以安装在GDT上),注意调用门的过程,我们使用的是call far,再进行门调用时,无论是间接远调用还是绝对远调用,处理器只会用选择子部分,而偏移地址部分会被忽略。在之后的给用户程序填充门描述符时,一定要记得把门描述符的特权级改为应用程序特权级3,,门调用时候处理器要按以下规则检查(数值上):

CPL<=调用门描述符的DPL

RPL<=调用门描述符的DPL

CPL>=目标代码段的DPL

  上述规则都满足,才能成功调用门,否则会引发异常中断。

  这里还有一个地方值得注意的是处理器对调用者的请求特权级RPL的问题,RPL事实上是处理器和操作系统的一个协议,处理器本身只会负责检查特权级的RPL,判断其是否有权限访问目标段。并不检查RPL是否是正确的,对RPL的正确性检查是操作系统的事情。换句话说,操作系统总是会把RPL改为真正发起调用的任务的特权级,以防止低特权级任务通过门等越权操作。为了帮助内核或者操作系统检查真正调用者的身份,并提供正确的RPL的值,处理器提供了arpl(Adjust RPL Field of Segment Selector)指令,以方便地调整段选择子的RPL字段,格式为:

arpl r/m16,r16

  该指令的目的操作数包含了16位段选择子的通用寄存器,或者指向一个16位的内存单元。源操作数只能是包含了段选择子的16位通用寄存器。

  该执行执行时,处理器检查目的操作数的RPL字段,如果它在数值上小于源操作数的RPL字段,则设置ZF标志,并调整目的操作数的RPL字段为源操作数的RPL,否则,清零ZF,且除此之外不进行任何操作。这个指令是典型的操作系统指令,它常用于调整应用程序传递给操作系统的段选择子,使其RPL字段的值和应用程序的特权级相匹配,这个指令也可以在应用程序上用。

  比如一个采用压栈传递参数,且通过门调用的读取磁盘函数前一段可以这么写:

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

★PART2:加载用户程序并创建任务

1. 记录任务数据临时块(TCB)(非处理器要求)

  一个合格的操作系统应该有能力跟踪程序的大小,加载的位置,使用内存的多少等,当然在现代流行的操作系统中,这个管理是非常复杂的。教材作者用一个简单的例子来做这个事情(和之前的符号表,内存管理什么的都一样,都是作者自己的私货),能解决问题就好了,暂时不需要考虑太复杂的东西。这个例子就是TCB链(Task Contorl Block,TCB)。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

在内和数据段加多一个上面的双字空间。然后创建TCB,也就是简单的分配内存

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

因为TCB是链表,所以每加一个TCB,就要寻链表一次,简单的遍历过程如下:

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

2. 创建和加载LDT到GDT中

  14章和13章不同的地方在于,因为14章要按照特权级3加载程序,要使用LDT,所以要给程序分配LDT的内存,而且要把一些描述符(包括那些用来切换的栈段)写入LDT,我们先把这些信息写入TCB。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

上面的代码的最后是安装LDT到GDT中,其实LDT的描述符和普通的段描述符长得差不多,就是他的S位必须是0,TYPE位必须是0010。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

3. 创建和加载TSS到GDT中

  要加载TSS,首先就要向TSS填入所需要的信息,再看一下TSS,除了段寄存器和段指针这些我们已经很熟悉了,我们来看一下我们不熟悉的部分,CR3(PDBR)和分页有关,这个将在16章讲述,LDT段选择子就是指的是当前任务的LDT在GDT中的选择子,任务切换时要用到(15章讲述),T位是软件调试位,如果T位是1,那么每次切换到该任务,就会引发一个调试异常中断,可以用来调试,现在只需要把这个清零就可以了。

  之前我们说过,EFLAGS中有一个IOPL位,是用来管理端口的读写问题的,而TSS恰好就有一个EFALGS段,那么现在我们就来搞明白这个域是怎么使用的。

  一个典型的例子就是硬件端口输入输出指令in和out,他们应该对特权级别位1的程序开放,因为设备驱动程序就工作在这个特权级别,不过,特权级3和特权级2的程序也同样需要快速访问端口,通过调用门来使用内核程序访问的方法太慢,而处理器是可以访问65536个硬件端口的。处理器允许只对应用程序开放那些他们需要的端口,而屏蔽敏感端口,这有利于设备的统一管理。每个任务都有EFLAGS寄存器的副本,其内容在任务创建的时候由内核或者操作系统初始化,在多任务系统中,每当任务恢复运行时,就由处理器固件自动从TSS恢复。

  EFLAGS寄存器的IPOL位决定了当前任务的I/O特权级别,如果CPL高于或者和任务的I/O特权级IOPL相同,也就是在数值上

CPL<=IOPL

  则虽有的I/O操作都是允许的,针对任何硬件端口的访问都可以通过。

  如果CPL低于任务的IOPL,则处理器要检索I/O许可位串,看是否允许这个任务访问这个接口。

  ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  如图所示,I/O许可串(I/O Perission Bit String)是一个比特序列,最多允许65536个比特(8KB),每一个比特代表一个端口,如果该比特为1,则这个比特对应的端口禁止访问,如果为0,则允许访问。这里要非常注意的是,I/O端口是按字节编址的,每个端口仅被设计者用来读写一个字节的数据,当以字或者双字访问时,实际上是连续访问2个或者4个端口,比如,从端口n读取一个字时,相当于从端口n和端口n+1各读取一个字节。

  TSS还可以包括一个I/O许可串,他所占用的区域称为I/O许可串映射区,处于TSS的偏移地址位102D的那个字单元,从TSS起始位置(0)开始算起,如果这个字单元的内容大于或者等于(教材是小于等于,有误)TSS的段界限(在TSS描述符中),则表明没有I/0许可串(且不能超过DFFFH),在这种情况下,如果CPL低于当前IOPL,执行任何硬件I/O都会引发处理器中断,TSS的界限值应该包括I/O许可映射区在内,I/O许可映射区在以TSS的初始位置(0)位起始点,按照I/O许可映射区的值作为偏移值所对应的内存的首地址,一直到TSS的段界限为止就是I/O映射区,且映射区的最后一个字节必须是0xFF,如果中间有空余,可以留给软件使用(TSS的尺寸最小是104)。

  处理器提供pushf和popf来给EFLAGS进行读写,pushf在16位模式下(实模式和16位保护模式),将16位的FLAGS压栈(16位),在32位保护模式下,将EFLAGS压栈(32位),pushf是一个字节的指令。则如果在16位模式下,pushf是压入EFLAGS的低16位,如果要压入32位EFLAGS,则要添加指令前缀66(指令:66 9C,66是前缀)为了区分EFLAGS在16位模式下的两种压栈方式,nasm提供了pushfd指令,它无论在16位模式还是在32位模式下,都会压入32位EFLAGS,而且在16为模式下,会给指令添加66(其实pushfd和pushf的作用是一样的)。popf和popfd同理。

  注意能够修改IOPL位和IF位的两个标志位的指令时popf(popfd),iret,cli,sti。注意没有pushf(pushfd),但是这是个指令都不是特权指令。处理器通过IOPL位来控制它们的使用。

  当且仅当:CPL<=IOPL(数值上),则允许执行上面4个指令和访问所有硬件端口。否则,当当前特权级低于I/O特权级,则执行iret和popf时,会引发异常中断,执行cli和sti时,不会引发异常中断,但不会改变标志寄存器的IF位,同时,能否安稳特定的I/O端口,要参考TSS中的I/O许可位映射串。

  接下来就是填写TSS和把TSS描述符写入GDT了,TSS描述符长得和LDT描述符差不多,只是TYPE位是10B1(B位是忙(Busy)位,一般由处理器填写,操作系统应该写这个位为0,和任务切换有关,15章讲)。

  ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

注:因为本章不用任务切换,所以TSS的其他段可以先不用填写。

★PART3:以特权级3加载用户程序

1. 加载任务寄存器TR和局部描述符寄存器LDTR

       和段寄存器一样,TR和LDTR寄存器都包括16位选择子部分,以及描述符高速缓存器部分,选择子部分是TR和LDT的描述符选择子,描述符高速缓存器同样是64位的(32位的线性基地址,20位的段界限,12位的段属性)。

加载任务寄存器TR需要用到ltr命令,指令格式为ltr r/m16,这条指令的操作数可以是16位的通用寄存器或者是16位的内存单元,包含16位的TSS选择子。当TSS加载到TR后,处理器自动将TSS的B位置1,但不进行任务切换。该指令不影响EFLAGS的任何标志位,但属于只能在0特权级下执行的特权命令。

加载据不描述符寄存器LDTR要用到lldt命令,指令格式为lldt r/m16,这条指令的操作数可以是16位的通用寄存器或者是16位的内存单元,包含16位的LDT选择子。lldt命令会把在GDT的ldt描述符的段界限和段基地址加载到LDTR的描述符高速缓存器部分,CS,SS,DS,ES,FS和GS寄存器的当前内容不受该指令的影响,包括TSS内的LDT选择子字段。

如果执行这条指令时,代入LDT选择器部分的选择子高14位全部都是0,则LDT寄存器的内容会被标记为无效,而该指令的执行会结束,且不会引发异常中断,但是后面的对LDT内描述符的操作都会引发异常中断。

处理器在加载TR和LDTR时,都要检查描述符的有效性。包括审查是不是TSS或者LDT描述符。

当然这一章没有将任务门,教材用了一个非常奇葩的方法(把切换到特权级3任务看成是从某个调用门的返回),非常别扭,等一下看代码就知道了,用户程序是直接用一个调用门返回的,而且教材给的例程返回后一定会引发常中断,因为其返回时用的是jmp返回,特权级还是3,最后教材那里要切换到内核数据区(DPL=3),肯定是不行的。

2. 显示处理器信息

上一章忘记这茬了,这里补一补,主要是讲如何用cpuid来显示处理器信息

cpuid(CPU Identification)指令是用于返回处理器标识和特性信息的,EAX是用于指定要返回什么样的信息,也就是功能,有时候还要用到ECX寄存器,但是无论如何cpuid指令执行后处理器将返回的信息放在EAX,EBX,ECX或者EDX中,cpuid指令是在(80486以后开始支持的,原则上使用cpuid之前要检测处理器是否支持该指令,再检测是否支持所需功能)。这个问题就出在EFLAGS上,EFLAGS的ID标志位(21位),如果可以被设置和清除,则他不支持cpuid指令,反之则可以支持。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

通常情况下不需要检查处理器是否支持cpuid(毕竟80486这款CPU年代太久远了),为了探测处理器能支持的最大的功能号,应该先用0号功能来执行cpuid指令:

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

  指令执行后,EAX寄存器返回最大可以支持的功能号,同时,还在EBX,ECX和ED中返回处理器供应商的信息,对于Intel来说,返回的信息就是“GenuineIntel”。

  要返回处理器品牌信息,需要使用0x80000002~0x80000004功能,分三次进行,该功能仅被奔腾4(Pentium4)之后的处理器支持,可以在内核数据区存放之后再显示。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

★PART4:14章的相关程序

这里直接课后习题两道题一起做了,感觉都是差不多的,主要是欣赏一下那个别扭的调用方法。

ASM:《X86汇编语言-从实模式到保护模式》第14章:保护模式下的特权保护和任务概述-LMLPHP

 ;===============================内核程序=================================
;定义内核所要用到的选择子
All_4GB_Segment equ 0x0008 ;4GB的全内存区域
Stack_Segement equ 0x0018 ;内核栈区
Print_Segement equ 0x0020 ;显存映射区
Sys_Routine_Segement equ 0x0028 ;公用例程段
Core_Data_Segement equ 0x0030 ;内核数据区
Core_Code_Segement equ 0x0038 ;内核代码段
;----------------------------------------------------------------
User_Program_Address equ ;用户程序所在逻辑扇区
Switch_Stack_Size equ ;切换栈段的大小
;=============================内核程序头部===============================
SECTION header vstart=
Program_Length dd Program_end ;内核总长度
Sys_Routine_Seg dd section.Sys_Routine.start ;公用例程段线性地址
Core_Data_Seg dd section.Core_Data.start ;内核数据区线性地址
Core_Code_Seg dd section.Core_Code.start ;内核代码区线性地址
Code_Entry dd start ;注意偏移地址一定是32位的
dw Core_Code_Segement
;----------------------------------------------------------------
[bits ]
;=========================================================================
;============================公用例程区===================================
;=========================================================================
SECTION Sys_Routine align= vstart=
ReadHarddisk: ;push1:28位磁盘号(esi)
;push2:应用程序数据段选择子(ax->ds)
;push3: 偏移地址(ebx)
;push4: 应用程序代码段选择子(dx)
pushad
push ds
push es mov ebp,esp mov esi,[ebp+*]
movzx eax,word[ebp+*]
mov ebx,[ebp+*]
movzx edx,word[ebp+*] arpl ax,dx
mov ds,ax mov dx,0x1f2
mov al,0x01 ;读一个扇区
out dx,al inc edx ;0-7位
mov eax,esi
out dx,al inc edx ;8-15位
mov al,ah
out dx,al inc edx ;16-23位
shr eax,
out dx,al inc edx ;24-28位,主硬盘,LBA模式
mov al,ah
and al,0x0f
or al,0xe0
out dx,al inc edx
mov al,0x20
out dx,al _wait:
in al,dx
and al,0x88
cmp al,0x08
jne _wait mov dx,0x1f0
mov ecx, _read:
in ax,dx
mov [ebx],ax
add ebx,
loop _read pop es
pop ds
popad
retf ;4个数据
;----------------------------------------------------------------
put_string: ;ebx:偏移地址
pushad
push ds
push es _print:
mov cl,[ebx]
cmp cl,
je _exit
call put_char
inc ebx
jmp _print
_exit:
pop es
pop ds
popad
retf ;段间返回
;--------------------------------------------------------------
put_char: ;cl就是要显示的字符
push ebx
push es
push ds mov dx,0x3d4
mov al,0x0e ;高8位
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al ;先把高8位存起来
mov dx,0x3d4
mov al,0x0f ;低8位
out dx,al
mov dx,0x3d5
in al,dx ;现在ax就是当前光标的位置 _judge:
cmp cl,0x0a
je _set_0x0a
cmp cl,0x0d
je _set_0x0d
_print_visible:
mov bx,ax
mov eax,Print_Segement
mov es,eax
shl bx, ;注意这里一定要把ebx变成原来的两倍,实际位置是光标位置的两倍
mov [es:bx],cl ;注意这里是屏幕!
mov byte[es:bx+],0x07
add bx,
shr bx,
jmp _roll_screen
_set_0x0d: ;回车
mov bl,
div bl
mul bl
mov bx,ax
jmp _set_cursor
_set_0x0a: ;换行
mov bx,ax
add bx,
jmp _roll_screen
_roll_screen:
cmp bx,
jl _set_cursor
mov eax,Print_Segement
mov ds,eax
mov es,eax cld
mov edi,0x00
mov esi,0xa0
mov ecx,
rep movsw
_cls:
mov bx,
mov ecx,
_print_blank:
mov word[es:bx],0x0720
add bx,
loop _print_blank
mov bx, ;别总是忘了光标的位置!
_set_cursor: ;改变后的光标位置在bx上
mov dx,0x3d4
mov al,0x0f ;低8位
out dx,al mov al,bl
mov dx,0x3d5
out dx,al mov dx,0x3d4
mov al,0x0e ;高8位
out dx,al mov al,bh
mov dx,0x3d5
out dx,al pop ds
pop es
pop ebx
ret
;----------------------------------------------------------------
allocate_memory: ;简易内存分配策略
;输入ecx:想要分配的总字节数
;输出ecx:分配的线性基地址
push ds
push eax
push ebx
call Cal_User_Mem mov eax,Core_Data_Segement
mov ds,eax
mov eax,[ram_alloc]
mov edx,eax ;edx暂存一下eax
add eax,ecx cmp eax,edx ;发现新分配的现地址比原来的还小,说明已经溢出
jge _alloc
mov ebx,mem_alloc_fail
call Sys_Routine_Segement:put_string
mov ecx, ;分配为0说明已经分配失败
jmp _exit1
_alloc: mov ebx,eax
and ebx,0xfffffffc
add ebx, ;强行向上取整
test eax,0x00000003
cmovnz eax,ebx
mov ecx,[ram_alloc] ;要返回要分配的初始地址
mov [ram_alloc],eax ;下一次分配的线性基地址 _exit1:
pop ebx
pop eax
pop ds retf
;----------------------------------------------------------------
recycled_memory_and_gdt:
mov eax,[ram_recycled]
sub [ram_alloc],eax
mov dword[ram_recycled], ;因为我们还没学到多任务,先这样简单地清零 sgdt [pgdt_base_tmp]
sub word[pgdt_base_tmp], ;应用程序的LDT,TSS
lgdt [pgdt_base_tmp] ;重新加载内核
retf
;----------------------------------------------------------------
Cal_User_Mem: ;输入ecx:应用程序用到的内存(字节)
add [ram_recycled],ecx
ret
;----------------------------------------------------------------
PrintDword: ;显示edx内容的一个调试函数
pushad
push ds mov eax,Core_Data_Segement
mov ds,eax mov ebx,bin_hex
mov ecx, _query:
rol edx,
mov eax,edx
and eax,0x0000000f
xlat push ecx
mov cl,al
call put_char
pop ecx loop _query pop ds
popad retf
;----------------------------------------------------------------
Make_Seg_Descriptor: ;构造段描述符
;输入:
;eax:线性基地址
;ebx:段界限
;ecx:属性
;输出:
;eax:段描述符低32位
;edx:段描述符高32位
mov edx,eax
and edx,0xffff0000
rol edx,
bswap edx
or edx,ecx shl eax,
or ax,bx
and ebx,0x000f0000
or edx,ebx
retf
;----------------------------------------------------------------
Make_Gate_Descriptor: ;构造门描述符
;输入:
;eax:段内偏移地址
;bx: 段的选择子
;cx: 段的属性
;输出:
;eax:门描述符低32位
;edx:门描述符高32位
push ebx
push ecx mov edx,eax
and edx,0xffff0000 ;要高16位
or dx,cx shl ebx,
and eax,0x0000ffff
or eax,ebx pop ecx
pop ebx retf
;----------------------------------------------------------------
Set_New_GDT: ;装载新的全局描述符
;输入:edx:eax描述符
;输出:cx选择子
push ds
push es mov ebx,Core_Data_Segement
mov ds,ebx mov ebx,All_4GB_Segment
mov es,ebx sgdt [pgdt_base_tmp] movzx ebx,word[pgdt_base_tmp]
inc bx ;注意这里要一定是inc bx而不是inc ebx,因为gdt段界限初始化是0xffff的
;要用到回绕特性
add ebx,[pgdt_base_tmp+0x02] ;得到pgdt的线性基地址 mov [es:ebx],eax
mov [es:ebx+0x04],edx ;装载新的gdt符
;装载描述符要装载到实际位置上 add word[pgdt_base_tmp], ;给gdt的段界限加上8(字节) lgdt [pgdt_base_tmp] ;加载gdt到gdtr的位置和实际表的位置无关 mov ax,[pgdt_base_tmp] ;得到段界限
xor dx,dx
mov bx, ;得到gdt大小
div bx
mov cx,ax
shl cx, ;得到选择子,ti=0(全局描述符),rpl=0(申请特权0级) pop es
pop ds
retf
;----------------------------------------------------------------
Set_New_LDT_To_TCB: ;装载新的局部描述符
;输入:edx:eax描述符
; : ebx:TCB线性基地址
;输出:cx选择子 push edi
push eax
push ebx
push edx
push ds mov ecx,All_4GB_Segment
mov ds,ecx mov edi,[ebx+0x0c] ;LDT的线性基地址
movzx ecx,word[ebx+0x0a]
inc cx ;得到实际的LDT的大小(界限还要-1) mov [edi+ecx+0x00],eax
mov [edi+ecx+0x04],edx add cx,
dec cx mov [ebx+0x0a],cx mov ax,cx
xor dx,dx
mov cx,
div cx shl ax,
mov cx,ax
or cx,0x0004 ;LDT,第三位TI位一定是1 pop ds
pop edx
pop ebx
pop eax
pop edi
retf
;=========================================================================
;===========================内核数据区====================================
;=========================================================================
SECTION Core_Data align= vstart=
;-------------------------------------------------------------------------------
pgdt_base_tmp: dw ;这一章的用户程序都是从GDT中加载的
dd ram_alloc: dd 0x00100000 ;下次分配内存时的起始地址(直接暴力从0x00100000开始分配了)
ram_recycled dd ;这里储存程序实际用的大小
salt:
salt_1: db '@Printf' ;@Printf函数(公用例程)
times -($-salt_1) db
dd put_string
dw Sys_Routine_Segement
dw ;参数个数 salt_2: db '@ReadHarddisk' ;@ReadHarddisk函数(公用例程)
times -($-salt_2) db
dd ReadHarddisk
dw Sys_Routine_Segement
dw ;参数个数 salt_3: db '@PrintDwordAsHexString' ;@PrintDwordAsHexString函数(公用例程)
times -($-salt_3) db
dd PrintDword
dw Sys_Routine_Segement
dw ;参数个数 salt_4: db '@TerminateProgram' ;@TerminateProgram函数(内核例程)
times -($-salt_4) db
dd _return_point
dw Core_Code_Segement
dw ;参数个数 salt_length: equ $-salt_4
salt_items_sum equ ($-salt)/salt_length ;得到项目总数 message_1 db ' If you seen this message,that means we '
db 'are now in protect mode,and the system '
db 'core is loaded,and the video display '
db 'routine works perfectly.',0x0d,0x0a, message_2 db ' Loading user program...', do_status db 'Done.',0x0d,0x0a, message_3 db 0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
db ' User program terminated,control returned.'
db 0x0d,0x0a,0x0d,0x0a,
message_4 db ' We have been backed to kernel.',0x0d,0x0a,
message_5 db ' The GDT and memory have benn recycled.',
message_6 db ' From the system wide gate:',0x0d,0x0a,
message_7 db ' Setting the gate discriptor...',
message_In_Gate db ' Hi!My name is Philip:',0x0d,0x0a, bin_hex db '0123456789ABCDEF'
;put_hex_dword子过程用的查找表
core_buf times db ;内核用的缓冲区(2049个字节(2MB)) esp_pointer dd ;内核用来临时保存自己的栈指针 cpu_brnd0 db 0x0d,0x0a,' ',
cpu_brand times db
cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,
mem_alloc_fail db 'The Program is too large to load'
core_ss dw
core_sp dd tcb_chain dd ;任务控制块链头指针
;=========================================================================
;===========================内核代码区====================================
;=========================================================================
SECTION Core_Code align= vstart=
;---------------------------------------------------------------------
append_to_tcb: ;写入新的TCB链
;输入:ecx新的TCB线性基地址
pushad push ds
push es mov eax,All_4GB_Segment
mov es,eax mov eax,Core_Data_Segement
mov ds,eax mov eax,[tcb_chain]
cmp eax,0x00
je _notcb _search_tcb:
mov edx,[tcb_chain+0x00]
mov eax,[es:edx]
cmp eax,0x00
jne _search_tcb mov [es:edx+0x00],ecx
jmp _out_tcb_search _notcb:
mov [tcb_chain],ecx _out_tcb_search:
pop es
pop ds popad
ret
;---------------------------------------------------------------------
load_program: ;输入push1:逻辑扇区号
; push2: 线性基地址
pushad
push ds
push es mov ebp,esp ;别忘了把参数传给ebp mov eax,Core_Data_Segement
mov ds,eax ;切换到内核数据段 mov eax,All_4GB_Segment
mov es,eax mov edi,[ebp+*] ;获取tcb的线性基地址,别忘了调用相对近调用还要有1个push mov ecx,
call Sys_Routine_Segement:allocate_memory
mov [es:edi+0x0c],ecx
mov word[es:edi+0x0a],0xffff ;初始化LDT界限位0xffff mov esi,[ebp+*] ;esi必须是逻辑扇区号
mov ebx,core_buf ;ebx要在内核数据缓冲区(先读取头部在缓冲区,esi已经是有扇区号了) push esi
push ds
push ebx
push cs
call Sys_Routine_Segement:ReadHarddisk mov eax,[core_buf] ;读取用户程序长度 mov ebx,eax
and ebx,0xfffffe00 ;清空低9位(强制对齐512)
add ebx,
test eax,0x000001ff
cmovnz eax,ebx ;低9位不为0则使用向上取整的结果 mov ecx,eax ;eax是整个程序的向上取整的大小
call Sys_Routine_Segement:allocate_memory
;先分配内存给整个程序,再分配内存给栈区
mov ebx,ecx
mov [es:edi+0x06],ecx ;tcb 0x06:程序加载基地址 xor edx,edx
mov ecx, ;千万不要改掉ebx
div ecx
mov ecx,eax mov eax,All_4GB_Segment ;切换到4GB段区域(平坦模式)
mov ds,eax _loop_read:
push esi
push ds
push ebx
push cs
call Sys_Routine_Segement:ReadHarddisk ;esi还是User_Program_Address
inc esi
add ebx,
loop _loop_read mov esi,edi ;esi:把TCB的线性基地址
mov edi,[es:esi+0x06] ;程序加载的线性基地址 ;建立头部描述符
mov eax,edi
mov ebx,[edi+0x04]
dec ebx ;段界限
mov ecx,0x0040f200
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0003 ;特权级3
mov [es:esi+0x44],cx ;记得要登记头部的选择子
mov [edi+0x04],cx ;建立代码段描述符
mov eax,edi
add eax,[edi+0x14]
mov ebx,[edi+0x18]
dec ebx
mov ecx,0x0040f800
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0003
mov [edi+0x14],cx ;建立数据段描述符
mov eax,edi
add eax,[edi+0x1c]
mov ebx,[edi+0x20]
dec ebx
mov ecx,0x0040f200
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0003
mov [edi+0x1c],cx ;建立栈段描述符
mov ecx,[edi+0x0c]
mov ebx,0x000fffff
sub ebx,ecx
mov eax, ;4KB粒度
mul ecx
mov ecx,eax
call Sys_Routine_Segement:allocate_memory
mov eax,ecx ;eax是栈段的线性基地址
mov ecx,0x00c0f600
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0003
mov [edi+0x08],cx ;现在开始重定位API符号表
;---------------------------------------------------------------------
mov eax,All_4GB_Segment ;因为这个时候用户头部在LDT,而LDT还没有被加载,只能通过4GB空间访问
mov es,eax
mov eax,Core_Data_Segement
mov ds,eax cld
mov ecx,[es:edi+0x24] ;得到用户程序符号表的条数
add edi,0x28 ;用户符号表的偏移地址是0x28 _loop_U_SALT:
push edi
push ecx mov ecx,salt_items_sum
mov esi,salt _loop_C_SALT:
push edi
push esi
push ecx mov ecx, ;比较256个字节
repe cmpsd
jne _re_match ;如果成功匹配,那么esi和edi刚好会在数据区之后的 mov eax,[esi] ;偏移地址
mov [es:edi-],eax ;把偏移地址填入用户程序的符号区
mov ax,[esi+0x04] ;段的选择子 or ax,0x0002 ;把RPL改为3,代表(内核)赋予应用程序以特权级3
mov [es:edi-],ax ;把段的选择子填入用户程序的段选择区 _re_match:
pop ecx
pop esi
add esi,salt_length
pop edi
loop _loop_C_SALT pop ecx
pop edi
add edi,
loop _loop_U_SALT
;--------------------------------------------------------------------- mov esi,[ebp+*] ;重新获得TCB的线性基地址 ;现在设置所有的特权级栈段,并且把特权级栈段放到TCB中(为了等一下设置TSS)
;设置TSS特权0级栈段(暂存在TCB中)
mov ecx,Switch_Stack_Size
mov eax,ecx
mov [es:esi+0x1a],ecx
shr dword[es:esi+0x1a], ;相当于除以4096
call Sys_Routine_Segement:allocate_memory
add eax,ecx ;得到最高地址
mov [es:esi+0x1e],eax ;登记线性基地址
mov ebx,0x000fffff
sub ebx,[es:esi+0x1a]
mov ecx,0x00c09600 ;特权级0
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0000 ;RPL为0
mov [es:esi+0x22],cx
mov dword[es:esi+0x24], ;设置TSS特权1级栈段(暂存在TCB中)
mov ecx,Switch_Stack_Size
mov eax,ecx
mov [es:esi+0x28],ecx
shr dword[es:esi+0x28], ;相当于除以4096
call Sys_Routine_Segement:allocate_memory
add eax,ecx ;得到最高地址
mov [es:esi+0x2c],eax ;登记线性基地址
mov ebx,0x000fffff
sub ebx,[es:esi+0x28]
mov ecx,0x00c0b600 ;特权级1
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0001 ;RPL为1
mov [es:esi+0x30],cx
mov dword[es:esi+0x32], ;设置TSS特权2级栈段(暂存在TCB中)
mov ecx,Switch_Stack_Size
mov eax,ecx
mov [es:esi+0x36],ecx
shr dword[es:esi+0x36], ;相当于除以4096
call Sys_Routine_Segement:allocate_memory
add eax,ecx ;得到最高地址
mov [es:esi+0x3a],eax ;登记线性基地址
mov ebx,0x000fffff
sub ebx,[es:esi+0x36]
mov ecx,0x00c0d600 ;特权级2
call Sys_Routine_Segement:Make_Seg_Descriptor
mov ebx,esi
call Sys_Routine_Segement:Set_New_LDT_To_TCB
or cx,0x0002 ;RPL为2
mov [es:esi+0x3e],cx
mov dword[es:esi+0x40], ;在GDT中存入LDT信息
mov eax,[es:esi+0x0c]
movzx ebx,word[es:esi+0x0a]
mov ecx,0x00408200 ;LDT描述符,特权级0级
call Sys_Routine_Segement:Make_Seg_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [es:esi+0x10],cx ;在TCB放入LDT选择子 ;在TCB中登记TSS的信息
mov ecx, ;创建一个最小尺寸的TSS
mov [es:esi+0x12],cx
dec word[es:esi+0x12] ;记得-1,要的是段界限
call Sys_Routine_Segement:allocate_memory
mov [es:esi+0x14],ecx ;TSS基地址 ;登记基本的TSS表格内容
mov word [es:ecx+], ;反向链=0 mov edx,[es:esi+0x24] ;登记0特权级堆栈初始ESP
mov [es:ecx+],edx ;到TSS中 mov dx,[es:esi+0x22] ;登记0特权级堆栈段选择子
mov [es:ecx+],dx ;到TSS中 mov edx,[es:esi+0x32] ;登记1特权级堆栈初始ESP
mov [es:ecx+],edx ;到TSS中 mov dx,[es:esi+0x30] ;登记1特权级堆栈段选择子
mov [es:ecx+],dx ;到TSS中 mov edx,[es:esi+0x40] ;登记2特权级堆栈初始ESP
mov [es:ecx+],edx ;到TSS中 mov dx,[es:esi+0x3e] ;登记2特权级堆栈段选择子
mov [es:ecx+],dx ;到TSS中 mov dx,[es:esi+0x10] ;登记任务的LDT选择子
mov [es:ecx+],dx ;到TSS中 mov dx,[es:esi+0x12] ;登记任务的I/O位图偏移
mov [es:ecx+],dx ;到TSS中 mov word [es:ecx+], ;T=0 ;在GDT中存入TSS信息
mov eax,[es:esi+0x14]
movzx ebx,word[es:esi+0x12]
mov ecx,0x00408900
call Sys_Routine_Segement:Make_Seg_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [es:esi+0x18],cx pop es
pop ds
popad
ret ;相当于是stdcall,过程清栈
;---------------------------------------------------------------------
start:
mov eax,Core_Data_Segement
mov ds,eax mov ebx,message_1
call Sys_Routine_Segement:put_string mov eax,
cpuid
cmp eax,0x80000004 ;判断是否有0x80000002-0x80000004功能
jl _@load ;显示处理器品牌信息,从80486的后期版本开始引入
mov eax,0x80000002
cpuid
mov [cpu_brand+0x00],eax
mov [cpu_brand+0x04],ebx
mov [cpu_brand+0x08],ecx
mov [cpu_brand+0x0c],edx mov eax,0x80000003
cpuid
mov [cpu_brand+0x10],eax
mov [cpu_brand+0x14],ebx
mov [cpu_brand+0x18],ecx
mov [cpu_brand+0x1c],edx mov eax,0x80000004
cpuid
mov [cpu_brand+0x20],eax
mov [cpu_brand+0x24],ebx
mov [cpu_brand+0x28],ecx
mov [cpu_brand+0x2c],edx mov ebx,cpu_brnd0
call Sys_Routine_Segement:put_string
mov ebx,cpu_brand
call Sys_Routine_Segement:put_string
mov ebx,cpu_brnd1
call Sys_Routine_Segement:put_string _@load:
mov ebx,message_7
call Sys_Routine_Segement:put_string
;----------------------------安装门------------------------------------
mov edi,salt
mov ecx,salt_items_sum
_set_gate:
push ecx
mov eax,[edi+]
mov bx,[edi+] ;选择子
mov cx,0xec00 ;门是特权级是3的门,那么任何程序都能调用
or cx,[edi+] ;加上参数个数 call Sys_Routine_Segement:Make_Gate_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [edi+],cx ;回填选择子
add edi,salt_length
pop ecx
loop _set_gate
;----------------------------------------------------------------------
mov ebx,do_status
call far [salt_1+]
mov ebx,message_6
call far [salt_1+]
mov ebx,message_In_Gate
call far [salt_1+] ;调用门显示字符信息(忽略偏移地址(前4字节)) mov ebx,message_4
call far [salt_1+]
mov ebx,message_2
call far [salt_1+] ;创建TCB链:TCB不是所有内核的要求,但是必须要有记录任务的机制
mov ecx,0x46
call Sys_Routine_Segement:allocate_memory
call append_to_tcb ;用栈去传数据,80386以后支持直接压一个双字,地址自己算
push dword User_Program_Address ;用户程序所在的逻辑地址
push ecx ;传入TCB线性基地址 call load_program
mov ebx,do_status
call Sys_Routine_Segement:put_string ;下一章讲任务切换的时候再改下面 mov [core_ss],ss
mov [core_sp],esp
mov eax,All_4GB_Segment
mov ds,eax ltr [ecx+0x18]
lldt [ecx+0x10] mov eax,[ecx+0x44] ;用户头部选择子
mov ds,eax push dword[0x08] ;栈段寄存器
push dword push dword[0x14]
push dword[0x10] ;切换特权栈段 retf _return_point:
pop eax
pop eax ;清除残存在栈段CS和EIP mov eax,Core_Data_Segement
mov ds,eax mov ss,[core_ss]
mov esp,[core_sp]
mov eax,Stack_Segement
mov ss,eax ;重新设置数据段和栈段
mov esp,[esp_pointer]
mov ebx,message_4
call Sys_Routine_Segement:put_string call Sys_Routine_Segement:recycled_memory_and_gdt
mov ecx,[ram_alloc] mov ebx,message_5
call Sys_Routine_Segement:put_string
cli
hlt
;=========================================================================
SECTION core_trail
;----------------------------------------------------------------
Program_end:
;==============================用户程序=======================================
SECTION header vstart= program_length dd program_end ;程序总长度#0x00 head_len dd header_end ;程序头部的长度#0x04 stack_seg dd ;用于接收堆栈段选择子#0x08
stack_len dd ;程序建议的堆栈大小#0x0c
;以4KB为单位 prgentry dd start ;程序入口#0x10
code_seg dd section.code.start ;代码段位置#0x14
code_len dd code_end ;代码段长度#0x18 data_seg dd section.data.start ;数据段位置#0x1c
data_len dd data_end ;数据段长度#0x20
;-------------------------------------------------------------------------------
;符号地址检索表
salt_items dd (header_end-salt)/ ;#0x24 salt: ;#0x28
Printf: db '@Printf'
times -($-Printf) db TerminateProgram:db '@TerminateProgram'
times -($-TerminateProgram) db ReadHarddisk: db '@ReadHarddisk'
times -($-ReadHarddisk) db header_end:
;===============================================================================
SECTION data align= vstart= buffer times db ;缓冲区 message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,
message_2 db ' Disk data:',0x0d,0x0a, data_end: ;===============================================================================
[bits ]
;===============================================================================
SECTION code align= vstart=
start:
User_Data_File equ ;数据文件存放地点
mov eax,ds
mov fs,eax mov eax,[stack_seg]
mov ss,eax
mov esp, mov eax,[data_seg]
mov ds,eax mov ebx,message_1
call far [fs:Printf] mov esi,User_Data_File
mov ebx,buffer ;缓冲区偏移地址 push esi
push ds
push ebx
push cs
call far [fs:ReadHarddisk] ;相当于调用函数 mov ebx,message_2
call far [fs:Printf] mov ebx,buffer
call far [fs:Printf] call far [fs:TerminateProgram] ;将控制权返回到系统 code_end: ;===============================================================================
SECTION trail
;-------------------------------------------------------------------------------
program_end:
05-08 15:47