LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 1169|回复: 1

基于i386的Linux实现特点剖析——基础的基础

[复制链接]
发表于 2003-2-26 03:47:57 | 显示全部楼层 |阅读模式
摘要

      本文讨论的是linux在i386体系的cpu上实现特点。通过介绍i386cpu的保护方式下的中断机制、存储器的段页式管理以及任务切换而提供的硬件机制,比较Linux的具体实现的特点与i386的设计意图之间的差异。描述了Linux中断向量表的设置过程,中断请求队列的初始化以及如何将特定的中断服务程序挂入指定的中断请求队列中。跟踪Linux中从逻辑地址到线性地址再到物理地址的变换过程,着重于线性地址到物理地址的页式变换,并结合i386cpu分析地址变换的效率。
    (2003-02-24 08:52:15)

By iamafan

基于i386体系结构的Linux实现特点剖析

一 前言

  本文讨论的是linux在i386体系的cpu上实现特点。通过介绍i386cpu的保护方式下的中断机制、存储器的段页式管理以及任务切换而提供的硬件机制,比较Linux的具体实现的特点与i386的设计意图之间的差异。描述了Linux中断向量表的设置过程,中断请求队列的初始化以及如何将特定的中断服务程序挂入指定的中断请求队列中。跟踪Linux中从逻辑地址到线性地址再到物理地址的变换过程,着重于线性地址到物理地址的页式变换,并结合i386cpu分析地址变换的效率。

二 80386 保护方式

  80386有两种工作方式:实方式和保护方式。尽管实方式下80386的功能较Intel先前的微处理器有很大的提高,但只有在保护方式下,80386才能真正发挥作用。在保护方式下,全部32根地址有效,可寻址达4G字节的物理空间;扩充的存储器分段管理机制和可选的存储器分页管理机制,不仅为存储器共享和保护提供了硬件支持,而且为实现虚拟存储器提供了硬件支持;支持多任务,能快速的进行任务切换和任务保护环境;4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码及数据的安全和保密及任务的隔离;支持虚拟8086方式,便于执行8086代码。

  1、80386 保护方式的寻址

  在保护方式下,当寻址扩展内存中的数据和程序时,仍然使用偏移地址访问位于存储器内的信息,但保护方式下的段地址不再像实方式那样有段寄存器提供,而是在原来放段地址的段寄存器里含有一个选择子,用于选择描述符表内的一个描述符。描述符描述存储器的位置、长度和访问权限。

  保护方式下有两个描述符表:全局描述符表和局部描述符表。全局描述符表包含适用于所有程序的段定义,而局部描述符表通常用于唯一的应用程序。每个描述符表包含8129个描述符,所以任何时刻应用程序最多可用16384个描述符。

  每个描述符长8字节,全局和局部描述符表每个最长为64kb。

  80386描述符格式:
这里有表格

  描述符的基地址指示存储段的起始位置,32位的基地址允许段其始于4GB存储器的任何地方。

  段界限包含该段中最后的偏移地址,20位的段界限使得段的长度为1KB至1MB之间或者4KB至4GB之间。描述符中特征位:G位(粒度位),若G=0,说明段的界限为0000H到FFFFFH(从0到1MB),若G=1,段的界限值要乘4K段的长度为4KB到4GB。

  若D=0,则指令是16位的,意味着使用16位偏移地址和默认的16位寄存器;若D=1,则指令就位32位。

  访问权限字节具体如下:
有表格


   P=0 描述符没有定义       P=1 段包含有效基地址和界限值
   DPL         描述符的优先级   
   S=0 系统描述符            S=1 代码或数据段描述符
   E=0 描述符描述数据段   
   ED=0 段向上扩展(数据段) ED=1 段向下扩展(堆栈段)  
   W=0 数据不能被写入        W=1 数据可以被写入
   E=1  描述符描述代码段     
   C=0 忽视描述符的优先级    C=1 遵循描述符的优先级
   R=0 代码段不能被读        R=1 代码段可以被读
   A=0 段未被访问            A=1 段已经被访问

  保护方式操作期间段寄存器的内容:
有表格

  选择子从8192个全局或局部描述符表,为了访问和指定这些表的地址,微处理器中包含了一些程序不可见寄存器。在保护方式下,这些寄存器控制着微处理器。

  在保护方式下,每个段寄存器都含有一个程序不可见区域,通常称为告诉缓冲器。每当段寄存器中的内容改变时,基地址、界限和访问权限就装入段寄存器中的不可见区域。这就允许微处理器重复访问一个段时,不必每次都去查询段描述符表。
有表格

  全局描述符表的基地址和界限包含爱GDTR(全局描述符表寄存器)中。全局描述符表的最大长度为64K,因此描述符表的界限为16位。要使微处理器工作与保护方式,全局描述表的基地址和界限要装入GDTR。
有表格

  而局部描述符表的位置是从全局描述符表中选择。为了寻址局部描述符表,必须建立一个全局描述符。为了访问局部描述符表,必须将选择子装入LDTR(局部描述符表寄存器),如同选择子装入段寄存器一样。这个选择子访问全局描述符表,并且将局部描述符表的基地址、界限和访问权限装入LDTR的告诉缓冲存储区。
有表格

  分页机制式存储管理机制的第二部分。分页机制在段机制之后进行操作,以完成虚拟地址到物理地址的转换。段机制把虚拟地址转换为线性地址,分页机制进一步把线性地址转换为物理地址。

  分页机制由微处理器中控制寄存器的内容控制。分页机制由CRO中的PG位启用。若PG=1,启用分页机制。PG=0,紧用分页机制,直接把段机制生成的线性地址当作物理地址。


  CR1 保留未用
  CR2 中存放出现页故障时的线性地址
  CR3 中的PCD和PWT位控制微处理器PCD和PWT引脚的操作。
如果PCD置位,则PCD引脚在非分页总线周期变为逻辑1,这
就允许外部硬件控制二级高速缓冲存储器。PWT位也在非分
页周期出现在PWT引脚上,用于控制写直达系统中的高速缓
冲存储器。CR3还包含页目录基地址(CR3的高10位),用于
定位页转换部件的页目录。这个基地址将页目录定位于任何
以4KB为边界的存储器中。

  由软件生成的线性地址分为三部分,分别用于寻址页目录项、页表项和页偏移地址。
线性地址: 有表格

  页目录表存储在一个4k字节的页中。页目录表共有1024个表项,每个页目录表项为4字节长,并指向一个页表。线性地址的高10位用量产生对页目录表的索引,由索引得到的表项重,指定并选择了1024个4字节的表项,每个表项用以选择对应的物理页。

  页目录和页表项:
有表格

  页目录和页表项高20位包含物理地址的高20位,而低12位包含页的属性。
有表格

    *   P 存在位,该位表示表项对地址转换有效(P=1)或无效(P=0)。在页转换期间,遇到无效表项,都会产生异常。若P=0,表项中的其余各位80386不对其做解释,可供软件使用。事实上,Linux在页面交换时就采用这种方法。当一个页面在内存中时,页面表中的表项的P位为,表示页面在内存中,而其余各位表示页面的各种属性。当一个页面被交换到了磁盘上,则相应的表项不再指向一个物理页面,而变成了一个磁盘页面,指示这个页面的去向。由于此时最低位为0,表示页面不再内存中,所以微处理器中的MMU单元对其各位都忽略不顾,而Linux内核中,就用它来唯一的确定一个页面在磁盘上的位置,包括在哪个文件或设备,以及页面在此文件汇中的位置。
    *   R/M 该/写位。若该位为1,对页表指定的页可进行读、写或执行;若该位为0,该页可读或执行,但不能对该页进行写操作。然而读写位并不总起作用。但微处理器在超级特权级之一(0、1或2级)执行时,R/M位被忽略。在目录表项中的R/M位,应用于该目录表项所映射的所有页。
    *   U/S 用户/系统位。若该委为1,页表指定的页可由在任何特权级下执行的程序访问;若该位为0,则该页只能在超级特权级执行阿程序访问。在目录表项中的U/S位,应用于该目录表项映射的所有各页。
    *   PMT 写直达位。控制用写直达或写回策略应用于页表或页面缓存管理。若该位为1,页表或页面采用写直达的缓存策略;若该位为0,采用写回策略。
    *   PCD 禁用高速缓冲位。若该位为1,页表和页面不用高速缓存;若该位为0,则可用高速缓存。当CRO中的CD位(Cache Disable)被置位,微处理器将忽略PCD位。
    *   A 访问位。若该位为1,表明指向的页表和页面已经被访问过(读或写);反之,指向的页表和页面未被访问。
    *   D 表明该页是否被写过。内存管理机制把某页调入物理内存时,将该位复位;当微处理器对其有写操作时,该位将会被置位。
    *   AVL 供软件使用。

  线性地址最低的12位和从页表项中的物理地址(高20位)形成了最后的物理地址。

  2 保护方式下的中断

  保护方式使用一组存储在中断描述符表(IDT,interrupt descriptor table)中的256个中断描述符取代实方式下的中断向量。中断描述符表为256*8(2K)字节长,每个描述符长8字节。中断描述符表由中断描述符表地址寄存器(IDTR)定位于系统中任一存储单元。IDTR长48位,其中32位的基地址规定IDT的基地址,16位的界限规定IDT的段界限。由于80386只支持最多256个中断/异常,所以IDT表最大长度是2K,以字节单位的段界限为7FFH。

  描述符项又称为“门”,意思是当中断发生时必须通过这些门,才能进入相应的服务程序。但是,这样的门并不是光为中断而设的,只要想切换CPU的运行状态,即其优先级别,例如从用户的3级进入系统的0级,就都要通过一道门。而从用户态进入系统态的途径也不只限于中断(或异常,或陷阱),还可以通过子程序调用指令CALL和转移指令JMP来达到目的。

  按不同的用途和目的,CPU一共有四种门,即任务门(task gate)、中断门(interrupt gate)、陷阱门(trap gate)和调用门(call gate)。除任务门外,其他三种门的结构基本相同,不过调用门并不是与中断向量表相联系的。

  中断门、陷阱门和调用门的结构如下:
有表格

  三种门之间的不同之处在于3位的类型码。中断门的类型码是110,陷阱门的类型码为111,而调用门的类型码100。中断门和陷阱门在使用上的区别不在于中断是外部产生的或是由CPU产生的,而是在于通过中断门进入中断服务程序是CPU会自动关闭中断,也就是将CPU中EFLAG寄存器的IF标志位清成0,以防止嵌套中断的发生;而在通过陷阱门进入服务程序时维持IF标志位不变。这就是中断门和陷阱门的唯一区别。

  任务门的结构如下:
有表格

  TSS段选择码的作用和段寄存器等相似,通过GDT指向特殊的“系统段”中的一种,称为“任务状态段”(task_state segment)TSS。TSS 实际上是一个用来保护任务运行“现场”的数据结构,其中包括CPU中所有和具体进程有关的寄存器的内容,还包含了三个堆栈指针。中断发生时,CPU在中断向量表找到相应的表项。如果此表项是一个任务门,并且通过了优先级的检查,CPU就会将当前任务的运行现场保存在相应的TSS中,并将任务所指向的TSS作为当前任务,将其内容装入CPU的各个寄存器,从而完成一次任务的切换。为此,CPU中又增设了一个“任务寄存器”TR,用来指向当前任务的TSS。在linux内核中,一个任务就是一个进程,但是进程的“控制块”中存放更多的信息。所以,从这个意义上讲,linux的进程并不完全按照Intel设计的意图中的任务。

  不管是什么门,都通过段选择码指向一个存储段。段选择码的作用于普通的段寄存器一样。对于中断门、陷阱门和调用门来说,段描述表中相应表项显然应该是一个代码段描述项,而任务门所指向的描述项,则是专门为TSS而设的TSS描述项。

  每个段描述项中都有一个DPL位段,即“描述项优先级别”位段。当CPU通过中断门找到一个代码段描述项,并进而转入相应的服务程序时,就把这个代码段描述项装入CPU中,而描述项DPL就变成了CPU当前运行级别,称为CPL。

 
 楼主| 发表于 2003-2-26 03:48:26 | 显示全部楼层
 在linux中,当通过一条INT指令进入一个中断服务器程序时,在指令中给出一个中断向量。CPU先根据该中断向量找到一扇门。然后,就要将这个门的DPL与CPU的CPL相比,CPL必须小于或等于DPL,也就是优先级别不低于DPL,才能穿过这扇门。不过,如果中断是由外部产生或因CPU异常而产生的,那就免去了这一层检查。穿过门之后,还有进一步将目标代码段描述项中的DPL和CPL比较,目标段的DPL必须小于或等于CPL。也就是说,通过中断门是只允许保持或提升CPU运行级别,而不允许降低其运行级别。这两个环节中的任何一个失败都会产生一次一般保护异常(general protection exception)。

  进入中断服务程序时,cpu要将当前EFLAGS寄存器的内容以及返回地址压入堆栈,返回地址是由段寄存器CS的内容和合取指令指针EIP的内容共同组成的。如果中断是由异常引起的,则还有将一个表示异常原因的出错代码也压入堆栈。进一步,如果中断服务程序的运行级别,也就是目标代码段的DPL,与发生中断时的DPL不同,那就要引起更换堆栈。

  3 Linux 汇编代码

  在linux内核的源代码中,以汇编语言编写的程序或程序段,有两种不同的形式。

  第一种事完全的汇编代码,这样的代码采用.s作为文件的后缀。事实上,尽管是完全的汇编代码,现代的汇编工具也吸收了C语言的长处,也在汇编之前加上了一趟预处理,而预处理之前的文件则以.s为后缀。此类(.s)文件也和C程序一样,可以使用#include、#ifdef等等成分,而数据结构也一样可以在.h的文件中加以定义。

  第二种是嵌在C程序中的汇编语言片断。虽然在ANSI的C语言标准中并没有关于汇编片段的规定,事实上各种实际使用的C编译中都作了这方面的扩充,而GNU的C编译gcc也在这方面作了很强的扩充。

  在DOS/windows领域中,386汇编语言都采用Intel定义的语句格式。可是,在Unix领域中,采用的却是由AT&T定义的格式。

  AT&T的汇编与Intel的汇编主要有以下的区别:

   1. 在Intel格式中大多使用大写字母,而在AT&T格式中都使用小写字母。
   2. 在AT&T格式中,寄存器名要加上“%”作为前缀,而在Intel格式中不带前缀。
   3. 在AT&T的386汇编语言中,指令的源操作数的顺序与在Intel的386汇编语言中正好相反。
   4. 在AT&T格式中,访问指令的操作数的宽度有操作码名称的最后一个字母(操作码的后缀决定)。用作操作码后缀的字母有b(8位)。 w(16位)和1(32位)。而在Intel格式中,则是在表示内存单元的操作数前面加上“BYTE PTR”“WORD PTR”,“DWORD PTR”来表示。
   5. 在AT$T格式中,直接操作数要加上“$”作为前缀,而在Intel格式中则不带前缀。
   6. 在AT$T格式中,绝对转移和调用指令jump/call的操作数要加上“*”作为前缀,而在intel格式则不带。
   7. 远程的转移指令和子程序调用指令的操作码名称,在AT$T格式中为“ljump”和“lcall”,而在intel格式中,则为“JMP FAR”和“CALL FAR”当转移和调用的目标为直接操作数时,两种不同的表示如下:
          *  CALL FAR SECTION:OFFSET(Intel 格式)
          *  JMP FAR SECTION:OFFSET(Intel 格式)
          *  lcall $section,$offset (AT$T格式)
          *  lcall $secton,$offset (AT$T格式). 与之相应的远程返回指令,则为:
          *  RET FAR STACK_ADJUST (Intel 格式)
          *  Lret $stack_adjust (AT$T 格式)
   8. 间接寻址的一般格式,两者的区别如下:
          *  SECTION :[BASE+INDEX*SCALE+DISP](Intel 格式)
          *  Section: disp(base,index,scale)(AT$T 格式)
          *  当需要在C语言的程序中嵌入一段汇编语言程序时,可以使用gcc提供的“asm”语句功能。

  一般而言,往C代码中插入汇编语言的代码片要比“纯粹”的汇编语言代码复杂的多,因为这里有个怎样分配使用寄存器,怎样与C代码中的变量结合的问题。为了这个目的,必须对所用的汇编语言作更多的扩充,增加对汇编工具的指导作用。其结果是其语法实际上变成了既不同于汇编语言,也不同于C语言的某种中间语言。

  插入C代码的一个汇编语言代码片段可以分为四个部分,以“:”号加以分隔,其一般形式为:


指令部: 输出部:输入部:损坏部

  第一部分就是汇编代码本身,其格式和在汇编语言中使用基本相同。这一部分称为“指令部”,是必须有的,而其他部分可视具体情况而省略。

  当将汇编语言代码片段嵌入到C代码中时,操作数与C代码中的变量如何结合显然是个问题。Gcc采用的策略是:程序员提供具体的指令,而对寄存器的使用则一般只提供一个“样板”和一些约束条件,而把到底如何与变量结合的问题留给了gcc和gas处理。

  在指令部中,数字加上前缀%,如%0、%1等等,表示需要使用的寄存器的样板操作数。可以使用的此类操作数的总数取决于具体CPU中通用寄存器的数量。这样,指令部中用到了几个不同的这种操作数,就说明有几个变量需要与寄存器结合,由gcc和gas在编译和汇编时根据后面的约束条件自行变通处理。由于这些样板操作数也使用“%”前缀,在涉及到具体的寄存器时就要在寄存器名前面加上两个“%”符,以免混淆。

  紧接在指令部后面的是“输出部”,用以规定对输出变量如何结合的约束条件。每个这样的条件称为一个“约束”。必要是输出部可以有多个约束,互相以逗号分隔。每个输出约束以“=”号开始,然后是一个字母表示对操作数类型的说明,然后是关于变量结合的约束。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行嵌入的汇编代码后均不保留执行之前的内容,这就给gcc提供了调度这些寄存器的依据。

  输出部后面是“输入部”。输入约束的格式和输出约束相似,但不带“=”号。如果一个输入约束要求使用寄存器,则在预处理时gcc会为之分配一个寄存器,并自动插入必要的指将操作数即变量的值装入该寄存器。与输入部中说明的操作数结合的寄存器或操作数本身,在执行嵌入汇编代码后也不保留执行之前的内容。

  在有些操作中,除用于输入数据操作和输出数的寄存器以外,还要将若干个寄存器用于计算或操作的中间结果。这样,这些寄存器原有的内容就损坏了,所以要在损坏部队操作的副作用加以说明,让gcc采取相应的措施。

  操作数的编号从输出部的第一个约束(序号为0)开始,顺序数下来,每个约束记数一次。在指令部中引用这些操作或分配用于这些操作数的寄存器时,就在序号前面加上一个“%”号。在指令部中引用一个操作数时总是把它当作一个32位的“长字”,但是对其实施的操作,则根据需要也可以是字节操作或是字操作。对操作数进行的字节操作时也允许明确指出是对哪一个字节的操作,此时在%与序号之间插入一个“b”表示最低字节,插入一个“h”表示次低字节。

  表示约束调节的字母有很多:


“m”“v”“o” 表示内存单元
“r” 表示任何寄存器
“q” 表示寄存器eax,ebx,ecx,edx之一
“i”和“h” 表示直接操作数
“E”和“F” 表示浮点数
“g”表示任意
“a”,“b”,“c”,“d” 分别表示寄存器eax,ebx,ecx,edx
“S”和“D” 分别表示寄存器esi,edi
“I”  表示常数(0至31)

  此外,如果一个操作数要求使用与前某个约束中所要求的是同一个寄存器,那就把与那个约束相对应的操作数标号放在约束条件中。在损坏部常常会以“memory”为约束条件,表示操作完成后内存中的内容已有改变,如果原来某个寄存器(也许在本次操作汇总并未使用到)的内容来自内存,则现在科能已经不一致。

  还要注意,当输出部为空,即没有输出约束时,如果有输入约束存在,则必须保留分隔标记“:”号。

  4.实方式向保护方式的转换

  80386在开机后,首先是在实方式下,而要完全发挥80386的功能,就必须使80386进入保护方式。为了将80386从实方式转换到保护方式,必须遵循一些步骤。在硬件复位或将CR中的PE位变成逻辑0后,微处理器进入实方式。通过给CRO寄存器中的PE位置为1,微处理器将进入保护方式,但在进入保护方式之前,必须对其他方面做好初始化。下面的步骤将完成从实方式到保护方式的切换:

   1. 初始化全局描述符表(GDT),使其描述符0为一个空描述符,并且使其至少包含一个有效的代码段描述符。
   2. 将CRO的PE位置位,切换到保护方式。
   3. 执行一条跳转指令清楚内部指令队列并把TSS描述符基址装到TR中。
   4. 将初始选择子的值装到段寄存器中。
   5. 现在80386已经运行在保护方式下。

  下面的一个汇编程序是在DOS的环境下,从实方式进入保护方式。全局描述符表中共有四个选择子,依次为空描述符、代码段选择子、数据段选择子和屏幕段选择子。其中空描述符时Intel微处理器编程要求,而屏幕段选择子是为了直接写屏法在屏幕上显示信息而设置的。


dseg segment
gdt      db   20h,00h,  00h,00h,00h,00h
;             ----+---  ------+---------
;                 |            GDT表的基地址
;                 |  
;                 +------------GDT表的长度,四个段8*4=32=20h
gdtt     db   000h,000h,000h,000h,000h,000h,000h,000h;空
         db   0ffh,0ffh,000h,000h,000h,09eh,000h,000h;代码段
         ;limit=   0ffff
         ;base =00000000
         ;flag =9e
         ;G,D,O,AVL=0
         db      0ffh,0ffh,000h,000h,000h,092h,000h,000h;数据段
         ;limit=   0ffff
         ;base =00000000
         ;flag =92
         ;G,D,O,AVL=0
         db      0ffh,0ffh,000h,080h,00bh,093h,000h,000h;屏幕段
          limit=   0ffff
         ;base =000b8000  显示存储区基地址
         ;flag =93
         ;G,D,O,AVL=0
pm       db     'Enter Protect Mode!'
rm       db     0dh,0ah,'Return Real Mode!',0dh,0ah,'$'
dseg ends
cseg segment
   assume cs:cseg,ds:dseg
.386p
start:
   mov ax,dseg
   mov ds,ax
   mov ax,0600h
   mov bx,0700h
   mov cx,000h
   mov dx,184fh
   int 10h
   ; 清屏,ah=06h,int 10h 向上滚屏
   
   mov ah,02h
   mov bh,00h
   mov dx,0100h
   int 10h
   ;置光标(1,0),dh=01h,dl-00h
  
  mov ax,dseg
  mov ds,ax
  mov ax,cs
  mov es,ax
  
  xor eax,eax
  xor ebx,ebx
  mov ax,ds
  mov cl,04h
  shl eax,cl
  mov bx,offset gdtt
  add eax,ebx
  mov bx,offset gdt+2
  mov ds:[bx],eax
  ;把gdtt的基地址填入gdt中对应的位置

  nop
  
  mov ax,dseg
  mov ds,ax
  xor eax,eax,
  xor ebx,ebx
  mov ax,cs
  mov cl,04h
  shl eax,cl
  add eax,ebx
  mov bx,offset gdtt
  mov ds:[bx+0ah],eax
  ;把代码段的基地址填入gdtt中对应的位置

  mov ax,dseg
  mov ds,ax
  xor eax,eax
  xor ebx,ebx
  mov ax,ds
  mov cl,04h
  shl eax,cl
  add eax,ebx
  mov bx,offset gdtt
  ;把数据段的基地址填入gdtt中对应的位置

   mov ax,dseg
   mov ds,ax
   mov ax,cs
   mov es,ax
   mov bx,offset gdt
   xor ecx,ecx
   cli
   cli
   lgdt fword ptr ds:[bx]
   ;载入gdt
   
   mov ax,dseg
   mov ds,ax
   mov eax,cr0
   or eax,01h
   mov cr0,eax
   jmp protection    ;进入保护方式

protection:
   db 66h
   mov ds,ax
   mov si,offset pm
   mov bx,0018h
   ;0000 0000 0001 1000 全局描述符表,第三描述符表项,权限00
   mov es,bx
   mov di,0000h
   mov cx,0014h;14h=20 pm的长度为20,设置循环次数为20
   mov ah,0ah;设置显示属性
  
   mov edx,cr0
   and edx,1
   cmp edx,1
   jnz go1
show:
   lodsb;源数据串ds:si 把ds:si的内容放入al,ah中放有显示属性
   stosw:目的数据串es:di 把ax的内容放入显示存储区
   ;将DSM搬到0018:00000000 即屏幕段
   ;(0010的区段=B8000 请参考GDT表)
   loop   show
go1:
   mov eax,cr0
   and al,not 1
   mov cr0,eax
   jmp real;返回实方式
real:
   sti
   mov edx,cr0
   and edx,1
   cmp edx,0
   jnz go2
  
   mov ax,dseg
   mov ds,ax
   mov ah,09h
   mov dx,offset rm
   int 21h
go2:
   mov ah,4ch
   int 21h
cseg ends
   end start
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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