80386ASM程序设计基础

    80386ASM程序设计基础   
 kingcaiyao
 
 
    80386 ASM程序设计基础,呵呵,这是最近一段时间我的业余爱好。本期将连续推出若干篇有关80386ASM程序设计的基础,主要介绍80386ASM指令的详细用法及如何在80386实模式下,保护模式下及虚拟8086模式编程以及我会详细介绍80386下的段页管理机制,我会将80386下的指令与8086下的相同指令进行比较。在你去看罗云彬的ASM编程之前,不妨先看看我的基础篇,希望有志于从事汇编语言的朋友,多提意见。
    80386处理器是Intel公司80x86发展史上的里程碑,它不但兼容先前的8086/8088,80186,80286处理器,而且也为后来的486,Pentium(586),Pentium Pro(686)的发展打下了坚实的基础,对于我们程序员来讲更重要的是:我们关心80386在指令上到底有哪些扩展呢?80386有哪些寻址方式呢?毫无疑问,它不但兼容了8086的所有指令,而且还对它们进行增强.
    呵呵,我知道有很多人问我CPU已经发展到PentiumIIII,没有必要学习80386的汇编。其实不然,80386处理器中的保护模式,虚拟8086模式以及地址的段页管理机制,虚拟内存这些都是以后处理器的核心。所以说80386是后续发展处理器的基础,比如说80486实质上80386+80387协处理,这块协处理器主要用于处理浮点运算,Pentium处理器在80386指令的基础上增加了57条指令,8个数据类型,8个64位的寄存器来处理多媒体。从这一点来看,完全有必要了解80386ASM,这就好像学习80386,必须先要熟练掌握8086。
    1.80386的的寄存器:
    80386的寄存器可以分为8组:通用寄存器,段寄存器,指令指针寄存器,标志寄存器,系统地址寄存器,控制寄存器,调试寄存器,测试寄存器,它们的宽度都是32位的。本篇主要介绍80386的寄存器。
    A1.General Register(通用寄存器)
    EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP,它们的低16位就是8086的AX,BX,CX,DX,SI,DI,SP,BP,它们的含义如下:
    EAX:累加器
    EBX:基址寄存器
    ECX:计数器
    EDX:数据寄存器
    ESI:源地址指针寄存器
    EDI:目的地址指针寄存器
    EBP:基址指针寄存器
    ESP:堆栈指针寄存器
    这些寄存器可以将低16位单独存取,也就是8086的AX,BX,CX,DX,SI,DI,SP,BP,在存取这些寄存器的低16位(AX,BX,CX,DX,SI,DI,SP,BP),它
们的高16位不受影响,同时和8086一样对于AX,BX,CX,DX这四个寄存器来讲,可以单独存取它们的高8位和低8位(AH,AL,BH,BL,CH,CL,DH,DL)
   
    A2:Segment Register(段寄存器)
    除了8086的4个段外(CS,DS,ES,SS),80386还增加了两个段FS,GS,这些段寄存器都是16位的,它们的含义如下:
    CS:代码段(Code Segment)
    DS:数据段(Data Segment)
    ES:附加数据段(Extra Segment)
    SS:堆栈段(Stack Segment)
    FS:附加段
    GS  附加段
   
    A3:Instruction Pointer(指令指针寄存器)
    EIP,它的低16位就是8086的IP,它存储的是下一条要执行指令的地址。
   
    A4:Flag Register(标志寄存器)
    EFLAGS,和8086的16位标志寄存器相比,增加了4个控制位,不过这4个控制位它们在实模下不起作,这四个控制位分别是:
    a.IOPL(I/O Privilege Level),I/O特权级字段,它的宽度为2bit,它指定了I/O指令的特权级。如果当前的特权级别在数值上小于或等于IOPL,那么I/O指令可执行。否则,将发生一个保护性异常。   
    b.NT(Nested Task):控制中断返回指令IRET,它宽度为1位。NT=0,用堆栈中保存的值恢复EFLAGS,CS和EIP从而实现中断返回;NT=1,则通过任务切换实现中断返回。
    c.RF(Restart Flag):重启标志,它的宽度是1位。它主要控制是否接受调试故障。RF=0接受,RF=1忽略。如果你的程序每一条指令都被成功执行,那么RF会被清0。而当接受到一个非调试故障时,处理器置RF=1。
    d.VM(Virtual Machine):虚拟8086模式(用软件来模拟8086的模式,所以也称虚拟机)。VM=0,处理器工作在一般的保护模式下;VM=1,工作在V8086模式下。
    其它16个标志位的含义和8086一样,在这里也重温一遍:
    e.CF(Carry Flag):进位标志位,由CLC,STC两标志位来控制
    f.PF(Parity Flag):奇偶标志位
    g.AF(Assistant Flag):辅助进位标志位
    h.ZF(Zero Flag):零标志位
    i.SF(Singal Flag):符号标志位
    j.IF(Interrupt Flag):中断允许标志位,由CLI,STI两条指令来控制
    k.DF(Direction Flag):向量标志位,由CLD,STD两条指令来控制
    l.OF(Overflow Flag):溢出标志位。
    控制寄存器,系统地址的寄存器,调试寄存器,测试寄存器将在介绍完80386分段,分页管理机制后介绍,请继续关注第二篇“80386存储器的寻址方式”。
 
   
    80386处理器的寻址方式
    在实式模式下,80386处理器的最大寻址空间仍然为1M,和8086/8088相似。即段地址*10H+段内偏移地址,从而形成20位地址。此种模式下,段基址是16的倍数,长度最大不超过64K。
    在保护模式下,80386处理器可以使用所有的物理内存。段基址可以是32位,也可以不是16的倍数,同时它的最大长度为4G,这与8086完全不同,在形成逻辑地址时用段基址直接加上段内偏移地址,而并不将段基址左移4位(乘以16)。通常情况下,除了访问堆栈外,默认的段都为DS,有跨段前缀就另当别论了。在以BP,EBP,ESP作为基址寄存器时,这时默认的段寄存器应该是SS,举几个简单的例子:
   MOV EAX,[SI];这里的段寄存器是DS
   MOV EAX,FS:[ESI];这里的段寄存器是FS,因为指令中使用跨段前缀显示指定了
   MOV EAX,[BP];这里的段寄存器是SS,因为指令中使用了BP作为基址寄存器
   MOV EAX,GS:[BP];这里段寄存器是GS,因为指令中使用跨段前缀显示指定了
   80386中32位数的操作的顺序是“高高低低”,即是说高16-》高16,高8-》高8,低16-》低16,低8-》低8,这和8086相似。同时80386微处理器兼容所有8086的寻址方式,而且对8086的寻址方式有很大的改进和扩展。在8086下,只允许BP,BX,SI,DI作为寻址寄存器,但在80386下,8个通用寄存器都可以作为寻址寄存器。不过有一点要注意的是在基址变址寄存器寻址方式或相对基址变址寻址方式中,段寄存器由基址寄存器来确定,而不是由变址寄存器来确定,同时除ESP外其它的7个通用寄存器都可以作为变址寄存器,用代码来表示就是:
   MOV EAX,[EBP+ESP+2];这条指令是错误的,因为不可以用ESP作为变址寄存器
   MOV EAX,[EBP+ESI+10H];这里的段寄存器应该有基址寄存器来决定。基址寄存器是BP,那么这里的段寄存就是SS
   MOV EAX,GS:[EBP+EDI+100H];不用看了,这里的段寄存器应该是GS,因为指令通过跨段前缀显示指定了
   80386支持的基地址+变址+位移量寻址进一步满足了高级语言支持的数据类型。对于C语言来讲,普通变量,数组,结构体,结构体的数组,数组的构体我们既可存放在栈中(静态定义-static definition),也可以存放在堆中(动态定义-dynamic definition),用ASM也一样可以实现。基址变址寄存器提供了两个可以改变的部分,而位移量则是静态的。看下面的例子:
   //Variables in C Programming-Language,the corresponding ASM will list below
   void main()
   {
     int a;//普通的变量,用ASM寻址时直接用DS:[一位移量],如DS:[2000],属于直接寻址方式
     int array[24];//数组,用ASM寻址时用DS:[BX+SI*4],4表示整型的长度,属于基址变址寻址方式
     struct abc
     {
       int a,b,c;
       float d;
     };
     struct abc aa;//结构体,用ASM寻址时DS:[BX+Shift],Shift代表位移量,属于寄存器相对寻址方式
     struct abc aa[100];//结构体数组,用ASM寻址时用DS:[BX+SI*sizeof(abc)+Shift],属于相对基址变址寻址方式
     struct cde
     {
          int array[100];
          float e,f,g;
     };
     struct cde ccc;//数组结构体,用ASM寻址时用DS:[BX+SI*4+Shift],属于相对基址变址寻址方式
   }
   80386与8086的寻址方式差不多完全一样,只不过80386的寻址方式更灵活,它的操作数有32位,16位,8位。
   让我们再重温一下8086的寻址方式:
   a.立即寻址,所谓立即寻址就是操作数就在指令中,比如说:MOV AX,5678H
   b.直接寻址,即直接包含操作数的有效地址EA,比如说MOV AX,[1234]
   c.寄存器间址寻址,用寄存器的内容来作为操作数的有效地址,比如说SI=1234,MOV AX,[SI],8086下可用的寄存器只有4个:BX,BP,SI,DI,80386下8个通用的寄存器都可以使用。
   d.寄存器相对寻址,即在寄存器间址寻址方式的基础上再加一个位移量,位移量可以是8位也可以是16位,比如说MOV AX,[BX+90H]。
   e.基址变址寻址,即操作数的有效地址由一基址寄存器和一变址寄存器产生,如MOV AX,[BX+SI]。那么在8086下,只有SI,DI可以作为变址寄存器,在80386下除ESP外的其它7个通用寄存器都可以作为变址寄存器,比如说MOV AX,[BX+SI]。
   f.相对基址变址寻址,在e寻址方式的基础上加上一位移量,比如说MOV AX,[BX+SI+100H]。
   在8086下,我们如进行字节或字操作,往往要加上伪指令WORD PTR或BYTE PTR。在80386下不用显示指定,处理器会自动处理,当发现目的操作为8位时,处理器就会进行8位操作,同理当发现目的操作为16位,处理器就会进行16位操作,80386下以目的操作数的长度为准,以下几条简单的传送指令:
   MOV AL,CS:[EAX];8位操作,段寄存器是CS,寻址方式是寄存器间址寻址
   MOV AL,ES:[BX];8位操作,段寄存器是ES,寻址方式是寄存器间址寻址
   MOV EDX,[EDX+EBX+1234H];32位操作,段寄存器是DS,寻址方式是相对基址变址寻址
   MOV AX,[EBX+ESI*4];16位操作,段寄存器是DS,寻址方式是基址变址寻址
   MOV BH,ES:[EBX+EDI+900H];8位操作,段寄存器是ES,寻址方式是相对基址变址寻址
   MOV DL,[EBP+ESI+1900H];8位操作,段寄存是SS,因为用了EBP作为基址寄存器。寻址方式是相对基址变址寻址

   在接下来的四篇里将介绍80386的汇编指令及用法,并和8086的指令进行比较。
   80386的指令集包含了8086/8088,80186,80286的指令集,可以分为几个大类:数据传送指令,算术运算/逻辑运算指令,移位指令,控制转移指令,串操作指令,高级语言支持的指令,条件字节设置指令,位操作指令,处理器控制指令和保护方式指令。高级语言支持指令始于80186,保护方式指令始于80286,条件字节设置指令和位操作指令是80386新增的。
  本篇主要介绍数据传送指令,数据传送指令可以分为:通用数据传送,累加器专用传送,地址传送,标志传送,分别介绍如下:
  A.数值传送指令MOV,MOVZX,MOVSX,XCHG,PUSH,PUSHA,PUSHAD,POPA,POPAD,
  a.MOV,指令和8086相似,不过它支持32位操作。
  b.MOVZX,零扩展传送,格式--MOVZX DST,SRC,表示将源操作送给目的操作数,目的操作数空出的部分用0填补。
  c.MOVSX,符号扩展传送,格式--MOVSX DST,SRC,表示将源操作送给目的操作数,目的操作数空出的部分用SRC的符号位来填补,举个简单的例子来演示:
  MOV DL,90H;
  MOVSX AX,DL;AX=FF90H
  MOVZX AX,DL;AX=0090H
  MOVSX ESI,DL;ESI=FFFFFF90H
  MOVZX ESI,DL;ESI=00000090H
  事实上在8086中也有两条指令CBW,CWD可以对操作数进行扩展。MOVSX可以对有符号数进行扩展,MOVZX可以对无符号数进行扩展,看看CBW,CWD的用法:
   CBW将字节数据扩展成字,符号位扩展到AH中
   CWD将字数据扩展成双字,符号位放到DX中
   MOV AL,70H;
   CBW;//AX=0070
   CWD;//DX=0000,AX=0070
  d.XCHG,功能和8080相同,不过它支持8位,16位,32位操作,下面的语句均是合法的。
   XCHG AH,AL
   XCHG AX,AL
   XCHG ESI,EDI
   XCHG ESI,[EBX+EDI+1000H]
  e.PUSH,和8086不同的是,它支持立即数入栈,8位入栈,当然还有32位入栈,下面的语句均是合法的。
   PUSH AL
   PUSH BH
   PUSH 100H
   PUSH EAX
   PUSH EBX
   PUSH DWORD PTR [EAX]
  f.POP,功能和用法和8086一样。
  g.PUSHA,将8个通用寄存器全部进栈,进栈顺序为:AX,CX,DX,BX,SP,BP,SI,DI,然后SP指针寄存减16,不过SP入栈的内容是PUSHA指令执行前的内容。
  h.POPA,8个通用寄存器全部出栈,堆栈指针寄存器不是堆栈中弹出的内容,而是加16而得到的,虽然这样得到的值和从堆栈中弹出来的内容一样,但物理意义不一样。
  i.PUSHAD,将8个32位通用寄存器全部入栈,入栈顺序EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI,ESP的内容是执行指令PUSHAD之前的内容
  j.POPAD,8个32位通寄存器全部出栈,ESP的内容参见h
 
  B.地址传送指令LEA,LDS,LES,LFS,LGS,LSS
  a.LEA,取有效地址,功能,用法与8086相同,不过它支持32位操作。规则:目的操作必须是16位或32位通用寄存器,当目的操作数是16位时,那么只装入有效地址的低16位。事实上LEA指令相当于伪指令OFFSET,看例子:
  MOV EAX,12345678H
  MOV EBX,56784321H
  LEA ECX,[EAX+EBX];ECX=99999999H
  b.LDS,装入指针,功能,用法与8086相同,不过它支持32位操作。格式:LDS REG,OPRD。规则,目的寄存器必须是16位或32位的通用寄存器,OPRD必须是内存单元,不可以是立即数。如果目的寄存器是16位,那么源操作数OPRD含32位指针;如果目的寄存器是32位,那么源操作数有48位指针。该指令将目的操作数OPRD所指向的内存单存的4个或6个连续字节的内容送给助记符指令中指定的DS段寄存器和指令中目的寄存器。比如:
  LDS EAX,[1000H];这表明将偏移地址为1000,1001H这两个字节单元的内容送给段寄存器DS,将偏移地址1002,1003,1004,1005四个字节单元的内容送往EAX。
  LDS AX,[1000H];这表明将偏移地址为1000,1001H这两个字节单元的内容送给段寄存器DS,将偏移地址1002,1003H两个字节单元的内容送往EAX。
  c.LES,同LDS,不过段寄存器是ES。
  d.LFS,同LDS,不过段寄存器是FS。
  e.LGS,同LDS,不过段寄存器是GS。
  h.LSS,同LDS,不过段寄存器是SS。
 
  C.标志传送指令LAHF,SAHF,PUSHF,PUSHFD,POPF,POPFD
  a.LAHF,将标志寄存器的低8位送至AH中,包括SF,ZF,ZF,PF,CF。
  b.SAHF,与i的过程恰好相反
  c.PUSHF,将标志寄存器的EFLAGS低16位内容入栈,和8086相同
  d.PUSHFD,将标志寄存器EFLAGS的内容入栈
  e.POPF,将栈顶的一个字弹出,并将它送到标志寄存器EFLAGS的低16位
  f.POPFD,将栈顶的两个字弹出,并将它送到标志寄存器EFLAGS
 
  D.累加器传送指令IN,OUT,XLAT
  a.IN,和8086相同,但可以输入一个双字节,同样如果端口的范围位于00H-FFH,可以直接用,如果超出这个范围,则先要将端口号送至DX,下面的语句是合法的:
  IN AL,20H;从20H端口读入一个字节
  IN AX,20H;从20H端口读入一个字
  MOV DX,0378H
  IN EAX,DX;从20H端口读两个字节
  b.OUT,和8086相同,但可以输出一个双字节,同样如果端口的范围位于00H-FFH,可以直接用,如果超出这个范围,则先要将端口号送至DX,下面的语句是合法的:
  OUT 20H,AL;从20H端口输出一个字节
  IN 20H,AX;从20H端口输出一个字
  MOV DX,0378H
  IN EAX,DX;从20H端口输出两个字
  c.XLAT,查表指令,功能和用法与8086相同,不过基址寄存器用的是EBX,来看看XLAT的实现过程:XLAT以BX作为基址寄存器,以AL作为变址寄存进器对指定的缓冲区进行查表,将AL指定位置的内容送往AL,比如说我们在MS-DOS方式写一个小程序:
  C:/>Debug
  -A100
     MOV BX,0120
     SUB AL,AL
     MOV DL,AL
     MOV AH,2
     INT 21
     MOV AH,4C
     INT 21
     INT 20
-E120 'ABCDEFGHIJKLLMMDDKDJDK'
=G100
   屏幕上会显示A,如果AL=3,那么屏幕会显示D

   以上所有的指令均不影响EFLAGS的各标志位。

  算术运算指令,逻辑运算指令,移位指令
   AA.算术运算指令
   A.加减法运算ADD,ADC,INC,SUB,SBB,DEC,CMP,NEG
   a.ADD,和8086功能,用法相同,不过支持32位操作,下面的语句都是合法的。
   ADD  ESI,EDI
   ADD  EAX,DWORD  PTR  [1000H]
   b.ADC,带进位的加法指令,即OPRDS+OPRDD+CF,其中OPRDS代表源操作数,OPRDD代表目的操作,CF代表进位标志位,功能和用法与8086相同,支持32位操作。
   c.SUB,和8086相同,支持32位操作。
   d.SBB,带进位的减法指令,即OPRDD-OPRDS-CF,其中OPRDS代表源操作数,OPRDD代表目的操作数,CF代表进位标志位,功能和用法与8086相同,支持32位操作。
   e.DEC,减1操作,功能和用法与8086相同,支持32位操作。
   f.CMP,比较操作,功能和用法与8086相同,支持32位操作。
   g.NEG,求补操作,功能和用法与8086相同,支持32位操作。
   h.INC  加1操作,功能和用法与8086相同,支持32位操作。
  
   B.乘除法指令MUL,DIV,IMUL,IDIV
   a.MUL,无符号数乘法指令,和8086功能用法一样,即指令中只给出一个操作,被乘数已默认,如果指令给出的操作数是32位的话,被乘数默认为EAX,那么乘积将存放在EDX:EAX中,其中EDX存放高32位,EAX存放低32位,如果此时EDX=0,即高32位为0的话,那么OF=0,CF=0,否则被置1。如果指令给出的操数作是16位的话,被乘数默认为AX那么乘积将放在DX:AX中,其中DX中将存放高16位,AX中存放低16位。如果指令给出的操作数是8位的话,被乘数默认为AL,那么乘积将放在AX,AH中存放高8位,AL中存放低8位。
  b.DIV,无符号数的除法指令,和8086一样,指令给出一个操作数,被除数已默认。如果指令中给出的操作数为32,那么被除数将是EDX:EAX, 最终的商将存放在EAX, 余数将存放在EDX中。如果指令给出操作数为16位,那么被除数为EAX,最终得到的商放在AX,余数放在EAX的高16位。如果指令中给出的操作数为8位,那么被除数是16位,最终得到的商将放在AL中,余数放在AH中。
  c.IMUL,有符号数的乘法指令,除了具有8086的用法外,有新的形式:
    c1.IMUL DST,SRC;将源操作数SRC与目的操作DST相乘,并将结果送往DST。
    c2.IMUL DST,SRC1,SRC2;将源操作数SRC1与源操作数SRC2相乘,并将结果送往DST。
    使用这种形式必须遵守的规则,形式c1指令中目的操作数必须是16位或32位通寄存器,源操作数的长度必须与目的操作的长度一样(8位立即数除外,即00H-FFH或80H-7FH),源操作数可以是通用寄存器,也可以是存储单元或立即数。形式c2指令中的源操作数SRC1可以是通用寄存器也可以是存储单元,源操作数SRC2必须是立即数,DST必须是16位或32位通用寄存器。呵呵,对于这些规则无需去问为什么,这是硬件的特性决定的,如果一定要问为什么,那只能问INTEL公司的硬件工程师了:)。同时,有一点要注意的是:这两种形式的指令,目的寄存器的长度与源操作数长度一样(8位立即数除外),这样的话,该指令事实上对有符号数和无符号数是一样的,因为乘积的低位部分均存储在目的寄存器中,而高位部分在这两种形式的指令中不予以存储。
  d.IDIV,有符号数的除法指令,用法和8086相同,不过支持32位操作。  

  C.符号扩展指令CBW,CWD,CWDE,CDQ
  a.CBW,前面已介绍,在第三篇。
  b.CWD,前面已介绍,在第三篇。
  c.CWDE,是80386新增的指令。格式:CWDE。功能:将AX的符号位扩展到EAX的高16位中。
  d.CDQ,是80386新增的指令。格式:CDQ。功能,将EAX的符号位扩展到EDX中。
  e.以上四条指令均不影响标志位。
  f.举例说明:
  ;If AX=1234H,EAX=99991234H
  CBW;After processing the instruction,AX=1234,DX=0000H
  CDQ;After processing the instruction,EAX=99991234H,EDX=FFFFFFFFH

  BB.逻辑运算指令和移位指令NOT,AND,OR,XOR,TEST,SAL,SAR,SHL,SHR,ROL,ROR,RCL,RCR,SHLD,SHRD
  a.NOT,AND,OR,XOR,TEST这些指令的功能和用法与8086完全相同,不过它们支持32位操作。
  b.TEST,测试指令,该指令测试的结果并不回送到目的操作数和源操数。之所以要使用这条的指令,主要是因为根据TEST指令得到的结果,进行程序的条件转移。
  c.SAL,算术左移,功能和8086一样,但在8086中,如果在移位的位数超过1位,那么一定要移位的位数放在CX寄存器中。在80386中,可以不用这样做,其它的移位指令也一样。除了这一点以外,用法和8086一样,当然也支持32位操作。以下的语句均是合法的。
  SHL AL,5;这在8086中是非法,但在80386中是合法的
  SHL WORD PTR [SI],3
  d.SAR,算术右移,将操作数右移指定的位数,但左边的符号位保持不变,移出的最低位进入CF标志位。
  e.SHL,逻辑左移,用法和功能与SAL一样。
  f.SHR,逻辑右移,将操作右移指定的位数,同时每移一位,左边用0补充,移出的最低位进入CF标志位。
  g.说明:在80386中,实际移位的位数是指令中移位位数的低5位,也就是说移位位数的范围在0-31或0-1FH,CF标志位总是保留着目的操作数最后被移出位的值。当移位位数大于操作数的长度时,CF被置0。如果移位位数为1,移位前后的结果的符号位都是一样,那么很明显的是该操作数经移位后没有移出,此时OF=0。这四条指令的移位示意图(我画的是16位操作数的移位示意图,8位和32依此类推),SAL,SHL相当于乘法;SAR,SHR相当于除法。
  SAL:
  |-------------------------------------------------------------------------------------------|
  |CF|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|
  |-- ----------------------------------------------------------------------------------------|
  SHL:
  |-------------------------------------------------------------------------------------------|
  |CF|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|
  |--- ---------------------------------------------------------------------------------------|
  SAR:
    |--------------------------------------------------------------------------------------------|
  |-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|->|CF||
  | |---|----------------------------------------------------------------------------------------|
  |     ^
  |-----|最高位保持不变
  SHR:
     |--------------------------------------------------------------------------------------------|
  0->|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|->|CF||
     |--------------------------------------------------------------------------------------------|
  h.ROL,循环左移,支持32位操作数,用法和8086一样。
  i.ROR,循环右移,支持32位操作数,用法和8086一样。
  j.RCL,带进位的循环左移,支持32位操作数,用法和8086一样。
  k.RCR,带进位的循环右移,支持32位操作数,用法和8086一样。
  l.ROL,ROR,RCL,RCR的移位示意图(仍然以16位操作数来画,8位/32位依次类推):
  ROL:
   |--------------------------------------------------------------------------------------------------|
   |<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|<--------|
   |--------------------------------------------------------------------------------------------------|
   |--------------------------------------------------------------------------------------------------| 
  ROR:
   |-------------------------------------------------------------------------------------------|
   |->|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|->|
   |-------------------------------------------------------------------------------------------|
   |-------------------------------------------------------------------------------------------|
 RCL:
   |-------------------------------------------------------------------------------------------------|
   |<-|CF|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|<-|
   |-------------------------------------------------------------------------------------------------|
   |-------------------------------------------------------------------------------------------------|  
  RCR:
   |-------------------------------------------------------------------------------------------------|
   |->|CF|<-|bit15|bit14|bit13|bit12|bit11|bit10|bit9|bit8|bit7|bit6|bit5|bit4|bit3|bit2|bit1|bit0|->|
   |---- --------------------------------------------------------------------------------------------|
   |-------------------------------------------------------------------------------------------------|
  m.SHLD,80386新增的双精度左位指令,指令格式:SHLD OPRD1,OPRD2,M
  n.SHRD,80386新增的双精度右移指令,指令格式:SHRD,OPRD1,OPRD2,M
  o.m,n这两条指令的使用规则是:源操作数OPRD1可以是16位或32位通用寄存器或者16位存储单元或者32位存储单元,源操作数OPRD2必须是16位或32位通寄存器,M表示移位次数,可以是CL寄存器,也可以是8位立即数。功能:SHLD是将源操作数OPRD1移M位,空出的位用OPRD2高端的M位来填补,源操作数OPRD2的内容不变,最后移出的位放在CF中;SHRD将源操作数OPRD1移M位,空出的位用OPRD2低端M位来填补,源操作数OPRD2保持不变,最后移出的位放在CF中,对于这两条指令,当移位位数仅为1的话,移出和移后的符号位不变的话,那么OF=0,如果符号位不一样的话,那OF=1。
  p.这两条指令是80386新增的指令,举两个简单的例子加以说明:
  p1.SHLD:
  MOV AX,8321H
  MOV DX,5678H
  SHLD AX,DX,1
  SHLD AX,DX,2
  分析一下该指令的详细执行过程(用示意图, 第一个图画的就是AX的内容):
  AX=8321h
  |-------------------------------|
  |1|0|0|0|0|0|1|1|0|0|1|0|0|0|0|1|  
  |-------------------------------|
  根据指令SHLD AX,DX,1,先左移一位,得到AX=0642H:
  |-------------------------------|
  |0|0|0|0|0|1|1|0|0|1|0|0|0|0|1|0|    CF=1   
  |-------------------------------|
  经过上一步的移位后,AX的最后一位(即bit0)空出来,其值为0;根据指令的用法将用DX的第15位填充,填充后AX的内容为:
  |-------------------------------|
  |0|0|0|0|0|1|1|0|0|1|0|0|0|0|1|0|   
  |-------------------------------|
  同时由于移位后AX的符号位与移位前AX的符号位不同,所以在移位过程中产生了溢出,OF=1,最后结果AX=0642H。
  同理,SHLD AX,DX,2,执行完这条指令后,最后结果为AX=0644H
  p2.SHRD:
  MOV EAX,12345678H
  MOV EDX,99994599H
  SHRD AX,DX,1
  SHRD AX,DX,2
  分析一下该指令的详细执行过程(用示意图,第一个图画的是EAX的内容):
  EAX=12345678H
  |---------------------------------------------------------------|
  |0|0|0|1|0|0|1|0|0|0|1|1|0|1|0|0|0|1|0|1|0|1|1|0|0|1|1|1|1|0|0|0|
  |---------------------------------------------------------------|
  根据指令SHRD AX,DX,1,将AX右移一位得到EAX=091A2B3EH:
  |---------------------------------------------------------------|
  |0|0|0|0|1|0|0|1|0|0|0|1|1|0|1|0|0|0|1|0|1|0|1|1|0|0|1|1|1|1|1|0|
  |---------------------------------------------------------------|
  经过上一步的移位后,EAX的最高位(第31位)空出来用0填充,根据指令的用法,最EDX的第0位来填充,填充后EAX的内容为:
  |---------------------------------------------------------------|
  |1|0|0|0|1|0|0|1|0|0|0|1|1|0|1|0|0|0|1|0|0|1|1|0|0|1|1|1|1|0|0|0|
  |---------------------------------------------------------------|
  即EAX=891A2B3EH,CF=0,OF=0
  同理,指令SHRD AX,DX,2,执行完这条件指令后,最后结果为EAX=048D159C,CF=0,OF=0

  控制转移指令,串操作指令
  80386控制转移指令包括:转移指令,循环指令,过程调用和返回指令。
  A.转移指令包括无条件转移指令JMP和条件转移指令,无条件转移指令分为段内直接转移,段内间接转移,段间直接转移,段间间接转移。由于80386有保护模式和实模式,在实模式下,段内转移的范围在-128~127,段间转移最大范围为64K。在保护模式需要用48位指针,即CS:EIP(16位+32位)。条件转移指令有很多包括JCXZ,JECXZ,JBE,JAE,JA,JB等,其用法和8086相似。
 
  B.循环指令LOOP,LOOPZ,LOO0PE,LOOPNZ,LOOPNE,TASM支持助记符LOOP,LOOPWE,LOOPWZ,LOOPWNZ,LOOPWNE,LOOPD,LOOPWD,LOOPDE,LOOPDNE,LOOPDNZ。以CX作为计数器时,就可用LOOP,LOOPWE,LOOPWZ,LOOPWNZ,LOOPWNE;在以ECX作为计数器时,以LOOPD,LOOPDE,LOOPDZ,LOOPDNZ,LOOPDNE,下面的一段例子可以说明问题:
  ABC PROC
     MOV CX,100H
     AA:
       ;ADD YOUR CODES HERE
       LOOP AA
  ABC END

  C.过程调用和返回调用CALL,RET
  这两个指令与8086的用法相同,但由于80386下有实模式和保护模式下。在实模式下,无论是段内调用还是段间调用均采用32位指针,即CS:IP,它们的用法与8086下相同。在保护模式下,段间调用和段内调用均用48位指针,即ECS:IP。RET用于返回,具体实现过程会比较复杂,在介绍完80386的地址的管理机制后会作介绍,先介绍一下以下CALL指令在8086中的用法:
  a.段内直接转移,具体格式:CALL 过程名。此时CS不入栈,IP的内栈入栈,入栈后再将加上目的地址与CALL指令的下一条指令的偏移地址之差值就可以转移到目的地址,详细过程:
  SP-2=>SP;将堆栈指针SP减2
  (SP)<=IP;将IP进栈
  IP+偏移地址之差;转到目的地址
  b.段内间接转移,具体格式:CALL OPRD,那么在这里OPRD可以寄存器或内存单元,它的具体实现过程:
  SP-2=>SP;将堆栈指针SP减2
  (SP)<=IP;将IP进栈
  IP<=(OPRD);转到目的地址
  同a一样,CS不入栈
  c.段间直接转移,具体格式:CALL 过程名 [FAR],此时CS,IP均要入栈,详细的实现过程:
  SP-2=>SP;将堆栈指针减2
  (SP)<=CS;将CS入栈
  SP-2=>SP;将堆栈指针再减2
  (SP)<=IP;将IP入栈
  ;装入新的CS,IP
  IP<=过程入口的偏移地址
  CS<=过程入口的段地址
  d.段间间接转移,具体格式:CALL OPRD [FAR],此时CS,IP均要入栈,OPRD是32位,你知道在8086中没有32位寄存器。因此,这里的OPRD一定是存储单元,高16位是CS的值,低16位是IP值,详细的实现过程:
  SP-2=>SP;将堆栈指针减2
  (SP)<=CS;将CS入栈
  SP-2=>SP;将堆栈指针再减2
  (SP)<=IP;将IP入栈
  ;装入新的CS,IP
  IP<=(OPRD+2,OPRD+3)
  CS<=(OPRD,OPRD1)  
  e.段内返回
  格式:RET。实际上它的实现过程:
  (SP)=>IP;从当前栈顶弹出一个字,将它送给IP指令计数器
  SP+2=>SP;SP
  f.段间返回
  格式:RET,实际上它的实现过程:
  (SP)=>IP;IP出栈
  SP+2=>SP;
  (SP)=>CS;CS出栈
  SP+2=>SP;
 
  D.中断返回指令IRET
  功能和用法与8086相同,这里顺便介绍一下8086的中断返回指令
  IRET,具体的实现过程:
  IP<=(SP);IP出栈
  SP+2=>SP;
  CS<=(SP);CS出栈
  SP+2=>SP;
  FLAGS<=(SP);标志寄存器出栈
  SP+2=>SP;
 
  E.串操作指令
  80386在串操作指令方面增加了双字操作,在8086五条指令的基础上增加了INS,OUTS。
  a.LOADSD,和8086的用法和功能相同,不过是对32位操作数操作。
  b.STOSD,和8086的用法和功能相同,不过是对32位操作数操作。
  c.CMPSD,和8086的用法和功能相同,不过是对32位操作数操作。
  d.SCANSD,和8086的用法和功能相同,不过是对32位操作数操作。
  e.MOVSD,和8086的用法和功能相同,不过是对32位操作数操作。
  f.重复前缀REP,和8086的功能与用法相同,仍以CX为计数器,看下面的一小程序:
  ROR ECX,2
  REP MOVSD;以CX为计数器,每次传送双字
  ROL ECX,1
  REP MOVSW;以CX为计数器,每次传送一字
  ROL ECX,1
  REP MOVSB;以CX为计数器,每个传送一个字节 
  g.INSB,INSW,INSD,OUTSB,OUTSW,OUTSD
  g1.INSB,串输入指令,以字节单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。 
  g2.INSW,串输入指令,以字单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
  g3.INSD,串输入指令,以双字单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
  g4.OUTSB, 串输出指令,以字节为单位,将DS:SI内存单元的内容送往DX指定的端口。
  g5.OUTSW, 串输出指令,以字为单位,将DS:SI内存单元的内容送往DX指定的端口。
  g6.OUTSD, 串输出指令,以双字为单位,将DS:SI内存单元的内容送往DX指定的端口。
  g7.串输入和串输出指令不影响标志寄存器中的各标志位,串操作指令可以与REP一起使用
 
高级语言支持,条件字节设置指令
  AA.高级语言支持指令,开始于80186,主要是用来简化高级语言的某些特征,总共有3条指令:ENTER,LEAVE,BOUND
  a.ENTER,LEAVE,建立与释放堆栈框架命令。在C语言中,栈不仅用来向函数传递入口参数,而且在函数内部的局部变量也存放在栈中。为了准确地存取这些这些局变量和准确地获得入口参数,就需要建立堆栈框架,先看一个小程序:
  //C Programming-Language
  int sum(int x,int y)
  {
    int sum;
    sum=x+y;
    return sum;
  }
  //The corresponding ASM codes lists below
  _sum proc near;注意C语言中函数参数的入栈方式是从右向左,即先是参数y入栈,再是x入栈,再是函数的返回地址入栈
    push bp
    mov bp,sp;建立堆栈框架
    sub sp,2
 
    mov ax,word ptr [bp+4];取参数x
    add ax,word ptr [bp+6];加参数y
    mov word ptr [bp-2],ax
    mov ax,word ptr [bp-2]
    mov sp,bp;释放栈框架
    pop bp
    ret
  _sum endp
 此时栈顶的示意图是:
 |----------------------|
 |         BP           |<====SP
 |----------------------|    
 |     函数返回地址     |<====BP+2
 |----------------------|
 |        参数x         |<====BP+4
 |----------------------|
 |        参数y         |<====BP+6
 |----------------------|
 |       ......         |<====BP+8
 |----------------------|
 |       ........       |<====BP+n,n是一能被2整除的数
 |----------------------|
 如果用建立和释放堆栈框架指令,那么对应的汇编程序应该是:
 _sum proc near
    enter 2,0;建立栈框架
    mov ax,word ptr [bp+4];取参数x
    add ax,word ptr [bp+6];加参数y
    mov word ptr [bp-2],ax
    mov ax,word ptr [bp-2]
    leave;释放栈框架
    ret
 _sum endp
 b.建立栈框架指令ENTER,格式如下:ENTER  CNT1,CNT2。其中CNT1表示框架的大小,即子程序中需要放在栈中局部变量的字节数;CNT2是立即数,表示子程序嵌套级别,即从调用框架复制到当前框架的指针数。在立即数CNT2为0时,ENTER指令的实过程是:
 PUSH BP
 SP=>BP
 SP<=SP-CNT1
 c.释放栈框架指令LEAVE,其具体实现过程:
 8086:
 BP=>SP
 POP BP
 80386:
 EBP=>ESP
 POP EBP
 d.ENTER和LEAVE指令均不影响标志寄存器中的各标志位,同时LEAVE指令只负责释放栈框架,并不负责函数返回。因此,要在LEAVE指令后安排一条返回指令。

 BB.条件字节设置指令
 这是80386新增的一组指令集,将会在后面全部列表出来。条件字节设置指令的格式:
 SETxx OPRD
 xx是助记符的一部分,OPRD只能是8位的寄存器或存储单元。
 eg:
 SETO AL;表示当溢出标志位为1时,即OF=1,将AL置1,否则AL清0
 SETNC CH;表示当CF=0时,将CH置1,否则将CH清0
 SETNA BYTE PTR [100];表示当AF=0,将DS:[100]这一个字置1,否则将它清0
 a.SETZ OPRD;等于0时(ZF=1),置OPRD为1,否则清0
 b.SETE OPRD;同a
 c.SETNZ OPRD;不等于0时(ZF=0),置OPRD为1,否则清0
 d.SETNE OPRD;同c
 e.SETS  OPRD;为负数时(SF=1)置OPRD为1,否则清0
 f.SETNS OPRD;同e正好相反(SF=0)
 g.SETO  OPRD;OF=1,置OPRD为1,否则清0
 h.SETNO OPRD;同g正好相反
 i.SETP  OPRD;偶(PF=1)置1
 j.SETPE OPRD;同i
 k.SETNP OPRD;奇(PF=0)置1
 l.SETPO OPRD;同k
 m.SETB OPRD;低于置OPRD为1,否则清0,这是针对无符号数的
 n.SETNAE OPRD;不高于即低于或等于时置OPRD为1,否则清0,这是针对无符号数的
 o.SETC OPRD;CF=1,置OPRD为1,否则清0
 p.SETNB OPRD;高于或等于时,置OPRD为1,否则清0,这是针对无符号数的
 q.SETAE OPRD;高于时置OPRD为1,否则清0,这是针对无符号数的
 r.SETNC OPRD;CF=0时,置OPRD为1,否则清0,这是针对无符号数的
 s.SETBE OPRD;低于或等于时,置OPRD为1,否则清0,这是针对无符号数的,CF|ZF=1
 t.SETNA OPRD;同s,这是针对无符号数的,CF|ZF=1
 u.SETNBE OPRD;高于时置OPRD为1,否则清0,这是针对无符号数的,CF OR ZF=0
 v.SETA OPRD;同u,这是针对无符号数的,CF OR ZF=0
 w.SETL OPRD;小于时,置OPRD为1,否则清0,这是针对有符号数的
 x.SETNGE OPRD;同w,这是针对有符号数的
 y.SETNL OPRD;大于或等于时,置OPR为1,否则清0,这是针对有符号数的
 z.SETGE OPRD;同y,这是针对有符号数的
 a1.SETLE OPRD;小于或等于时,置OPRD为1,否则清0,这是针对有符号数的
 a2.SETNG OPRD;同a1,这是针对有符号数的
 a3.SETNLE;大于时,置OPRD为1,否则清0,这是针对有符号数的
 a4.SETG;同a3,这是针对有符号数的

 位操作指令,处理器控制指令
 AA.位操作指令,8086新增的一组指令,包括位测试,位扫描。BT,BTC,BTR,BTS,BSF,BSR
 a.BT(Bit Test),位测试指令,指令格式:
   BT OPRD1,OPRD2,规则:操作作OPRD1可以是16位或32位的通用寄存器或者存储单元。操作数OPRD2必须是8位立即数或者是与OPRD1操作数长度相等的通用寄存器。如果用OPRD2除以OPRD1,假设商存放在Divd中,余数存放在Mod中,那么对OPRD1操作数要进行测试的位号就是Mod,它的主要功能就是把要测试位的值送往CF,看几个简单的例子:
 b.BTC(Bit Test And Complement),测试并取反用法和规则与BT是一样,但在功能有些不同,它不但将要测试位的值送往CF,并且还将该位取反。
 c.BTR(Bit Test And Reset),测试并复位,用法和规则与BT是一样,但在功能有些不同,它不但将要测试位的值送往CF,并且还将该位复位(即清0)。
 d.BTS(Bit Test And Set),测试并置位,用法和规则与BT是一样,但在功能有些不同,它不但将要测试位的值送往CF,并且还将该位置位(即置1)。
 e.BSF(Bit Scan Forward),顺向位扫描,指令格式:BSF OPRD1,OPRD2,功能:将从右向左(从最低位到最高位)对OPRD2操作数进行扫描,并将第一个为1的位号送给操作数OPRD1。操作数OPRD1,OPRD2可以是16位或32位通用寄存器或者存储单元,但OPRD1和OPRD2操作数的长度必须相等。
 f.BSR(Bit Scan Reverse),逆向位扫描,指令格式:BSR OPRD1,OPRD2,功能:将从左向右(从最高位到最低位)对OPRD2操作数进行扫描,并将第一个为1的位号送给操作数OPRD1。操作数OPRD1,OPRD2可以是16位或32位通用寄存器或存储单元,但OPRD1和OPRD2操作数的长度必须相等。
 g.举个简单的例子来说明这6条指令:

 AA DW 1234H,5678H
 BB DW 9999H,7777H
 MOV EAX,12345678H
 MOV BX,9999H
 BT EAX,8;CF=0,EAX保持不变
 BTC EAX,8;CF=0,EAX=12345778H
 BTR EAX,8;CF=0,EAX=12345678H
 BTS EAX,8;CF=0,EAX=12345778H
 BSF AX,BX;AX=0
 BSR AX,BX;AX=15
 
 BT WORD PTR [AA],4;CF=1,[AA]的内容不变
 BTC WORD PTR [AA],4;CF=1,[AA]=1223H
 BTR WORD PTR [AA],4;CF=1,[AA]=1223H
 BTS WORD PTR [AA],4;CF=1,[AA]=1234H
 BSF WORD PTR [AA],BX;[AA]=0;
 BSR WORD PTR [AA],BX;[AA]=15(十进制) 
 
 BT DWORD PTR [BB],12;CF=1,[BB]的内容保持不变
 BTC DWORD PTR [BB],12;CF=1,[BB]=76779999H
 BTR DWORD PTR [BB],12;CF=1,[BB]=76779999H
 BTS DWORD PTR [BB],12;CF=1,[BB]=77779999H
 BSF DWORD PTR [BB],12;[BB]=0
 BSR DWORD PTR [BB],12;[BB]=31(十进制) 

 BB.处理器控制指令
 处理器控制指令主要是用来设置/清除标志,空操作以及与外部事件同步等。
 a.CLC,将CF标志位清0。
 b.STC,将CF标志位置1。
 c.CLI,关中断。
 d.STI,开中断。
 e.CLD,清DF=0。
 f.STD,置DF=1。
 g.NOP,空操作,填补程序中的空白区,空操作本身不执行任何操作,主要是为了保持程序的连续性。
 h.WAIT,等待BUSY引脚为高。
 i.LOCK,封锁前缀可以锁定其后指令的操作数的存储单元,该指令在指令执行期间一直有效。在多任务环境中,可以用它来保证独占其享内存,只有以下指令才可以用LOCK前缀:
  XCHG,ADD,ADC,INC,SUB,SBB,DEC,NEG,OR,AND,XOR,NOT,BT,BTS,BTR,BTC
 j.说明处理器类型的伪指令
  .8086,只支持对8086指令的汇编
  .186,只支持对80186指令的汇编
  .286,支持对非特权的80286指令的汇编
  .286C,支持对非特权的80286指令的汇编
  .286P,支持对80286所有指令的汇编
  .386,支持对80386非特权指令的汇编
  .386C,支持对80386非特权指令的汇编
  .386P,支持对80386所有指令的汇编
  只有用伪指令说明了处理器类型,汇编程序才知道如何更好去编译,连接程序,更好地去检错。
  在后续的几篇里将详细介绍80386的段页管理机制及控制寄存器,调试寄存器,以及如何在386实模下和保护模式下编程。
 
 80386实模式下编程
 80386在实模式下是一个更快的8086,它不但可以进行32位操作,而且还可以进32位寻址,并且还可以使用80386的扩展指令。不过,由于是在实模下,寻址的最大空间为1M。在一个段内,段的最大长度不超过64K,否则就会发生异常。
 在8086下定义一个段的完整格式是:
 段名 [定位类型]  [组合类型]  [‘类别’]
 80386下定义一个段的完整格式是:
 段名 [定位类型]  [组合类型]  [‘类别’] [属性类型]
 说明:属性类型有两种:USE32和USE16,USE32表示32位段,USE16表示16位段。如果你在程序中用到伪指令.386,那么默认的属性类型就是USE32(32位段),如果没有用伪指令指定CPU的类型,那么默认的属性类型就是USE16,在实方式下只能使用16位段,即用USE16。
 eg:
    CSEG PARA PUBLIC USE32;定义一个32位的段
      AA DW ?

      BB DD ?
      CC DB ?
      DD DW ?
      EE DW 0,0,0.....
    CSEG ENDS
 由于在80386中用到了66H操作前缀和67H地址前缀,因此尽管在实式模式下,只要设定的CPU类型是80386,仍然可以进行32位操作,可以进行32位寻址,66H,67H这两个前缀无需程序员在程序中书写,汇编程序会自动加上的。只要在程序中对32位操作数进行访问,或进行32位寻址,那么就会加上操作数前缀66H和地址前缀67H。相反,如果在32位段中对16位或8位的访问,汇编程序中也会加上这两个前缀。
    下面将给出一个例子程序,演示一下在80386的实模式下编程的方法与技巧(这是从网上down的一个程序,不是我写的,但我会作详细的解剖,并与8086下的程序设计作出比较):
    用十进制,十六进制,二进制三种形式显示双字存储单元F000:1234中的内容
   |------------------MAIN PROC------------|
   | .386                                  |
   | code segment para public 'code' use16 |
   |   assume cs:code                      |
   | begin:                                |
   |   mov ax,0f000h                       |
   |   mov fs,ax                           |
   |   mov eax,fs:[1234H]                  |
   |   call todec                          |
   |   call newline                        |
   |   call tohex                          |
   |   mov al,'H'                          |
   |   call echo                           |
   |   call newline                        |
   |   call tobin                          |
   |   mov al,'B'                          |
   |   call echo                           |
   |   call newline                        |
   |   mov ah,4ch                          |
   |   int 21h                             |
   |---------------------------------------|  
    ;sub-function todec
    todec proc near
       pushad
       mov ebx,10
       xor cx,cx
     dec1:
       xor edx,edx
       div ebx
       push dx
       inc cx
       or eax,eax
       jnz dec1
     dec2:
       pop ax
       call toasc
       call echo
       loop dec2
       popad
       ret
   todec endp
  
   ;sub-function tobin
   tobin proc near
      push eax
      push ecx
      push edx
      bsr edx,eax
      jnz bin1
      xor dx,dx
    bin1:
      mov cl,31
      sub cl,dl
      shl eax,cl
      mov cx,dx
      inc cx
      mov edx,eax
    bin2:
      rol edx,1
      mov al,'0'
      adc al,0
      call echo
      loop bin2
      pop  edx
      pop  ecx
      pop  eax
      ret
   tobin endp

   ;sub-function tohex
   tohex proc near
     countb=8
     enter countb,0
     movzx ebp,bp
     mov   ecx,countb
     mov   edx,eax
   hex1:
     mov al,dl
     and al,0fh
     mov [ebp-countb+ecx-1],al
     ror edx,4
     loop hex1
     mov cx,countb
     xor ebx,ebx
   hex2:
     cmp byte ptr [ebp-countb+ebx],0
     jnz hex3
     inc ebx
     loop hex2
     dec ebx
     mov cx,1
  hex3:
     mov al,[ebp-countb+ebx]
     inc ebx
     call toasc
     call echo
     loop hex3
     leave
     ret
  tohex endp
 
 ;sub-function toasc
 toasc proc near
     and al,0fh
     cmp al,'0'
     cmp al,'9'
     seta dl
     movzx dx,dl
     imul dx,7
     add al,dl
 toasc1:ret
 toasc endp

 ;sub-function newline
  newline proc near
    push dx
    push ax
    mov dl,0dh
    mov ah,2
    int 21
    mov dl,0ah
    int 21
    pop ax
    pop dx
    ret
  newline endp

  echo proc near
    push ax
    push dx
    mov dl,al
    mov ah,2
    int 21h
    pop dx
    pop ax
  echo endp   
 剖析:
   先来看主程序框架,下面就是MAIN PROC:
   |------------------MAIN PROC-------------------------------|
   |.386;定义处理器的类型为386表示可以使用所有80386指令       |
   | code segment para public 'code' use16                    |
   |   assume cs:code                                         |
   | begin:                                                   |
   |   mov ax,0f000h                                          |
   |   mov fs,ax;将f000h装入段寄存器fs                        |
   |   mov eax,fs:[1234H];将1234H内存单元中的双字送给寄存器EAX|
   |   call todec;调用子过程todec                             |
   |   call newline;调用子过程newline进行回车换行             |
   |   mov eax,fs:[1234h];                                    |
   |   call tohex;调用子过程tohex                             |
   |   mov al,'H'                                             |
   |   call echo;显示字符H                                    |
   |   call newline;                                          |
   |   mov eax,fs:[1234H]                                     |
   |   call tobin;调用子过程tobin                             |
   |   mov al,'B'                                             |
   |   call echo                                              

 
   |   call newline                                           |
   |   mov ah,4ch                                             |
   |   int 21h                                                |
   |----------------------------------------------------------|
   主程序中的内容一目了然,很简单。和8086下唯一不同的是就是要用伪指令定义CPU的类型,并且段寄存器的定义多了一个属性类型USE16,再就是32位操作,使用80386的指令,其它的和8086下没有什么区别。
   重点是要分析几个过程,从网上down下来时,过程newline和toasc没有实现代码,因为这很简单,所以上述toasc,newline,echo的过程体是由我写进去的,这两个过程体代码不多而且非常简单,就不作介绍了。重点介绍todec,tobin,tohex。
   a.子过程todec,这个子过程的主要功能是将f000:1234双字单元的内容用十进制显示,下面就来看每一行代码:   
   |-----------------------------------------------------------|
   |todec proc near                                            |
   |   pushad                                                  |
   |   mov ebx,10                                              |
   |   xor cx,cx                                               |
   |  dec1:                                                    |
   |   xor edx,edx                                             |
   |   div ebx                                                 |
   |   push dx                                                 |
   |   inc cx                                                  |
   |   or eax,eax                                              |
   |   jnz dec1                                                |
   |  dec2:                                                    |
   |   pop ax                                                  |
   |   call toasc                                              |
   |   call echo                                               |
   |   loop dec2                                               |
   |   popad                                                   |
   |   ret                                                     |
   |todec endp                                                 |
   |-----------------------------------------------------------|
   分析:将一个数用十进制数来表示,要它对它进行除以10的运算,得到商和余数。再将商除以10,如此循环直到商为0为止,在这个过程中得到的一系列的模(余数)就是十进制数系列。在主程序中,已经将f000:1234双字单元的内容放到EAX寄存器中,由于后来要用十六进制数,二进制数显示,所以EAX寄存器的内容不允许改变,因此在子过程的一开始,要将EAX的内容先入栈,所以子过程的一开始就用PUSHAD将8个32位通用寄存器的内容全部入栈。在标号dec1不断地进行除以10运算,将所得到的余数全部入栈,同时用cx进行计数。在标号dec2中,逐个弹出在标号dec1中得到的余数,然后分别将它们显示出来,这样就可以将该存储单元中的内容用十进数表示,下面解释每一条指令的功能:
   a1.pushad;将8个32位通用寄存器全部入栈
   a2.xor cx,cx;cx清0
   a3.mov ebx,10;10=>ebx
   a4.xor edx,edx;edx清0
   a5.div ebx;edx存放高32位,不过是0,EAX中存放低32位,即ffff:[1234]双字的内容;除法得到的商放在EAX,余数放在EDX 
   a6.push dx;将edx的低16位dx入栈
   a7.inc cx;cx+1=>cx
   a8.or eax,eax;对eax进行或操作,主要是用来判断eax是否为0,即判断商是否为0,从而判断是否应该结束标号为dec1的循环。
   a9.jnz dec1
   a10.pop ax;将放在堆栈中的余数逐个弹出到ax中
   a11.call toasc;显示ax的内容
   a12.call echo
   a13.loop dec2;将所有的余数显示完毕
   a14.popad;8个32位通用寄存器全部出栈
   a15.ret

   b.子过程tohex
    PUSH BP
    SP=>BP
    SP<=SP-CNT1
   |------------------------------------------------------------|
   |tohex proc near                                             |
   |  countb=8                                                  |
   |  enter countb,0                                            |
   |  movzx ebp,bp                                              |
   |  mov   ecx,countb                                          |
   |  mov   edx,eax                                             |
   |hex1:                                                       |
   |  mov al,dl                                                 |
   |  and al,0fh                                                |
   |  mov [ebp-countb+ecx-1],al                                 |
   |  ror edx,4                                                 |
   |  loop hex1                                                 |
   |  mov cx,countb                                             |
   |  xor ebx,ebx                                               |
   |hex2:                                                       |
   |  cmp byte ptr [ebp-countb+ebx],0                           |
   |  jnz hex3                                                  |
   |  inc ebx                                                   |
   |  loop hex2                                                 |
   |  dec ebx                                                   |
   |  mov cx,1                                                  |
   |hex3:                                                       |
   |  mov al,[ebp-countb+ebx]                                   |
   |  inc ebx                                                   |
   |  call toasc                                                |
   |  call echo                                                 |
   |  loop hex3                                                 |
   |  leave                                                     |
   |  ret                                                       |
   |tohex endp                                                  |
   |------------------------------------------------------------|  
   分析:该子过程的功能是将f000:1234双字单元的内容以16进制数显示出来,首先来考虑一下将一个数以16进制数表示出来的算法,事实上在汇编语言中操作数一直都是以十六进制表示的。因此,在这个子过程中不可以像上一个子过程一样,通过不断的除法取模得到结果。事实上,我们只需要将32位操作,以每半个字节(四位)的内容显示出来就可以了,有了这一编程思想,就很容易看懂上面的子过程。当然你们会问,为什么要每次只显示半个字节而不显示一个字节呢?呵呵,十六进制的十六个数是从0000-1111,不就是半个字节了。所以要循环8次才可以显示出32位的EAX,所以这里用ror指令来不断循环移位,每次右移4位放到dl的低4位中。这8个半字节分别放在[ebp-1]至[ebp-8]的存储单元中。不过,存储的顺序是由低位到高位,如果就这样显示结果肯定显示反了。标号hex2,hex3的主要功能是用来判断f000:1234双字单元的内容是否为0,如果为0,只需要将最后结果显示一个0即可,否则就显示出8位内容。下面是每条指令的功能:
   b1.countb=8;伪指令定义一局部变量countb,其值为8
   b2.enter countb,0;建立堆栈框架指令
   b3.movzx ebp,bp;对bp进行零扩展
   b4.mov   ecx,countb;8=>ecx
   b5.mov   edx,eax;将eax=>edx
   b6.mov al,dl
   b7.and al,0fh;取低4位
   b8.mov [ebp-countb+ecx-1],al;将8个半字节的内容逐一送到[ebp-1]至[ebp-8]的内存单元中
   b9.ror edx,4;对edx进行循环右移,每次移动4位
   b10.loop hex1
   b11.mov cx,countb
   b12.xor ebx,ebx;ebx清0
   b13.cmp byte ptr [ebp-countb+ebx],0;下面的语句主要用来判断源操作数f000:1234的内容是否为0,如果是0,就在屏幕上只显示一个0
   b14.jnz hex3
   b15.inc ebx
   b16.loop hex2
   b17.dec ebx
   b18.mov cx,1
   b19.mov al,[ebp-countb+ebx];逐一显示[ebp-8]到[ebp-1]的内容。
   b20.inc ebx
   b21.call toasc
   b22.call echo
   b23.loop hex3
   b24.leave;释放堆栈框架
   b25.ret
    
   c.子过程tobin
   |---------------------------------------|
   |tobin proc near                        |
   |   push eax                            |
   |   push ecx                            |
   |   push edx                            |
   |   bsr edx,eax                         |
   |   jnz bin1                            |
   |   xor dx,dx                           |
   |bin1:                                  |
   |   mov cl,31                           |
   |   sub cl,dl                           |
   |   shl eax,cl                          |

   |   mov cx,dx                           |
   |   inc cx                                                  
   |   mov edx,eax                         |
   |bin2:                                                      

   |   rol edx,1                                               
   |   mov al,'0'                                              
   |   adc al,0                                                
   |   call echo                                               
   |   loop bin2                                               
   |   pop  edx                            |
   |   pop  ecx                                                
   |   pop  eax                                                
   |   ret                                                     
   |tobin endp                                                 
   |---------------------------------------|
   分析:将一个数用二进制数显示出来,只需要用ROL指令就可以了。这里作者写的程序就是这个思路,在标号bin1中主要判断f000:1234单元的内容是否为0,如果为0,那么只需要在屏幕上显示一个0就可以了。否则的话,就用ROL指令对源操作数移位32位,从最高位31位到最低位逐一显示出来,程序设计思路很简单,没有什么复杂的算法,下面看每一条指令的含义:
   c1.push eax;eax入栈
   c2.push ecx;ecx入栈
   c3.push edx;edx入栈
   c4.bsr edx,eax;对eax进行扫描,并把第一个为1的位号送给edx
   c5.jnz bin1;如果eax不为0,就跳到c7去执行
   c6.xor dx,dx;如果eax为0,就将dx清0
   c7.mov cl,31;从c7到c12主要用来设置计数器cx,如果eax=0,那么就设置cx=1,如果eax不等于0,那么就设置ecx=32
   c8.sub cl,dl
   c9.shl eax,cl
   c10.mov cx,dx
   c11.inc cx
   c12.mov edx,eax
   c13.rol edx,1;从c13到c15主要用来显示二进制数据,顺序是从最高位31位到最低位0位
   c14.mov al,'0'
   c15.adc al,0
   c16.call echo
   c17.loop bin2
   c18.pop  edx;edx出栈
   c19.pop  ecx;ecx出栈
   c20.pop  eax;eax出栈
   c21.ret
   在后续的篇幅里将主要介绍保护式下的段页管理机制及及如何在保护模下编程。

 虽然80386处理器要较以前的处理器的功能大大增强,但这些功能只能在保护模式下才能全部得到发挥。在实模式下最大寻址空间只有1M,但在保护模式最大寻址空间可达4G,可以访问到所有的物理内存。同时由于引入虚拟内存的概念,在程序设计中可使用的地址空间为64TB。80386处理器采用了可扩充的分段管理和可选的分页管理机制,这两个存储管理机制由MMU(Memory Management Unit)部件来实现。因此,如果在80386下进行实模式编程,这时的80386处理器相当于一功能更强大,运行速度更快的8086处理器。80386提供对虚拟存储器的支持,虚拟存储器的理论基础就是:速度非常快的内存储器和海量的外存储器,所以它是一种软硬件结合的技术,它能够提供比物理内存大得多的存储空间。
   80386下的段具有三个属性:段基址,段界限,段属性,通常描述段的称作段描述符(Segment Descriptor),而描述符通常放在一个线性表中,这种线性表又分为:GDT(Global Descriptor Table),LDT(Local Descriptor Table),IDT(Interrupt Descriptor Table),通常用一个叫做选择子的东西去确定使用上述三个线性表中哪一个描述符。程序中使用的地址空间就是虚拟地址空间,上面已经说过80386下虚拟地址空间可达到64TB(后面将解释为什么可以达到64TB),虚拟地址空间由一个选择子和段内偏移组成,这是因为通过段的选择子我们可以得到该段的描述符,而在描述符中又说明了段的基址,段的界限及段的属性,再加上段的偏移就可以得到虚拟地址空间。不过请注意,这里并没有将段基址乘以16再加上偏移地址,这是保护模式与实式模式的区别之一。很明显,任何数据都必须装入到物理内存才能够被存储器处理,所以二维的虚拟地址空间必须转换成一维的物理地址。同时,由于每个任务都有自已的虚拟地址空间,为了防止多个并行任务将虚拟地址空间映射同一物理地址空间采用线性地址空间隔离虚拟地址和物理地址,线性地址空间由一维的线性地址构成,线性地址空间与物理地址空间对等,线性地址为32位,可寻址空间为4GB(物理地址空间最大也可以达到4GB,址址为32位,所以说线性地址空间与物理地址空间对等)。下面是80386虚拟地址空间与物理址空间的转换示意图:
  
        |----------|              |------------|       |--------|        |------------------|       |--------|
        | 虚拟地址 |------>|分段管理部件|------>|线性地址|---|--->|可选的分页管理部件|---|-->|物理地址|
        |----|-----|       |------------|       |--------|   |    |------------------|   |   |--------|
      |------|-------|                                       |                           |
      |              |                                       |---------------------------|  
 |----------|    |---------|  
 |  选择子  |    | 段内偏移|
 |----------|    |---------|

   地址映射过程中,通过分段管理部件将虚拟地址空间转换成线性地址,这一步是必然存在的。如果在程序中启用了分页管理机制,那么线性地址还要经过分页管理部件的处理才得到最后的物理地址。如果没有采用分页管理机制,那么得到的线性地址就是物理地址。分页管理部件的主要的工作机制在于将线性地址和物理地址划分成大小相同的块,通过在建立两者之间的页表来建立对应关系。分段管理机制使用大小可变的存储块,使用分段管理机制适合处理复杂系统的逻辑分段。分页管理机制使用固定大小的块,所以它适合管理物理存储器,分页管理机制能够更有效地使用虚拟地址空间。
   80386支持多任务,因此对各个任务进行保护是非常必要的,对任务的保护可分为:同一任务内的保护,不同任务之间的保护。
   a.同一任务内的保护,在同一任务内定义有四种特权级别(Previlege Level),将这些特权级别分配给段中的代码和数据,把最高的特权级别分配给最重要的数据和最可信任的代码,将较低级别的特权分给一般的代码和不重要的数据。特权级别用0~3来表示,用数字0表示最高特权级别,用数字3表示最低特权级别,在比较特权级别时不使用大于或小于,而是使用外层或里层来比较,很明显特权级别为0表示最里层,特别级别为3表示最外层。任何一个存储段(程序直接进行访问的代码段和数据段)都有一个特权级别,在一个程序试图访问这个存储时,就会进行特权级别的比较,如果小于或等于(如果等于表明同级,小于则表明是内层)处该存储段的特权级别就可以对该存储段进行访问。任务在特定时刻下的特权级别称为CPL(Current Previlege Level),看一简单的结构示意图:
                  
                   |---------|-------|  
                   |  CodeA  | DataA | 特权级别为0                             
                   |---------|-------|
                   |---------|-------|  
                   |  CodeB  | DataB | 特权级别为1                             
                   |---------|-------|
                   |---------|-------|  
                   |  CodeC  | DataC | 特权级别为2                             
                   |---------|-------|
                   |---------|-------|  
                   |  CodeD  | DataD | 特权级别为3                             
                   |---------|-------|

     CodeA可以访问DataA,CodeB,DataB,CodeC,DataC,CodeD,DataD
     CodeB可以访问Datab,CodeC,DataC,CodeD,DataD,但不可以访问CodeA,DataA
     CodeC可以访问DataC,CodeD,DataD,但不可以访问CodeA,DataA,CodeB,DataB
     CodeD处在最外层,只能访问同级的DataD,不可以访问CodeA,DataA,CodeB,DataB,CodeC,DataC
     通常应用程序放在最外层,但由于每个应用程序的虚拟地址空间不同,因此它们被隔离保护。这种特权级别的典型用法就是:将操作系统的核心放在0层,操作系统的其余部分放在1级,2级留给中间软件使用,3级放应用程序,这样的安排的好处在于:操作系统的核心因为放在0层,因此它可以访问任务中所有的存储段,而1级的部分操作系统可以访问除0级以外的所有存储段,应用程序只能访问自身的存储段。
   b.不同任务间的保护,通过把每个任务放在不同的虚拟地址空间来实现隔离保护,虚拟地址到物理地址之间的映射由每个任务中的映射函数来决定,随着任务切换,映射函数也跟着切换,这样可以保证任务A映射到物理内存中的区域与任务B映射到内存中的区域是不同的,尽管有可能它们的虚拟地址空间相同,但它们最终在物理内存中的位置是不同的,从而起到了保护作用。
  
主要介绍段描述符,段选择子
   在保护模式下,段是实现虚拟地址到线性地址转换的基础。在保护方下,每个段有三个参数:段基址,段界限,段属性。段基址规定了线性地址空间中段的开始地址,段基址长度为32位,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始,这一点和实式方式不同,实式方式下要求段的边界必须被16整除。段界限规定段的大小,段界限用20位表示,而且段界限可以是字节或4K为单位,这个称为段的粒度。当段界限以字节为单位时,那么段的范围是1字节至1M字节;当段界限是以4K字节为单位时,那么段的范围是4K至4G。段的界限同时也是用来校验偏移地址的合法性,比如说段A的基址为00123456H,段界限为1000H,如果段界限以字节为单位,那么段的范围是00123456H-00124456H;如果段界限以4K字节为单位,那么段的范围是00123456H-00223456H。事实上,段的界限也可以用来校验偏移地址的合法性,上面的例子中界限为1000H,那么偏移地址的范围就是0-1000H,如果偏移地址不在这个范围内那就会引起异常。需要说明的是,数据段有点特殊,因为数据段的偏移范围不仅仅是由段界限来决定,还要由段的扩展方向(Extension Direction)来决定,因为要照顾到堆栈段(堆栈段是一种特殊的数据段,它是向低端地址扩展的),如果段界限为Limit,段的扩展方向为向高端地址扩展的话,那么我们可以断定它是一普通的数据段,0-Limit是有效的偏移范围,而Limit以上属于无效的偏移范围;如果段界限为Limit,段的扩展方向为向低端地址扩展的话,那么可以断定它是一堆栈段,此时0-Limit是无效的偏移范围,Limit以上则属于有效的偏移范围,正好和向高端地址扩展的普通数据段相反。除了堆栈段以外,其它的段均是自然向高端扩展。
   段基址,段界限及段属性这三个参数在保护模式下用描述符来描述,每个描述符的长度为8个字节,每个段都有一个对应的描述符。在保护模式下有三种描述符:存储段描述符,系统段描述符,门描述符。
   A.存储段描述符:存储段是指程序直接执行的代码段和数据段,存储段描述符是用来描述存储段的,也可以说是用来描述代码和数据段的,它的长度为8个字节,该描述符结构示意图:
   
    第7字节  第6字节  第5字节     第4字节  第3字节  第2字节   第1字节  第0字节
   |--------|------------------|-----------------------------|-----------------|
   |段基址的|                  |                             |                 |
   |高8位   |Segment Attributes|       段基址的低24位        | 段界限的低16位  |
   | 24~31  |   段属性,占用两 |         0~23                |      0~15       |
   |        |   个字节         |                             |                 |
   |--------|------------------|-----------------------------|-----------------|
            |                  |
            |                  |
   _________|                  |_____________________________
   | 15  14 13  12 11            8 7 6     5   3           0|
   |---|---|---|---|-------------|---|--- -|---|------------|
   | G | D |0  |AVL|段界限的高4位| P | DPL |DT |    TYPE    |
   |---|---|---|---|--- ---------|---|-----|---|------------|
  
   段基址和段界限都被安排在描述符的两个域中,主要是来看段的属性:
   a.G(第15位),这是段界限粒度,即是说段界限到底是以字节为单还是以4K字节为单位。G=0表示段界限是字节,G=1表示段界限为4K字节。
   b.D(第14位),D是一个很特殊的位,在描述可执行段,向低扩展数据段或者由SS寄存器寻址的段。在描述可执行段的描述符中,D位决定了指令使用的地址及操作数据默认的大小,D=1表示默认情况下使用32位地址及32位或8位操作数,这样的代码段称为32位代码段;D=0表示默认情况下使用16位地址及16位操作数或8位操作数,这样的代码段称为16位代码段;在向低扩展的数据段中,D=1表示段的上部界限为4G,D=0表示段的上部界限为64K;在描述由SS寄存器寻址的段中,该位决定使用隐式的堆栈访问指令使用何种堆栈指针寄存器。D=1表示使用32位堆栈指针寄存器ESP,D=0表示使用16位堆栈指针寄存器SP,隐式的堆栈访问指令指的是那些指令中没有明显对SP或ESP进行操作的指令,比如说PUSH,POP,PUSHA,POPA,PUSHAD,POPAD都属于隐式的堆栈访问指令。
   c.0(第13位),这一位恒为0,为80386以后的处理器保留的。
   d.AVL(第12位),软件可利用位,主要是为了保持和以后的处理兼容。
   e.第11位到第8位是段界限的高4位。
   f.P(第7位),存在位,P=1表示描述符对转换地址有效。P=0表示描述符对转换地址无效,如果使用该描述符将会引起异常。   
   g.DPL(Descriptor Privelege Level)描述符特权级,共2位,它规定了所述段的特权级别,用于特权检查,以决定是否能对该段进行访问。
   h.DT(Descriptor Type)描述符的类型,DT=0表示存储段描述符,DT=0表示系统段描述符和门描述符。
   i.TYPE,共4位,说明存储段的具体属性:
     TYPE0:指示描述符是否被访问,用A标记,A=0表示描述符未被访问,A=1表示描述符已被访问。
     TYPE1:根据TYPE3来确定。
     TYPE2:根据TYPE3来确定。
     TYPE3:指示描述符所描述的段是数据段还是代码段,用E标记。E=0表示是不可执行段,是数据段,对应的描述符也就是数据段描述符。E=1表示是可执行段,也就是代码段,对就的描述符也就是代码段描述符。
     如果TYPE3=0,也就是说描述符是数据段描述符,那么TYPE1指示该数据段是否可写,用W标记。W=0表示对应的数据段不可写,只读。W=1表示对应的数据段可写。TYPE2则指示数据段的扩展方向,用ED标记。ED=0表示向高端扩展,ED=1表示向低端扩展。
     如果TYPE3=1,也就是说描述符是代码段描述符,那么TYPE1指示该代码段是否可读,用符号R标记。R=0表示对应的代码段不可读,只能执行,R=1表示对应的代码可读可执行。TYPE2则指示所描述的代码段是否是一致代码段,用C表示。C=0表示代码段不是一致代码段,C=1表示是一致代码段。
     TYPE3-TYPE0这四位可以列成一个表:
     ___________________________________________________________________________________
    |0000 |只读                                                                        |
    |_____|____________________________________________________________________________|
    |0001 |只读,已访问                                                                |
    |_____|____________________________________________________________________________|
    |0010 |可读,可写                                                                  |
    |_____|____________________________________________________________________________|
    |0011 |读写,已访问                                                                |
    |_____|____________________________________________________________________________|
    |0100 |只读,向低扩展                                                              |
    |_____|____________________________________________________________________________|
    |0101 |只读,向低扩展                                                              |
    |_____|____________________________________________________________________________|
    |0110 |读/写,向低扩展                                                             |
    |_____|____________________________________________________________________________|
    |0111 |读/写,向低扩展,已访问                                                     |  
    |_____|____________________________________________________________________________|
    |1000 |只执行                                                                      |
    |_____|____________________________________________________________________________|
    |1001 |只执行,已访问                                                              |
    |_____|____________________________________________________________________________|
    |1010 |可执行,可读                                                                |
    |_____|____________________________________________________________________________|
    |1011 |可执行,可读,已访问                                                        |
    |_____|____________________________________________________________________________|
    |1100 |只执行,一致代码段                                                          |
    |_____|____________________________________________________________________________|
    |1101 |只执行,一致代码段,已访问                                                  |
    |_____|____________________________________________________________________________|
    |1110 |可执行,可读,一致代码段                                                    |
    |_____|____________________________________________________________________________|
    |1111 |可执行,可读,一致代码段,已访问                                            |
    |_____|____________________________________________________________________________|                                        
    存储段描述符的结构可以这样定义:
    DESCRIPTOR STRUCT
    Segment_LimitL16 DW 0;段界限的低16位   
    Segment_BaseL16 DW 0;段基址的低16位
    Segment_BaseM8 DB 0;段基址的中间8位
    Segment_BaseH8 DB 0;段基址的高8位
    Segment_Attributes DW 0;段属性
    DESCRIPTOR ENDS
    一个任务有多个段,每个段都有一个描述符。因此在80386下,为了方便管理这些段描述符,将描述符组成一个线性表,称之为描述符表。在80386下有三种描述符表:GDT(Global Descriptor Table),LDT(Local Descriptor Table),IDT(Interrupt Descriptor Table)。在整个系统中全局描述符表GDT和中断描述符表只有一张,局部描述符表可以由若干张。每个描述符表都形成一个特殊的16位数据段,这样的特殊数据段最多可以有8192个描述符,具体使用哪一个段描述符,由段的选择子来确定。每个任务都有自已的局部描述符表LDT,它包含自已的代码段,数据段,堆栈段,也包含该任务使用的一些门描述符。随着任务的切换,LDT也跟着切换。GDT包含每一个任务都可能或可以访问的段的描述符,通常包含描述操作系统所用的代码段,数据段以及堆栈段的描述符,也包含描述任务LDT的描述符。在任务切换时,并不切换GDT。一个任务的整个虚拟地址空间可以分为相等的两半,一半空间的描述符在全局描述符表GDT中,一半空的描述符在局部描述符表LDT中。由于全局描述符表和局部描述符表都可以包含最多为8192个描述符,而每个描述符所描述的段的最大长度为4G,因此最大的虚拟地址空间为:8192*4G*2=64TB。
   段选择子用来确定使用描述符表中的哪一个描述符。实式模式下逻辑地址由段地址*16再加上段内偏移地址;保护模式下虚拟地址空间由段选择子和段内偏移来确定,和实式模式比较,段选择子代替了段值,实际上通过段选择子就可以确定了段基址。选择子的高13位是描述符表中的索引号,用来确定描述符,因为是13位,所以说最多可以有2的13次方8192个描述符,索引号:0-8191。标记TI指示是从全局描述符中读取描述符还是从局部描述符表中读取描述符。TI=0指示是从全局描述符表中读取描述符,TI=1指示从局部描述符表读取描述符。RPL表示请求特权级,用于特权检查。假设段选择子为88H,则表示请求的特权级别是0,从全局描述表中读取描述表,描述符的索引号为11H。有一个特殊的选择子称为空选择子,它的Index=0(即高13位为0),TI=0,RPL则可以为任意值。当用空选择子对存储器进行访问,会出现异常。空选择子对应于全局描述表中的第0个描述符,因此全局描述符表中的第0个描述符总是不会被访问。如果TI=1,那么就不是空选择子,它指定的是当前局部描述符表中的第0个描述符。为了更快地从段选择子中获得段的基本信息(段基址,段界限,段属性),从80386开始为每个段寄存器在硬件上配备了段描述符高速缓冲存储器,对我们写程序的人来讲,它是不可编程的。有了这种高速缓冲寄存器后,每当将选择子装入段寄存器后,处理器将自动装入描述符表中相应的描述符,并将描述表的信息装入到高速缓冲寄存器,这样可以加快访问速度,以下是段选择子的结构示意图:
  
   15________________________________________________________________3__2__1_____0
   |                                                                |TI |  RPL   |
   |________________________________________________________________|___|________|
 
 主要介绍系统地址寄存器和控制寄存器以及在程序中实方式下与保护方式下的切换
   80386处理器新增了一组控制寄存器CR0,CR1,CR2,CR3和一组系统地址寄存器GDTR,LDTR,IDTR,TR,它们全部都是32位的。CR0包含了指定处理器工作方式的控制位,CR1保留未使用,CR2和CR3由分页管理部件使用,CR0中的5~30位和CR3中的0~11位必须为0,分别介绍如下:
 
   ___________________________________________________________________________
  |PG|0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |ET|TS|EM|MP|PE|   CR0
  |__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|_ |__|  
  |                               Reserved                                   |   CR1
  |__________________________________________________________________________|
  |                         页故障线性地址                                   |   CR2
  |__________________________________________________________________________|
  |   高20位页表的起始物理地址                          |低12位为0           |   CR3
  |_____________________________________________________|____________________|

  PE标记用于指定处理器的工作模式。PE=0,处理器处于实模式;PE=1,处理器处于保护模式
  PG标记用于指定处理器是否启用分页管理机制。PG=0,禁用分页管理机制,此时由分段管理部件产生的线性地址就是物理地址。;PG=1,启用分页管理机制,此时由分段管理部件产生的线性地址须再经过分页管理机制才能得到最终的物理地址。
  MP,EM,TS,ET用于控制浮点协处理器的操作。
  CR2和CR3控制寄存器由分页管理机制使用。CR2用于发生页异常时报告出错信息。当发生页故障时,处理器会将当前的线性地址保存在CR2。CR3用于保存页表在内存中的起始物理地址,由于页表是对齐的,所以仅高20位有效,低12位必须为0。
  全局描述符表GDT,局部描述符表LDT和中断描述符表IDT在保护模式下是特殊的段,也就是说处理器将这些线性表当段一个特殊的段来处理,它包含了对段机制所用的重要数据。为了能够更快速地进位这些段,386处理器采用特殊的寄存器保存这种段的基地址和界限,这种寄存器就是系统地址寄存。在80386下系统地址寄存器有:全局描述符表寄存器GDTR,局部描述符表寄存器LDTR,中断描述符表IDTR,任务状态段寄存器TR。全局描述符表寄存器GDTR,长度为48位,其中高32位是基址,低16位含界限。由于GDT本身不可以由GDT内的描述符来描述,所以处理使用GDTR寄存器为GDT这样的特殊段提供一个伪描述符,即是说:
 
   |                |
   |________________|           全局描述符表寄存器GDTR
   |                |      ________________________________
   |      GDT       |______|                   |           |
   |________________|______| 32位基址          |    16界限 |
   |                |      |___________________|___________|
   |                |
因为段选择子只用了13位来表描述表中的索引号,即是说最多可以有8192个描述符,而每个描述符是8个字节。而在80386处理器下将全局描述表作为一个特殊的系统段,那么段的界限实际上就是8192*8,所以段的界限用16位就可以了。通常情况下,如果GDT有N个描述符,那么GDT的段界限为N*8-1,这个伪描述符也就是全局描述符寄存器内容可以用结体定义成:
  PreDesc STRUCT
  BASE32 DD 0
  LIMIT16 DW 0
  PreDesc ENDS
局部描述表寄存器LDTR规定了当前任中使用的局部描述表LDT,LDTR类似于一个段寄存,它的长度为32位,一个16位的寄存器和对程序员来讲不可见的高速缓冲存储器。每一个任务的局部描述符表作为一个特殊的系统段,它由定义在全局描述符表GDT中的描述符来描述,前面已提到过一个任务只能有一张全局描述符表GDT和一张中断描述符表IDT,但可以有多张局部描述行表LDT,而每一张局部描述符表都由定义在GDT中的描述符来确定。通常将描述LDT的选择子装入到LDTR,LDTR根据选择子从全局描述符表中取出对应的描述符,并把LDT的基址及界限信息保存到对程序员来讲不可见的高速缓冲存储器,随后就可以对LDT进行访问。当前任务中的所有段都由GDT中的描述符来描述。
   _________       ____________________________________________________
   |       |______|                   |                     |         |
   | LDTR  |______| 32位基址          | 32位界限            |12位属性 |
   |_______|      |___________________|_____________________|_________|

中断描述符表和全局描述符表一样,长度为48位。32位段基址和16位界限。
如何从实式模式切换到保护模式下呢?通常来讲,要两个步骤:1.作好切换到保护模式下的准备;2.切换到保护模式。主要准备工作就是建立全局描述符表,并使GDTR指向GDT,因为切换到保护模式下,至少要将代码段的选择子装入到CS中,看程序片段:

;定义好描述符的结构
DESCRIPTOR STRUCT
  LIMIT DW 0;段界限
  BASEL DW 0;段基址的低16位
  BASEM DB 0;段基址的16~23位
  ATTRIBUTES DW 0;段属性
  BASEH DB 0;段基址的高8位,24~31
DESCRIPTOR ENDS
;定义好伪描述符
PDESC STRUCT
  LIMIT DW 0
  BASE DD 0
PDESC ENDS
;通常要定义一个段间跳转的宏,这样的话就可以保证在进入保护模式时将代码段的选择子装入到CS寄存器
JUMP MACRO selector,offset
  DB 0EAH
  DW offsetv;段偏移
  DW selector;段选择子
  ENDM
;打开A20地址线
PUSH AX
IN AL,92H
OR AL,2
OUT 92H,AL
POP AX
;关闭A20地址线
PUSH AX
IN AL,92H
AND AL,0FDH
OUT 92H,AL
POP AX
;切换到保护模式下,将CR0寄存中的第0位置1
MOV EAX,CR0
OR CR0,1
MOV CR0,EAX
其它的部分就要根据具体的应用来写, 下面的例子是如何在保护模下访问820000H单元开始的内容,看程序:
.386P
data segment use16
GDT LABEL BYTE;定义全局描述符表
DUMMY DESCRIPTOR<>;空描述符,它有特定义的含义,空描述符可以保证GDT中的第1个描述符永远不会被访问
CODE DESCRIPTOR<0FFFFH,,,SAttr,>;代码段的描述符
CODE_SEL=CODE-GDT;代码段描述符的选择子
DATAS DESCRIPTOR<0FFFFH,0H,82H,DAttr,>;源数据段描述符,即820000H
DATAS_SEL=DATAS-GDT;源数据段选择子
GDTLEN=$-GDT
VGDTR DESCRIPTOR<GDTLEN-1,>
data ends

code segment use16
   assume cs:code,ds:data
start:
   mov ax,data
   mov ds,ax
   mov bx,16
   mul bx;设置全局描述表GDT基址,因为现在还处在实模式下,所以段地址要左移4位
   add ax,offset GDT
   adc dx,0
   mov word ptr VGDTR.BASE,ax;设置全局描述符表寄存器GDTR的内容
   mov word ptr VGDTR.BASE+2,dx
   ;设置代码段描述符
   mov ax,cs
   mul bx
   mov CODE.BASEL,ax
   mov CODE.BASEM,dl
   mov CODE.BASEH,dh     
   ;以下部分你可以根据实际的应用来编写
   .........
   ...........
   ;加载GDTR
   LGDT QWORD PTR VGDTR
   CLI;关中断
   ;打开A20地址线
   ;切换到保护模式
   mov eax,cr0
   or eax,1
   mov cr0,eax
   JUMP <CODE_SEL>,<OFFSET VIRTUAL>;清指令预取队列,真正进入保护模式
   ........
   ........
   virutal:
     ;add your code here according to your needs
   ............
   ;回到实模式
   ;关闭A20地址线
   STI;开中断
code ends
end start
上述的程序片段是随手写的,可根据需要自已加以调整,不过有点要说明。
a.通常来讲,从实模式下切换到保护模式下只要将CR0寄存器中的最低位设置为1就可以了。但是,此时CS的内容仍然是实模式下的内容,所以加了一条段间跳转指令JUMP <CODE_SEL>,<OFFSET VIRTUAL>,执行完这条指令就可以将代码段选择子CODE_SEL装入到段寄存器CS中,同时也可以刷新指令预取队列。 
b.LGDT QWORD PTR VGDTR,该指令的功能是将VGDTR的内容装入到全局描述符表寄存器GDTR中。
c.上面的代码片段中并没有建立中断描述符表IDT,这样的话就要求整个程序必须运行在关中断情况下进行。
d.为了访问1M以上的存储单元,应该打开A20地址线,在WINDOWS下只需加载HIMEM.SYS就可以了。能不能进入保护模式只与是否加载HIMEM.SYS有关,与处理器工作在实方式下还是在保护方式下无关。也就是说,只要加载HIMEM.SYS,就算处理器当前处在实模式下,A20地址线关闭,处理器也一样可以进入保护模式。
下集预告:
80386ASM程序设计基础(十二)---任务切换
80386ASM程序设计基础(十三)---80386中断和异常
80386ASM程序设计基础(十四)---分页管理机制
80386ASM程序设计基础(十五)---V86模式
敬请关注,谢谢。  
   

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值