基于8086实模式汇编

通过本篇博客面向8086CPU实模式编程。

先了解一下8086机:

  • 8086是16位机(因此运算器一次能处理16位数据,寄存器最大宽度是16位)

  • 有20位地址总线(在总线上传输时,用两个16位地址,即段地址和偏移地址,通过段地址<<4+偏移地址组合成20位的物理地址在总线上传送)

  • Intel CPU编码方式属于小端存储(逆序存储,高位对应高地址)

    RISC CPU编码方式属于大端存储(正序存储,低位对应高地址)。

    因此属于Intel系列的8086机是小端存储。

南邮《汇编语言程序设计》2018/2019 学年第一学期期末考试回忆_Wonz-CSDN博客

南邮《汇编语言程序设计》往年试题及答案解析_Wonz-CSDN博客

一、微型计算机基础

(一)编码

BCD码

四位二进制数 等值的一位BCD码数 等值的一位十进制数
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
0
1
2
3
4
5
6
7
8
9
1010
1011
1100
1101
1110
1111
非法BCD码

BCD码在计算机中的存储分为紧凑型非紧凑型

  • 紧凑型BCD码(组合BCD码,压缩BCD码)

    \[(37)_D=00110111B=37H \]

    BUF DB 37H    ;放在一个字节里
    
  • 非紧凑型BCD码(未组合BCD码,未压缩BCD码)

    \[(37)_D=00000011B\ \ \ \ 00000111B \]

    BUF DW 0307H   ;放在一个字中
    

补码

一些常用转换

  1. \(\color{red}真值 \rightarrow 补码\)

    正数:将真值用二进制表示,符号位置0。
    负数:

    • 方法一:符号位置1,其余位按位取反,然后加1。
    • 方法二:对于n+1位字长,用二进制下的\(2^{n+1}\)减去真值的绝对值的二进制形式。
  2. \(\color{red}原码\rightarrow 补码\)
    正数:原码 == 补码
    负数:除了符号位,其余位按位取反然后加一

  3. \(\color{red}补码\rightarrow 相反数的补码\)

    对一个补码,减1后每一位(包括符号位)按位取反,即为其相反数的补码。(或者先每一位按位取反再加1)

补码的特点

  • 扩展时满足正数补0,负数补1。
  • 真值的补码与其相反数的补码相加等于0。
  • 真值0表示方法唯一。
  • C语言中变量以补码形式存放在内存中

表示范围:由于0的补码唯一,所以补码的范围相比原码的最小值可以表示一个更小的值。

  • n+1位补码表示定点小数:最大值\(1- 2^{-n}\) ,最小值\(-1\)

  • n+1位补码表示定点整数:最大值\(2^n-1\),最小值\(-2^n\),即\(-2^n\thicksim 2^n-1\)

    n+1位二进制数能够表示的无符号整数范围即\(0\thicksim 2^{n+1}-1\)

(二)微型计算机硬件结构

以CPU为核心通过3条总线连接存储器、I/O接口。

CPU集成了运算器、控制权、寄存器组、存储器管理部件等。

I/O接口是CPU和外部设备交换信息的”中转站“。

CPU程序控制完整流程:取指令、分析指令(译指)、去操作数、执行指令、回送结果,其中取指令分析指令(译指)执行指令是不可或缺的核心步骤

(三)8086与80486微处理器

8086:

  • 8086是16位微处理器(因此运算器一次能处理16位数据,寄存器最大宽度是16位)
  • 8086有16根数据线和20根地址线,它既能处理16位数据,也能处理8位数据, 可寻址的内存空间为1MB(在总线上传输时,用两个16位地址,即段地址和偏移地址,通过段地址<<4+偏移地址组合成20位的物理地址在总线上传送)

80486:

  • 80486是32位微处理器
  • 内外部数据总线是32位,地址总线为32位,可寻址4GB的存储空间,实模式下只能访问第一个1M内存,保护模式下可以访问4G物理存储空间,虚拟空间可达64T

Intel CPU编码方式属于小端存储(逆序存储,高位对应高地址)

RISC CPU编码方式属于大端存储(正序存储,低位对应高地址)。

因此属于Intel系列的8086机是小端存储。

(四)Intel系列32位微处理器的3种工作模式

CPU在不同阶段的工作状态。

1. 实模式

加电复位之后CPU自动工作在实模式,此时只能访问第一个1M内存,开始主板初始化。

实模式下

  • CS和IP的初值由操作系统自动赋值

  • SS和SP的初值由程序员赋值或者由操作系统自动赋值

  • DS/ES/FS/GS,BX/SI/DI/BP的初值都是由程序员赋值

  • \(物理地址=段寄存器\times 2^4+偏移地址\)

2. 保护模式

引导之后,CPU处于保护模式,此时提供支持多任务环境的工作方式。(CPU绝大多数时间都处于保护模式)。

保护模式下段寄存器存放的不是段基址而是段选择符

3. 虚拟8086模式

这种模式方便了用户在保护模式下运行一个或者多个原8086程序,保护模式可以随时切换至该模式的其中一种工作方式。

由于8086将段寄存器内的值视作段基址,所以同样此时段寄存器内存放的被认为是段基址。

搭载Intel系列CPU的PC机一开机是处于实模式下,如果安装的系统是DOS,那么会始终处于实模式,但如果是Windows,那么CPU会被切换至保护模式,如果在Windows下运行DOS系统下的程序,那么CPU会切换到虚拟8086模式下运行。

(五)存储器

1. 基本概念

  • 存储元:存储器的最小组成单位。

  • 存储单元:CPU访问存储器基本单位,由若干个具有相同操作属性的存储元组成。

  • 存储体:存储单元的集合,是存放二进制信息的地方。

  • 存储器:存储体和地址译码电路、读写控制电路等一起构成存储器。

2. 地址空间

  • 存储空间:包括物理空间(程序的运行空间,即主存空间)、虚拟空间、线性空间
  • I/O空间:486利用低16位地址线访问I/O端口,I/O地址空间与存储空间不重叠

(六)系统总线

微型计算机的各个部件是由系统总线连接起来的

系统总线上传送的信息包括数据信息、地址信息、控制信息,因此,系统总线包含有三种不同功能的总线,即数据总线DB(Data Bus)、地址总线AB(Address Bus)和控制总线CB(Control Bus)

  • 数据总线DB用于传送数据信息。

    数据总线是双向三态形式的总线,即他既可以把CPU的数据传送到存储器或I/O接口等其它部件,也可以将其它部件的数据传送到CPU。数据总线的位数是微型计算机的一个重要指标,通常与微处理的字长相一致。例如Intel 8086微处理器字长16位,其数据总线宽度也是16位。需要指出的是,数据的含义是广义的,它可以是真正的数据,也可以指令代码或状态信息,有时甚至是一个控制信息,因此,在实际工作中,数据总线上传送的并不一定仅仅是真正意义上的数据。

  • 地址总线AB用于传送地址。

    由于地址只能从CPU传向外部存储器或I/O端口,所以地址总线总是单向三态的,这与数据总线不同。地址总线的位数决定了CPU可直接寻址的内存空间大小,比如8位微机的地址总线为16位,则其最大可寻址空间为216=64KB,16位微型机的地址总线为20位,其可寻址空间为220=1MB。一般来说,若地址总线为n位,则可寻址空间为2n字节。

  • 控制总线CB用来传送控制信号和时序信号。

    控制信号中,有的是微处理器送往存储器和I/O接口电路的,如读/写信号,片选信号、中断响应信号等;也有是其它部件反馈给CPU的,比如:中断申请信号、复位信号、总线请求信号、限备就绪信号等。因此,控制总线的传送方向由具体控制信号而定,一般是双向的,控制总线的位数要根据系统的实际控制需要而定。实际上控制总线的具体情况主要取决于CPU。

    大部分控制线是单向,少数是双向

二、8086寄存器

(一)通用寄存器、段寄存器、指针寄存器

  • AX/BX/CX/DX/SI/DI/SP/BP:通用寄存器(16位)。

    在现在的64位机上通用寄存器叫RAX/RBX/RCX/RDX/RSI/RDI/RSP/RBP,32位机上通用寄存器叫EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP,出于兼容性考虑,可以理解为EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP的低16位就是AX/BX/CX/DX/SI/DI/SP/BP。16位得一般寄存器AX/BX/CX/DX的低8位是AL/BL/CL/DL,高8位是AH/BH/CH/DH。image-20210709203704840

  • CS/DS/SS/ES/FS/GS:提供段地址的段寄存器(16位)。

    • CS & IP指令段寄存器CS提供的段地址和指针寄存器IP提供的偏移地址组合即得待执行指令的物理地址,CPU取指执行上一条指令的地址(CPU在执行当前指令的时候,IP就已经指向了下一条指令的偏移地址。

      [CS:IP]存放下一条要执行的指令的地址。

    • SS & SP栈段寄存器SS提供的段地址和SP栈顶寄存器提供栈顶单元的偏移地址组合成栈顶单元的物理地址。

      [SS:SP]始终指向栈顶。

    • DS:提供数据段段地址的数据段寄存器

    • ES/FS/GS附加数据段(附加段)寄存器

      FS:指向一种被称为线程信息块(TIB)的结构,这种结构是由内核在创建线程时创建的,用于支持操作系统相关功能、服务和API

      GS:在32位Windows上GS保留供将来使用,在x64模式下,FS和GS段寄存器已交换

      Win64使用GS的原因是该FS寄存器用于32位兼容性层(称为Wow64),32位应用程序永远不会导致GS更改,而64位应用程序永远不会导致FS更改。注意,在Win64和Wow64中GS是非零的,这可以用来检测一个32位应用程序是否在64位Windows中运行,在一个“真正“的32位Windows中GS总是零

  • IP:指令指针寄存器,32位称为EIP,低16位为IP。

  • FS & GS:在80386及之后的处理器,又增加了两个寄存器 FS 寄存器和 GS寄存器,被OS内核用于访问线程专用的内存。

(二)标志寄存器(EFLAGS)

EFLAGS是32位寄存器,FLAGS是(低)16位寄存器

1. 状态标志寄存器

  • CF:进位/借位标志位。

    将操作数(字节、字、双字)视作无符号数进行运算,并判断是否由最高有效位产生向更高位进位或者借位的标志位。发生进位为1,未发生进位为0。

  • OF:溢出标志位。

    将操作数视作有符号数进行运算,并判断是否溢出的溢出标志位。发生溢出为1,未发生溢出为0。

    溢出

    进行有符号运算的时候结果超出了机器所能表示的范围称为溢出。
    

    OF置位原理
    对于

    \[[X]_补=X_{n-1}X_{n-2}X_{n-3}……; \]

    \[[Y]_补=Y_{n-1}Y_{n-2}Y_{n-3}……; \]

    \[[Z]_补=[X]_补+[Y]_补=Z_{n-1}Z_{n-2}……; \]

    那么

    \[\color{red}{OF=X_s\cdot Y_s\cdot \overline{Z_s}+\overline{X_s}\cdot \overline{Y_s}\cdot Z_s} \]

    我们人工判溢同样可以这样,补码相加之后取加数、被加数、和的最高有效位后,带入OF的公式得到OF的值。

  • AF:辅助进位/辅助借位标志位。

    判断是否由D3位(将操作数从低位到高位依次从0开始标记)产生向更高位进位或者借位的标志位。发生进位为1,未发生进位为0。

  • SF:符号标志位。

    字节/字/双字运算之后,最高位为1,则置1,否则置0。

  • ZF:结果标志位。

    运算结果全0则置1,否则置0。

  • PF:奇偶标志位。

    最低的1个字节中1的个数为偶数个则置1,否则置0.

2. 控制标志寄存器

DF、IF、TF

三、操作数寻址

(一)操作数的分类

  • 立即数:包含在指令中的操作数

    书写规定

    1. 以A-F开头的16进制数必须以0为前缀,防止编译器认为你写的是标号而非数字

      add al,0C8H
      
    2. 立即数的后缀表示进制,H表示16进制,B表示8进制。十进制数无需任何前缀后缀,系统自动将其转换成补码。

      add dl,10101010B   ;8进制
      add eax,12345678H  ;16进制
      mov al,-4          ;[-4]补=FCH-->al
      
    3. 立即数是字符要包上单引号。

      mov dl,'A'
      
    4. 可以用*+-/组成立即数表达式

      mov si,3*5
      
  • 寄存器操作数:操作数放在CPU某个寄存器中

  • 存储器操作数:操作数存放在寄存器中

  • I/O端口操作数:操作数存放在I/O端口中

(二)实模式下寻址方式

  • 立即寻址:获取立即数。

    mov dx,1234H 
    
  • 寄存器寻址:获取寄存器操作数。

    mov ax,bx
    
  • 存储器寻址:获取存储器操作数。

    • 直接寻址:直接在指令中给出偏移地址。

      mov al,ES:[2CH]  ;直接给出偏移地址是2CH
      

      有的时候为了省去手动计算偏移地址的麻烦,我们会用变量名代替上面给出的存储单元的有效地址。变量名关联着一个单元,也有着段基址和偏移量两种属性,所以段基址也可以省略不写。

      mov al,ES:var
      ;相当于 
      mov al,var
      ;相当于
      mov al,[var]
      
    • 间接寻址:偏移地址在寄存器(间址寄存器)中。当访问约定的逻辑段时,段寄存器可以省略不写。并不是所有寄存器都能充当间址寄存器,16位寻址和32位寻址可采用的间址寄存器及对应的约定逻辑段罗列如下。

      间址寄存器 约定访问的逻辑段 字长
      BP 堆栈段SS 16位寻址方式
      BX,SI,DI 数据段DS
      EBP,ESP 堆栈段SS 32位寻址方式
      EAX ~ EDX,ESI,EDI 数据段DS
      mov ax,SS:[bp]
      ;相当于
      mov ax,[bp]
      
      mov ax,DS:[bp]
      ;不等同于
      mov ax,[bp]
      
    • 基址寻址:有效地址由2部分组成,一部分在基址寄存器,另一部分是常量。同样并不是所有寄存器都能充当基址寄存器,16位寻址和32位寻址可采用的基址寄存器及对应的约定逻辑段罗列如下。

      基址寄存器 约定访问的逻辑段 字长
      BP 堆栈段SS 16位寻址方式
      BX 数据段DS
      EBP,ESP 堆栈段SS 32位寻址方式
      EAX ~ EDX,ESI,EDI 数据段DS
      mov ax,SS:[bp+1]
      ;相当于
      mov ax,[bp+1]
      
      mov ax,DS:[bp+1]
      ;不等同于
      mov ax,[bp+1]
      
    • 变址寻址

      \[段寄存器:[比例因子*变址寄存器+偏移量](仅可用于32位寻址,且比例因子只能是1,2,4,8) \]

      \[段寄存器:[变址寄存器+偏移量] \]

      同样并不是所有寄存器都能充当变址寄存器,16位寻址和32位寻址可采用的变址寄存器及对应的约定逻辑段罗列如下。

      变址寄存器 约定访问的逻辑段 适用于
      SI,DI 数据段DS 无比例因子,16位寻址方式
      EBP 堆栈段SS 有比例因子,32位寻址方式
      EAX ~ EDX,ESI,EDI 数据段DS
      ```assembly mov al,[8*SI+15] ;非法,比例因子只能用于32位寻址 mov ax,[7*ESI+5] ;非法,比例因子只能是1,2,4,8 ```
    • 基址加变址寻址

      \[段寄存器:[基址寄存器+比例因子*变址寄存器+偏移量] \]

      此时不存在约定逻辑段的概念,所以段寄存器最好需要明确写出。结构体通常采用的寻址方式就是基址加变址寻址。

      mov ax,[BP+SI-200H] ;默认访问堆栈段
      

四、汇编源程序

汇编源程序的构成

  • 指令性语句:即符号指令,由CPU执行

    符号指令对应目标指令(机器指令),硬件只能识别、存储、运行目标指令。

  • 指示性语句

    • 伪指令:为汇编链接工具提供信息,在汇编链接期间由汇编链接工具执行。
    • 宏指令

(一)伪指令

伪指令为汇编程序链接程序提供信息,功能由相应软件完成。

1. 数据定义伪指令

  • DB:字节定义/伪指令。

    \[变量名\quad DB\quad 一个或者多个用逗号间隔的单字节数 \]

  • DW:字定义伪指令。

    \[变量名\quad DW\quad 一个或者多个用逗号间隔的双字节数 \]

    例如:

    N1 DB 12H,64,-1,3*3
       DB 01010101B,'A',0A6H,'HELLO'
    N2 DB ?,?       ;?代表随机数,相当于N2 DB 2DUP(?)
    N3 DW 1234H,12,'AB','C'
       DW ?,? 
    

    DUP的用法:

    \[DB/DW/DD/DQ\quad重复的次数\quad DUP(重复的内容) \]

    汇编程序把DB后面的单字节数依次存入从第一个定义的变量开始的单元,这些内存单元的属性都是”字节型”,负数用补码表示,单引号的字符翻译成ASCII码。

    汇编程序把DW后面的双字节数依次存入从第一个定义的变量开始的单元,每一个数占2个字节,低位放到低地址单元,高字节放到高地址单元,这些内存单元的属性都是”字型”,负数用补码表示,单引号的字符翻译成ASCII码。

    根据上面的案例,各个变量在内存中的地址分配如下。

    值得注意的是,我们访问内存单元的时候偏移量仍然是以字节为单位的,字属性仅仅是与内存单元的读取有关,例如

    mov ax,N3+1
    

    此时的ax寄存器中的值并不是N3变量的第二个字12(000CH),而是0C12H,也就是说这里的偏移量1并不是1个字,而是一个字节,尽管N3拥有字属性。

  • DD/DF/DQ/DT:分别是双字定义伪指令,6字节定义,8字节定义,10字节定义

    \[变量名\quad DD/DF/DQ/DT\quad 一个或者多个用逗号间隔的数据 \]

    类比DW。

2. 符号定义伪指令

类似于C语言中的宏定义。

  • EQU:等值伪指令,但EQU后面的值一旦定义之后是不能修改的

    \[符号常数\quad EQU\quad 表达式 \]

    例如:

    NUM EQU 33
    mov al,NUM   ;该指令相当于mov al,33,此时是立即寻址
    NUM EQU 22   ;非法语句
    
  • =:等号伪指令(equal-sign directive),把一个符号名称与一个整数表达式连接起来。

    \[符号常数name\quad =\quad 表达式expression \]

    通常,表达式是一个 32 位的整数值。当程序进行汇编时,在汇编器预处理阶段,所有出现的 name 都会被替换为 expression。使用符号将会让程序更加容易阅读和维护。等号后面的值定义之后是可以修改的,如果符号名称会在整个程序中出现多次,那么,在之后的时间里,程序员就能方便地重新定义它的值

    例如:

    NUM = 33
    mov al,NUM   ;该指令相当于mov al,33,此时是立即寻址
    NUM = 22
    mov al,NUM   ;该指令相当于mov al,22,此时是立即寻址
    

3. 运算符

运算符通常在汇编指令之前就完成了计算,此时表达式也被计算结果代替。

  • []:访问内存操作数,可以标注数组元素下标。

  • $:可以返回汇编计数器(以字节为单位)的当前值,通常用于计算字符串、数据段的长度。

    BUF DB 'Hello World'
    LENGTH1 EQU $-BUF     ;此时LENGTH1就是字符出的长度
    
    BUF DB 'Hello World'
    BUF1 EQU 'A'
    LENGTH2 EQU $-BUF     ;此时LENGTH2仍然是字符出'Hello World'的长度,因为BUF1 EQU 'A'其实是不占空间的
    
  • SEG:计算某一逻辑段的段基址。

    \[SEG\quad段名/变量名/标号名 \]

    mov ax,SEG DATA  ;DATA是数据段的段名
    mov DS,ax        ;数据段段基址放到数据段段寄存器
    
  • OFFSET:计算某个变量或者标号名所在单元相对于段首的偏移基址。

    \[OFFSET\quad 变量名/标号名 \]

  • 算术运算符、逻辑运算符、关系运算符

    可以用AND、OR、XOR(异或)、NOT(非)、SHL(左移位)、SHR(右移位)组合成逻辑表达式

    或者用EQ(等于)、NE(不等于)、GT(大于)、LT(小于)、GE(大于或等于)、LE(小于等于)组合成关系表达式,如果为真,结果是FFFFH,否则为0。

  • PTR:在本条指令中临时修改地址表达式的属性,即让该地址表达式指向的单元在该指令中具备类型说明符指明的属性。

    \[类型说明符\quad PTR\quad 地址表达式 \]

    【注意】:

    • 地址表达式的原类型属性取决于变量在定义时所采用的数据定义伪指令,也就是说数据定义伪指令定义的该内存操作数的属性字长必须要跟另一个操作数一致,否则就要临时修改地址表达式的属性

    类型说明符和地址表达式指向之间存在一定的对应关系。

    类型说明符 地址表达式
    BYTE、WORD、DWORD、FAR、NEAR 内存单元的5种寻址方式
    FAR、NEAR 子程序的名称

    在双操作数指令中当出现下面几种的情况时,

    (1)源操作数为立即数,目标操作数为直接寻址内存操作数。两者不一致时,目标操作数必须要用PTR临时修改属性。

    (2)源操作数是单字节/双字节立即数,目标操作数为非直接寻址的内存操作数,无论是否一致,都必须要用PTR临时修改属性

    修改的属性不一定要跟立即数一致,但不能低于立即数的字长。

    mov BYTE PTR [BX], 12H    ;√
    mov WORD PTR [BX], 12H    ;√
    
    mov BYTE PTR [BX], 0012H  ;× (BYTE位数显然低于0012H的位数)
    mov WORD PTR [BX], 0012H  ;√
    

    (3)源操作数和目标操作数有一方是直接寻址内存操作数(如果源操作数是直接寻址内存操作数,那么目标操作数必然是寄存器操作数,因为双操作数不允许源操作数和目标操作数都是内存操作数),但两者的属性不一致,则必须用PTR临时修改直接寻址的操作数属性。

    BUF DB 'Em0s_Er1t'
    
    MOV AX,BUF           ;×
    MOV AL,BUF           ;√
    MOV AX,WORD PTR BUF  ;√
    

    在单操作数指令中当出现下面几种的情况时,

    (1)非直接寻址的内存操作数必须用PTR说明

    (2)如果是直接寻址的内存操作数,要根据指令对操作数的类型属性要求(如push指令)该条指令的操作意图

4. 其他伪指令

(1)宏指令

宏指令是汇编语言提供的原指令。

宏指令分为有参数宏指令和无参数宏指令。

宏指令先定义后调用。

宏指令的定义可以不发生在任何逻辑段中,习惯上放在源程序首部。

宏指令与子程序

  • 在对源文件进行汇编时,编译器会自动用宏体替换宏调用。
  • 而子程序在程序运行过程中切换运行程序主体代码。
  • 对于相同功能的宏指令和子程序,宏指令一定程度上比子程序花费更少的时间。
;无参宏指令定义
此无参宏指令名称 MACRO
              ......  ;宏体
              ENDM

;有参宏指令定义
此有参宏指令名称 MACRO 用逗号分隔的哑元表
              ......  ;宏体
              ENDM
              
;有参宏指令调用
宏指令名称  实元表       ;实元表与哑元表一一对应

例如用宏指令实现回车换行如下

.586
CRLF MACRO         ;此处实现回车换行的功能
     mov ah,0EH    
     mov al,0DH    ;回车
     int 10H
     mov al,0AH    ;换行
     int 10H
     ENDM

CODE segement USE16
assume CS:CODE
BEG:
     ......
     CRLF          ;宏调用
     ......

(2)LOCAL伪指令

如果在一个宏指令的宏体中用了一个标号,且在程序中多次调用了这个宏指令,那么由于宏指令会被汇编程序替换这个机制,就容易出现重复定义的错误

\[LOCAL\quad 用逗号间隔的标号名 \]

将标号名定义为局部标号,避免重复调用宏指令出现重名的问题。放在宏体的开头部分

(3)ORG

通知汇编程序将下一条指令或数据存放在表达式给出的段内起始偏移地址

(4)PROC & ENDP

PROC和ENDP定义一个过程

  • ENDP:过程定义结束伪指令

(5)ASSUME

段约定伪指令,指明变量与段寄存器的联系,通知MASM寻址某一特定段时要使用哪一个段寄存器

(二)符号指令

在进行数据传送或者运算时,要注意两个操作对象的属性是一致的,比如add bx,al 就是错的。

双操作数指令不允许两个操作数都是内存操作数。

MOV

mov指令有几个注意点:

  • mov指令不允许把数据直接送入段寄存器,如mov ds,1000H\(\color{red}{(\times)}\)

    但可以用一个寄存器作中转,如mov bx,1000H;mov ds,bx\(\color{red}{(\surd)}\)

  • mov指令不允许用来直接设置CS和IP的值,如mov IP,0H\(\color{red}{(\times)}\)

LEA

有效地址传送指令。将内存表达式的有效地址放入目标寄存器中。

\[LEA\quad R16/R32,\quad内存地址表达式 \]

lea ax, BUF
mov ax, offset BUF   ;这两个指令等价

XCHG

完成2个操作数的互换。

  • 这两个操作数不能都是内存操作数
  • 操作数不能是段寄存器和立即数

PUSH/POP & PUSHF/POPF & PUSHA/POPA

“堆栈宽度”是可以推入堆栈的最小数据量,被定义为处理器的寄存器字长,所以8086CPU的入栈和出栈操作是以为单位进行的,Intel系列机中栈顶对应低地址,栈底对应高地址

栈为空时SS:SP指向栈空间下面一单元。

8086CPU不能自动检测栈溢出。

当向一个栈段内一直压栈,从SP=0xFFFF到SP=0后继续压栈,此时就会覆盖栈底的元素。

入栈,操作数可以是除CS之外的段寄存器16位及以上的一般寄存器带字属性的内存单元立即数

出栈,操作数可以是除CS之外的段寄存器16位及以上的一般寄存器带字属性的内存单元

  • push reg指令分为两步:

    1. SP=SP-2
    2. reg中的内容入栈
  • pop reg指令分为两步:

    1. SS:SP送入reg
    2. SP=SP+2
  • PUSHA指令依次把AX、CX、DX、BX、SP、BP、SI、DI的值压栈。

  • POPA指令则是以字为单位倒序出栈。

  • PUSHF指令把标志寄存器压栈

  • POPF指令出栈到各个标志寄存器

ADC/SBB & ADD/SUB & INC/DEC

  • ADC/SBB是带进位(上条指令执行后的CF标志寄存器中的值)的加减运算指令。

    影响所有状态标志寄存器

  • ADD/SUB是不带进位的加减运算指令。

    影响所有状态标志寄存器

  • INC/DEC是自增1自减1的单操作数指令。

    不影响C标志

    影响的是A、O、P、S、Z标志

NEG

求补单操作数指令,影响所有状态标志寄存器。

neg ax
相当于
mov bx,0
sub bx,ax
mov ax,bx

常用于负数取绝对值

CMP

比较指令,通过 “目标操作数-源操作数”来影响所有状态标志寄存器,即按照与 SUB 指令相同的方式设置状态标志,但不改变源操作数和目标操作数

该指令通常跟条件转移指令配合使用。

MUL/IMUL

此指令的操作与结果的位置取决于操作码与操作数大小。

1. 单操作数形式

\[MUL/IMUL\quad 乘数 \]

实现乘法的隐含操作数指令,此时乘数不能是立即数(无法确定乘数的字长)

  • MUL是无符号二进制乘法

  • IMUL是有符号二进制乘法

MUL/IMUL 被乘数默认在 乘数为 高位积在 低位积在
字节相乘 AL R8/M8 AH AL
字相乘 AX R16/M16 DX AX
双字相乘 EAX R32/M32 EDX EAX
> 如果乘法运算之后产生了高位积,则CF=1、OF=1,此时要到对应的存放高位积的寄存器中去获取高位积的值。 > > **如果没有产生高位积则C和O标志位置0**。

2.双操作数形式和三操作数形式

只有imul才具备这两种形式。

双操作数形式:目标操作数是通用寄存器,源操作数可以是立即数、通用寄存器或内存位置。乘积随后存储到目标操作数位置。

\[IMUL\quad 目标操作数,源操作数 \]

三操作数形式:此种形式需要一个目标操作数(第一个操作数)与两个源操作数(第二个与第三个操作数)。这里,第一个源操作数(可以是通用寄存器或内存位置)乘以第二个源操作数(立即数),乘积随后存储到目标操作数(通用寄存器)。\(源操作数\times 立即数\rightarrow 目标操作数\)

\[IMUL\quad 目标操作数,\quad 源操作数,\quad 立即数 \]

有效位进位到结果的上半部分时,CF 与 OF 标志设置为 1。结果正好可以存储到结果的下半部分时,清除 CF 与 OF 标志。

【注意】

  • 对于双操作数或三操作数形式,在将结果存储到目标寄存器之前,需要将它截断至目标寄存器的长度。由于此种截断的原因,应该测试 CF 或 OF 标志,以确保不丢失有效位。

  • 无论操作数是否有符号,乘积的下半部分都相同,所以双操作数与三操作数形式也可以使用无符号操作数。不过,此时不能使用 CF 与 OF 标志确定结果的上半部分是否非零。

DIV/IDIV

\[DIV/IDIV\quad 除数 \]

实现除法的隐含操作数指令

  • DIV是无符号二进制乘法

  • IDIV是有符号二进制乘法

DIV/IDIV 除数由指定格式指定 被除数默认在 商在 余数在
字节除法 R8/M8 AX AL AH
字除法 R16/M16 DX=高16位
AX=低16位
AX DX
双字除法 R32/M32 EDX=高32位
EAX=低32位
EAX EDX

被除数应该是除数的双倍长度

如果除数太小,使商超过范围,会引发内中断,此时对中断的处理由操作系统决定

DAA——组合BCD码调整

有的时候我们需要通过计算机将两个10进制数相加,并能够很容易看出来相加结果的大小,所以选择用BCD码表示这个结果会比二进制数据更加直观。在用BCD码表示加数和被加数之和,我们通过DAA指令对结果进行适当修正,就能得到结果的BCD码形式。

关于BCD码的表示规范

  • 组合形式:一个字节中含有2位BCD码,如十进制69可以用BCD码69H代表
  • 未组合形式:一个字节中含有1位BCD码,高四位为0,如十进制69可以用BCD码09H,06H代表

BCD码修正指令,根据标志位选择适当的修正数,所以DAA指令必须紧跟在加减指令之后,默认操作对象是AL,根据具体情况对AL中的高低4位进行修正。

;计算1234+5678,计算结果最终存入SUM
N1 DW 1234H
N2 DW 5678H
SUM DW ?

mov al, byte ptr N1
add al, byte ptr N2
DAA                        
mov byte ptr SUM, al       ;al=12
mov al, byte ptr N1+1
adc al, byte ptr N2+1      
DAA                        
mov byte ptr SUM+1, al     ;al=69,SUM=6912H

转移指令

  • 实现段内短转移

\[jmp/jnz/……\quad short\quad 标号 \]

  • 实现段间转移

\[jmp\quad 标号 \]

short是短转移,相对于指令地址\(+129\)\(-126\)个单元(正代表向下,负代表向上)。

不加short是长转移可以转移的范围是64k个单元。

1. 无条件跳转指令JMP

\[jmp\quad 标号 \]

相当于C语言的goto。

2. 条件跳转指令

除了A标志位,其余的标志位的状态都可以单独作为条件跳转指令的条件,如下:

  • jc 跳转指令,条件:CF=1

  • jnc 跳转指令,条件:CF=0

  • jz 跳转指令,条件:ZF=1

  • jnz 跳转指令,条件:ZF=0

  • js 跳转指令,条件:SF=1

  • jns 跳转指令,条件:SF=0

  • jp 跳转指令,条件:PF=1

  • jnp 跳转指令,条件:PF=0

  • jo 跳转指令,条件:OF=1

  • jno 跳转指令,条件:OF=0

其他的一些条件跳转指令如下:

  • ja 跳转指令,条件:CF=0 和 ZF=0
  • jab 跳转指令,条件:CF=0
  • jb 跳转指令,条件:CF=1
  • jbe 跳转指令,条件:CF=1 或者 ZF=1
  • jcxz 跳转指令,条件:CX=0
  • je 跳转指令,条件:ZF=1
  • jecxz 跳转指令,条件:ECX=0
  • jg 跳转指令,条件:ZF=0 和 SF=OF
  • jge 跳转指令,条件:SF=OF
  • jl 跳转指令,条件:SF!=OF
  • jle 跳转指令,条件:ZF=1 和 SF!=OF
  • jna 跳转指令,条件:CF=1 或者 ZF=1
  • jnae 跳转指令,条件:CF=1
  • jnb 跳转指令,条件:CF=0
  • jnbe 跳转指令,条件:CF=0 和 ZF=0
  • jne 跳转指令,条件:ZF=0
  • jng 跳转指令,条件:ZF=1 或者 SF!=OF
  • jnge 跳转指令,条件:SF!=OF
  • jnl 跳转指令,条件:SF=OF
  • jnle 跳转指令,条件:ZF=0 和 SF=OF
  • jpe 跳转指令,条件:PF=1
  • jpo 跳转指令,条件:PF=0

3.循环控制转移指令LOOP

      mov cx, 值
next:   ......
	    ......
	    ......
	  loop next

4. 应用

  • 实现无符号数条件跳转指令

    cmp N1, N2    ;若程序员认定N1,N2是无符号数
    ja  标号       ;跳转条件N1>N2
    jna 标号       ;跳转条件N1<=N2
    jc 标号        ;跳转条件N1<N2
    jnc 标号       ;跳转条件N1>=N2
    
  • 实现有符号数条件转移指令

    cmp N1, N2    ;若程序员认定N1,N2是无符号数
    jg 标号        ;跳转条件:N1的真值>N2的真值
    jge 标号       ;跳转条件:N1的真值>=N2的真值
    jl 标号        ;跳转条件:N1的真值<N2的真值
    jle 标号       ;跳转条件:N1的真值<=N2的真值
    

CALL & RET/RETN

(CALL)调用子程序并(RET)返回。

;子程序编写的模板
子程序名称  PROC
		  .....
		  ;实现
		  .....
		   RET
子程序名称  ENDP		
  • 段内调用:主程序和子程序在同一个代码段,用堆栈只需保存IP。
  • 段间调用:主程序和子程序在不同的代码段,先把CS压栈,再把IP压栈。(RET,先实现POP IP,后实现POP CS)

RETN:返回并修改栈顶,例如

retn 8

相当于

pop IP
add esp,8

常用于stdcall约定的函数中由函数自己释放栈空间。

INT/IRET

INT调用中断程序,IRET返回

\[INT\quad N \]

N是中断号(\(0 \backsim 255\)),每一个号都对应一个中断处理程序,相比于call,int调用的是中断处理程序,且进入中断处理程序前先将标志寄存器入栈,再将CS和IP入栈。主要的应用是功能调用,后面会详细讲述。

关于向子程序传递参数的方法:

  • 通过寄存器传递参数(最常用)
  • 利用堆栈区传递参数(通常用于实现某种特定的数据结构)
  • 利用内存单元传递参数

逻辑运算指令

所有逻辑运算指令都影响P、S、Z标志,且C、O运算之后置0。

  • NOT:所有位按位取反。单操作数指令
  • AND:双操作数与逻辑运算,常用于取位操作。
  • OR:双操作数或逻辑运算,常用于置数。
  • XOR:双操作数异或逻辑运算,常用清0操作,数据位取反操作。
  • TEST:相当于不改变2个操作数的值进行与运算。

    应用

    • 可以用于检测特定位是1还是0。
    • 可以用于检测寄存器是否为空。
      test ax, ax
      jnz 标号
      

移位指令

1. 开环移位指令

\[SAL/SAR/SHL/SHR\quad 操作数\quad 移动位数 \]

移动位数可以是CL或者立即数

  • SAL/SAR算术移位:左移时0补全,右移时符号位(最高位)补全。(通常用于有符号数的运算)

  • SHL/SHR逻辑移位:左移时0补全,右移时0补全。(通常用于无符号数的运算)

2.闭环移位指令

\[RCL/RCR/ROL/ROR\quad 操作数\quad 移动位数 \]

移动位数可以是CL或者立即数

处理机控制指令

STD/CLD

通常与串传送指令配合使用。

改变控制标志寄存器中D标志的值。

  • 无操作数指令STD可以使D标志置1。
  • 无操作数指令CLD可以使D标志置0。

STC/CLC/CMC

  • STC可以使C状态标志位置1
  • CLC可以使C状态标志位置0
  • CMC可以使C状态标志位取反

STI/CLI

  • STI可以使I标(中断标志)置1
  • CLI可以使I标置0

HLT/NOP

  • HLT是暂停操作
  • NOP是空操作

串操作指令

  • 串操作数指令都是隐含指令,源串要放在DS数据段,目标串要放在ES附加段。

  • 对于16位寻址操作:

  • 访问源串一定要用SI寄存器间址访问,访问附加段一定要用DI寄存器间址访问,一定要用CX作为串计数器。

  • 对于32位寻址操作:

  • 访问源串一定要用ESI寄存器间址访问,访问附加段一定要用EDI寄存器间址访问,一定要用ECX作为串计数器。

  • SI、DI、CX的值会自动修改

1. 串传送指令

内存到内存。

运行之前自动根据控制标志位D的值对应的元素属性自动修改间址寄存器的值指向下一次比较的元素。(控制标志位D的值决定是增量传送还是减量传送,D=0,则为增址型;D=1,则为减址型)

  • \(\textcolor{red}{MOVSB}\):传送一个字节元素。

  • \(\textcolor{red}{MOVSW}\):传送一个字元素。

  • \(\textcolor{red}{MOVSD}\):传送一个双字元素。

  • \(\textcolor{red}{REP\quad MOVSB/MOVSW/MOVSD}\):实现重复传送。以整块移动 CX 个字节、字或双字

2. 串装入指令

内存到寄存器。

  • \(\textcolor{red}{LODSB}\):将DS:[SI]的一个字节存入AL,根据控制标志位D和元素属性自动修改SI
  • \(\textcolor{red}{LODSW}\):将DS:[SI]的一个字存入AX,根据控制标志位D和元素属性自动修改SI
  • \(\textcolor{red}{LODSD}\):将DS:[SI]的一个双字存入EAX,根据控制标志位D和元素属性自动修改SI

3.串存储指令

寄存器到内存。

  • \(\textcolor{red}{STOSB}\):将AL内的值存入ES:[DI]的1个单元,根据控制标志位D和元素属性自动修改DI
  • \(\textcolor{red}{STOSW}\):将AX内的值存入ES:[DI]的2个单元,根据控制标志位D和元素属性自动修改DI
  • \(\textcolor{red}{STOSD}\):将EAX内的值存入ES:[DI]的4个单元,根据控制标志位D和元素属性自动修改DI
  • \(\textcolor{red}{REP\quad STOSB/STOSW/STOSD}\):实现重复存储,存储元素个数取决于计数器CX的值。

4. 串比较指令

比较源目操作数对应的元素,相等则置ZF=1,不相等则置ZF=0。

运行之前,自动根据控制标志位D的值对应的元素属性自动修改间址寄存器的值指向下一次要比较的元素。(控制标志位D的值决定是增量传送还是减量传送,D=0,则为增址型;D=1,则为减址型)

  • \(\textcolor{red}{CMPSB}\):比较字节元素

  • \(\textcolor{red}{CMPSW}\):比较字元素

  • \(\textcolor{red}{CMPSD}\):比较双字元素

  • \(\textcolor{red}{REPE\quad CMPSB/CMPSW/CMPSD}\):逐个比较多个元素是否对应相等,都相等则置1,出现有一个不等则置0,不再继续比较,结束当前指令。比较个数取决于计数器CX内的值。

  • \(\textcolor{red}{RENPE\quad CMPSB/CMPSW/CMPSD}\):逐个比较多个元素是否对应不相等,有一个不等则置0,出现有一个相等则置1,不再继续比较,结束当前指令。比较个数取决于计数器CX内的值。

    适用于搜索字符串。

5.串搜索指令

在ES:[DI]的目标区,搜索是否有指定的“关键字”,即比较\(AL/AX/EAX=ES:[DI]\)? 修改DI,若\(ES:[改前DI]=关键字\),则Z置1,否则Z置0 。

  • \(\textcolor{red}{SCASB}\)

  • \(\textcolor{red}{SCASW}\)

  • \(\textcolor{red}{SCASD}\)

  • \(\textcolor{red}{REPE\quad SCASB/SCASW/SCASD}\):搜索与指定关键字不相同的(相同则继续搜索,不同则停止搜索)

  • \(\textcolor{red}{RENPE\quad SCASB/SCASW/SCASD}\):搜索指定关键字(不同则继续搜索,相同则停止搜索)

    REPE和RENPE都是重复前缀,只是停止的条件不同。

6. I/O串操作

CDQ

把EDX的所有位都设成EAX最高位的值(官方文档是说把32位扩展成64位,即\(EAX\rightarrow EDX:EAX\))。

  • \(EAX <80000000\)\(EDX\)\(0000\ 0000\)\(80000000\)以下是正数,原码0开头);
  • \(EAX \ge 80000000\)\(EDX\)则为\(FFFF\ FFFF\)\(80000000\)及以上是负数,原码1开头)。

应用:大多出现在除法运算之前。

例如idiv 32位的除数要求被除数是64位,且高32位在EDX,低32位在EAX,如果被除数其实只用32位就能存储完,DX里存储的是别的值,但为了适应imul的要求,就需要用CDQ指令把被除数扩展成64位。

(三)功能调用

汇编程序可以直接进行DOS功能调用(轮子集成在操作系统内核中)和BIOS功能调用(轮子集成在BIOS主板上,独立于操作系统,更加灵活复杂)。

mov ah,功能号m            ;由CPU执行n号中断服务程序的m号功能
设置入口参数
int 中断号n               ;调用n号中断服务程序
分析出口参数

下面是常用的几个功能调用中断服务程序。

INT 21H【DOS功能调用】

21H号中断服务子程序不同功能号实现的功能如下:

  • 功能号01H:等待键入一个字符,有回显,响应Ctrl_C退出

    出口参数:

    • AL存放按键的ASCII码。(键入”3”,那么AL=33H)

    • 如果按下功能键、光标键,则AL=0,需要再次调用此功能才能返回按键的扩展码。

  • 功能号07H:等待键入一个字符,无回显,不响应Ctrl_C

  • 功能号08H:等待键入一个字符,无回显,响应Ctrl_C

  • 功能号0AH:等待键入一串字符,存入用户程序数据缓冲区,有回显。

    入口参数:

    • DS:DX指向存放键入字符的缓冲区(缓冲区数据段需要按照约定定义)

      BUF DB n        ;能容纳的最大字符个数n(把回车键考虑在内)(程序员定义)
      	DB ?        ;实际接收的字符个数(由0A号服务程序写入)(不包括回车键)
      	DB n DUP(?) ;存放输入字符的ASCII码(由0A号服务程序写入)(把回车键考虑在内)
      
  • 功能号02H:显示一个字符,响应Ctrl_C,会破坏AL寄存器中的内容(间接调用了BIOS功能调用的0EH功能号对应的程序)。

    入口参数:

    • DL存放待显示字符的ASCII码。
  • 功能号09H:从屏幕的当前位置开始显示字符串,响应Ctrl_C ,会破坏AL寄存器中的内容

    入口参数:

    • DS:DX=字符串的首地址,字符串必须以’$’作为结束标志(’$’不显示)
  • 功能号4CH:中止当前程序的运行,并把控制权交给调用它的程序。

    入口参数:

    • 设置AL=返回码(也可以不设置)

INT 16H【BIOS功能调用】

实现键盘功能调用

  • 功能号00H:读取键入的一个字符,无回显,响应Ctrl_C,无键入则等待。

    出口参数:

    • AL存放按键的ASCII码。(键入”3”,那么AL=33H)
    • 若AL=0,则AH存放扩展码。

INT 10H【BIOS功能调用】

实现文本显示功能调用

  • 功能号0EH:显示一个字符。

    入口参数:

    • AL=待显示字符的ASCII码

(四)完整汇编程序编写

下面是模板

.586                      ;处理器选择伪指令(指定CPU类型)
DATA segment USE16        ;段定义伪指令(数据段)
	FIRST DB ......
	SECOND DB ......
	SUM DB ......
DATA ends
CODE segment USE16        ;段定义伪指令(定界,标志了名为CODE的代码段开始,实模式下编程段长度为16,表示字长16位,按16位寻址,段长度是2^16=64K)
assume CS:CODE,DS:DATA    ;段约定伪指令
        ORG 100H          ;从代码段开始偏移100H处开始存放BEG(通常在COM格式中使用)
    BEG:mov ax,DATA       ;段寄存器赋值
        mov ds,ax
         ... ...
         ;实现
         ... ...
	    mov ah,4cH        ;【由CPU执行,实现程序返回功能,返回操作系统(DOS)】
	    int 21H
	    
	     
     fun PROC             ;名为fun的子程序(函数)编写
		 ... ...
		  ;实现
		 ... ...
	 	 RET
     fun ENDP	          ;
CODE ends                 ;(定界,标志了名为codesg的代码段结束)
end BEG                   ;汇编结束伪指令(标志整个源程序的结束)

段寄存器赋值

  • CS:IP 由DOS自动赋初值。
  • SS:SP 由DOS自动赋初值(字长16位,系统自动申请128个字节的堆栈段),或由程序员赋给。
  • 其他段需要程序员自己赋值。

(五)编译运行可执行文件

\(\color{red}{编写源码文件1.asm\xrightarrow{汇编编译器编译}1.obj\xrightarrow{链接器连接}1.exe\xrightarrow{由command命令解释器加载}内存中的程序\rightarrow运行}\)

1. 可执行文件

  • .COM:源程序只能有一个逻辑段,即代码段(代码段中可以插入数据,但执行到此处时需要转移),适用于编写中小型程序,体量小。
  • .EXE:允许使用多个逻辑段

2. 可执行文件的运行机制

在DOS系统中,系统启动时,首先进行一些初始化操作,然后运行command.com命令解释器,运行后执行完相关任务后,屏幕上才显示命令提示符,等待用户输入,用户可以输入一些如cd,dir之类的命令由command执行。
image-20210710212659233
而要想在DOS系统中执行可执行文件,那么就要输入可执行文件名,command根据文件名将其加载入内存,设置CS:IP指向程序入口处,command停止运行,CPU控制权交给该程序,程序运行完成后把CPU控制权交还给command,如此往复……

但debug程序是如何实现对程序的调试的?其实在运行debug的时候,command一如既往把CPU控制权交给debug程序,debug程序从开始运行到调试结束都没有放弃对CPU的控制,而在这个过程中debug程序把待调试程序载入内存,CS:IP指向程序入口。

image-20210710213638828

五、汇编浮点运算

参考

(一)FPU数据寄存器

浮点数的运算离不开浮点运算器,即FPU。FPU 不使用通用寄存器 (EAX、EBX 等等),它有自己的8个独立、可寻址的80位寄存器\(R_0\sim R_7\),称为**寄存器栈 **(register stack),FPU 状态字中名为 TOP 的一个 3 位字段给出了当前处于栈顶的寄存器编号。

在编写浮点指令时,栈顶的位置也称为 ST(0)(或简写为 ST),最后一个寄存器被称为 ST(7),对于开发者而言就不用去在意当前栈顶是哪个寄存器了。

注意:ST(n)所对应的寄存器会随着每次入栈出栈操作会发生变化,因为此时栈顶寄存器发生了变化。

浮点数值从内存加载到寄存器栈,然后后缀表达式形式计算,再将堆栈数值保存到内存,寄存器中浮点数使用的是 IEEE 10 字节扩展实数格式(也被称为临时实数(temporary real))。当 FPU 把算术运算结果存入内存时,它会把结果转换成如下格式之一:整数、长整数、单精度(短实数)、双精度(长实数),或者压缩二进制编码的十进制数(BCD)。

与内存堆栈一样,寄存器堆栈也有入栈出栈操作

  • 入栈:TOP减一,即指向更小编号的寄存器,但如果此时入栈前TOP=0,那么入栈后TOP=7。
  • 出栈:TOP加一,即指向更小编号的寄存器,但如果此时入栈前TOP=7,那么出栈后TOP=0。

(二)FPU专用寄存器

FPU 有 6 个专用(special-purpose)寄存器,如下图所示:

  • 操作码寄存器:保存最后执行的非控制指令的操作码。

  • 控制寄存器:执行运算时,控制精度以及 FPU 使用的舍入方法。还可以用这个寄存器来屏蔽(隐藏)单个浮点异常。

  • 状态寄存器:包含栈顶指针、条件码和异常警告,每一位表示的含义如下:

    15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
    B(FPU正在计算中的忙标志) C3 TOP(栈顶指针) C2 C1 C0 ES(错误摘要状态) SF(栈错误) PE(精度异常标志) UE【溢出异常(值太小时溢出)标志】 OE【溢出异常(值太大时溢出)标志】 ZE(除零异常标志) DE(非常规操作数异常标志) IE(无效操作异常标志)

    浮点比较指令影响C3、C2、C0这3个标志位

    条件 C3(零标志位) C2(奇偶标志位) C0(进位标志位) 使用的条件跳转指令
    ST(0) > SPC 0 0 0 JA.JNBE
    ST(0) < SPC 0 0 1 JB.JNAE
    ST(0) = SPC 1 0 0 JE.JZ
    无序 1 1 1 (无)
  • 标识寄存器:指明 FPU 数据寄存器栈内每个寄存器的内容。其中,每个寄存器都用两位来表示该寄存器包含的是一个有效数、零、特殊数值 (NaN、无穷、非规格化,或不支持的格式 ),还是为空。

  • 最后指令指针寄存器:保存指向最后执行的非控制指令的指针。

  • 最后数据(操作数)指针寄存器:保存指向数据操作数的指针,如果存在,那么该数被最后执行的指令所使用。

(三)浮点运算指令集

关于浮点运算指令集,还是建议以查阅官方文档为准,下面的仅供参考。

为了更容易区分同类指令之间的差距,下面阐述一下协处理器FPU指令的操作符(或助忆符)在命名设计时所遵循的规则

  1. 在操作符后面加上字母P

    表示该指令执行完后,还进行一次堆栈弹出操作,弹出栈顶数据以后要对其它的寄存器进行相应的调整。如:FADDP/FSUBP/FSUBRP /FMULP/FDIVP /FDIVRP等;

  2. 在操作符后面加上字母R

    表示将两个操作数的源/目的位置交换再进行运算,它仅限于减法、除法指令,因为加法和乘法不受源/目的操作数的位置影响结果。如:FSUBR和FDIVR等;

    • 不加R时 —— 目的操作数=目的操作数 op 源操作数
    • 加R模式 —— 目的操作数=源操作数 op 目的操作数

    假设:栈顶数据st(0)为10,内存变量data的值为1,分别执行下列指令将有不同的结果。

    FSUB data            ;ST(0)=ST(0)-data
    FSUBR data           ;ST(0)=data-ST(0) 
    FSUB ST(3), ST(0)    ;指令执行后,ST(3)=ST(3)-ST(0)
    FSUBR ST(3), ST(0)   ;指令执行后,ST(3)=ST(0)-ST(3)
    
  3. 操作符的第二个字母是I

    表示内存操作数是整数(注意:不能是BYTE类型)。它对加、减、乘、除指令以及堆栈操作指令都有效。

    FIADD data —— 整数加法,它表示内存单元data是一个整数,把该整数加到栈顶的浮点数上(ST(0)=ST(0)+data)。

  4. 操作符的第二个字母是B

    表示用于操作压缩的BCD码格式的内存操作数(用TWORD声明,10个字节),如FBLD和FBSTP等。

  5. 操作符的第二个字母是N

    表示在指令执行之前检查非屏蔽数值性错误。如:FSAVE和FNSAVE等,前者称为等待形式(wait version),后者称为非等待形式(no-wait version)。在使用.8087伪指令情况下,汇编程序会在等待形式的指令前面加上指令WAIT,而在非等待形式的指令前面加上空操作指令NOP。

接下来介绍几个常见的浮点运算指令。

FCOM/FCOMP/FCOMPP

比较2个实数

操作码 指令 说明
D8 /2 FCOM m32real 比较 ST(0) 与 m32real
DC /2 FCOM m64real 比较 ST(0) 与 m64real
D8 D0+i FCOM ST(i) 比较 ST(0) 与 ST(i)。
D8 D1 FCOM 比较 ST(0) 与 ST(1)。
D8 /3 FCOMP m32real 比较 ST(0) 与 m32real,并弹出寄存器堆栈。
DC /3 FCOMP m64real 比较 ST(0) 与 m64real,并弹出寄存器堆栈。
D8 D8+i FCOMP ST(i) 比较 ST(0) 与 ST(i),并弹出寄存器堆栈。
D8 D9 FCOMP 比较 ST(0) 与 ST(1),并弹出寄存器堆栈。
DE D9 FCOMPP 比较 ST(0) 与 ST(1),并弹出寄存器堆栈两次。

比较结果对状态寄存器的3位有影响,如下。

条件 C3 C2 C0
ST(0) > SRC 0 0 0
ST(0) < SRC 0 0 1
ST(0) = SRC 1 0 0
无序 1 1 1

FSTSW/FNSTSW

操作码 指令 说明
9B DD /7 FSTSW m2byte 检查未决的无掩码浮点异常之后,将 FPU 状态字存储到 m2byte
9B DF E0 FSTSW AX 检查未决的无掩码浮点异常之后,将 FPU 状态字存储到 AX 寄存器。
DD /7 FNSTSW m2byte 将 FPU 状态字存储到 m2byte,不检查未决的无掩码浮点异常。
DF E0 FNSTSW AX 将 FPU 状态字存储到 AX 寄存器,不检查未决的无掩码浮点异常。

可用指令FSTSW把其16位状态寄存器的值送到内存单元中。如果当前使用的是80287及其以后的协处理器,那么,可用指令FSTSW AX把该状态寄存器的值传送给通用寄存器AX

本篇博客默认协处理器是80287系列及以后的版本。

对于80287协处理器,它还可通过I/O地址00FAH~00FFH来实现其与CPU之间的数据交换,而80387~Pentium系列芯片,则是通过I/O地址800000FAH~800000FFH来实现这两者之间的数据交换

一旦状态寄存器的值复制到内存或AX中,那么,就可以在浮点运算之后配合一些位操作以及跳转指令,实现对其各状态位进行分析,检测出当前协处理器的工作状态。

  • 用TEST指令来检测其相应的状态位
    例如检测是否有“0作除数”的错误可以用如下代码。

    FDIV  DATA1   ;用协处理器中堆顶数据去除DATA1
    FSTSW AX      ;把状态寄存器的值传送给AX
    TEST AX, 4    ;测试第2位,即:检测ZE是否为1,即是否除以了0
    JNZ DIV_ERR   
    

    还有检测是否有“非法操作数”的错误。

    FSQRT         ;求协处理器中堆顶数据的平方根
    FSTSWAX
    TEST AX, 1    ;测试第0位,即:检测IE是否为1
    JNZSQRT_ERR
    
  • 用SAHF指令把Al传送给EFLAGES
    例如检测内存单元的数据与协处理器堆顶数据之间的大小关系。

    FCOM DATA1    ;内存单元DATA1的值与协处理器堆顶数据进行比较
    FSTSW AX
    SAHF          ;把AX的低字节存入CPU的状态寄存器
    JE ST_EQUAL   
    JB ST_BELOW
    JA ST_ABOVE
    

FLD

操作码 指令 说明
D9 /0 FLD m32real m32real 压入 FPU 寄存器堆栈。
DD /0 FLD m64real m64real 压入 FPU 寄存器堆栈。
DB /5 FLD m80real m80real 压入 FPU 寄存器堆栈。
D9 C0+i FLD ST(i) 将 ST(i) 压入 FPU 寄存器堆栈。此时,压入寄存器 ST(0) 相当于复制栈顶。

FLD(加载浮点数值)指令将浮点操作数复制到 FPU 堆栈栈顶(称为 ST(0))。操作数可以是 32 位、64 位、80 位的内存操作数(REAL4、REAL8、REAL10)或另一个 FPU 寄存器:

内存操作数类型 FLD 支持的内存操作数类型与 MOV 指令一样。

FST/FSTP - 存储实数

操作码 指令 说明
D9 /2 FST m32real 将 ST(0) 复制到 m32real
DD /2 FST m64real 将 ST(0) 复制到 m64real
DD D0+i FST ST(i) 将 ST(0) 复制到 ST(i)
D9 /3 FSTP m32real 将 ST(0) 复制到 m32real,并弹出寄存器堆栈
DD /3 FSTP m64real 将 ST(0) 复制到 m64real,并弹出寄存器堆栈
DB /7 FSTP m80real 将 ST(0) 复制到 m80real,并弹出寄存器堆栈
DD D8+i FSTP ST(i) 将 ST(0) 复制到 ST(i),并弹出寄存器堆栈

FDIV/FDIVP/FIDIV - 除法

操作码 指令 说明
D8 /6 FDIV m32real 将 ST(0) 除以 m32real,结果存储到 ST(0)。
DC /6 FDIV m64real 将 ST(0) 除以 m64real,结果存储到 ST(0)。
D8 F0+i FDIV ST(0), ST(i) 将 ST(0) 除以 ST(i),结果存储到 ST(0)
DC F8+i FDIV ST(i), ST(0) 将 ST(i) 除以 ST(0),结果存储到 ST(i)
DE F8+i FDIVP ST(i), ST(0) 将 ST(i) 除以 ST(0),结果存储到 ST(i),并弹出寄存器堆栈
DE F9 FDIVP 将 ST(1) 除以 ST(0),结果存储到 ST(1),并弹出寄存器堆栈
DA /6 FIDIV m32int 将 ST(0) 除以 m32int,结果存储到 ST(0)。
DE /6 FIDIV m16int 将 ST(0) 除以 m64int,结果存储到 ST(0)。

FADD/FADDP/FIADD - 加法

操作码 指令 说明
D8 /0 FADD m32real m32real 与 ST(0) 相加,结果存储到 ST(0)。
DC /0 FADD m64real m64real 与 ST(0) 相加,结果存储到 ST(0)。
D8 C0+i FADD ST(0), ST(i) 将 ST(0) 与 ST(i) 相加,结果存储到 ST(0)。
DC C0+i FADD ST(i), ST(0) 将 ST(i) 与 ST(0) 相加,结果存储到 ST(i)。
DE C0+i FADDP ST(i), ST(0) 将 ST(0) 与 ST(i) 相加,结果存储到 ST(i),并弹出寄存器堆栈
DE C1 FADDP 将 ST(0) 与 ST(1) 相加,结果存储到 ST(1),并弹出寄存器堆栈
DA /0 FIADD m32int m32int 与 ST(0) 相加,结果存储到 ST(0)
DE /0 FIADD m16int m16int 与 ST(0) 相加,结果存储到 ST(0)
posted @ 2021-09-26 23:47  Em0s_Erit  阅读(359)  评论(0编辑  收藏  举报