80386ASM程序设计基础

  80386 ASM程序设计基础,呵呵,这是最近一段时间我的业余爱好。本期将连续推出若干篇有关80386ASM程序设计的基础,主要介绍80386ASM指令的详细用法及如何在80386实模式下,保护模式下及虚拟8086模式编程以及我会详细介绍80386下的段页管理机制,我会将80386下的指令与8086下的相同指令进行比较。在你去看罗云彬的ASM编程之前,不妨先看看我的基础篇,希望有志于从事汇编语言的朋友,多提意见。
   80386处理器是Intel公司80×86发展史上的里程碑,它不但兼容先前的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存储器的寻址方式”。

   2.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/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中的内容

Leave a comment

Your comment