基础知识

语言分类

计算机运行由 CPU 控制,根据机器指令决定每一步的操作。计算机硬件能理解和执行的就是机器指令代码,该代码被称为“机器语言”

机器语言是由组成由二进制数组成的代码

1
2
3
1010 1111
0011 0111
0111 0110

采用助记符表示机器指令的操作码,用变量代替操作数存放地址形成的语言被称为“汇编语言”

汇编语言要翻译成机器语言程序才可以由计算机执行,翻译的过程被称为“汇编”

把汇编源程序翻译成目标程序的语言加工程序称为”汇编程序“

基本操作与机器指令相对应

1
2
3
MOV A,47
ADD A,B
HALT

高级程序设计语言接近于人类自然语言的语法习惯,与计算机硬件无关,易被用户掌握和使用

目前广泛应用的高级语言有多种,如 BASIC、 FORTRAN、PASCAL、C、C++、JAVA 等等

高级语言又分为面向过程和面向对象

汇编特点

优点 缺点
汇编语言与处理器密切相关 编写汇编语言源程序比编写高级语言源程序烦 琐
汇编语言程序效率高 调试汇编语言程序比调试高级语言程序困难
  • 高级语言适合于数值计算、数据处理等软件的开发
  • 汇编语言则适合于对时间(执行速度)和空间 (程序长度)要求特别高的情况
  • 在很多工程应用中,经常将这两种语言结合起来 使用。例如,Linux 操作系统的大部分代码使用 C 语言开发的,而与 CPU 及机器硬件密切相关的代 码就需要用汇编语言编写

字节、字、字长

位:计算机能处理的最小单元,它不随 CPU 的处理能力的变化而变化

字节:就是 8 位数据的大小,它也不随 CPU 的处理能力的变化而变化

字:计算机进行数据处理时,一次存取、加工和传送的数据长度称为字(word),一个字通常是由 16 位组成,也就是双字节

字长:计算机的每个字所包含的的位数称为字长。一般我们说的 8 位机、16 位机、32 位机、64 位机指的就是计算机的字长

针对以上概念,有如下结论:

  • 在 16 位CPU中,一个字长表示 16 位(两个字节)
  • 在 32 位CPU中,一个字长表示 32 位(四个字节双字)
  • 在 64 位CPU中,一个字长表示 64 位(八个字节四字)

以此类推下去…

堆和栈

  • 计算机的堆区地址从低到高向上增长
  • 计算机的栈区地址从高到低向下增长
  • 堆栈就是线性栈,也就是栈的一种

计算机执行程序时都默认使用的是栈区

进位计数

进制转换

常用用到二进制(B)、八进制(O)、十进制(D)、十六进制(H)

进制可以借助进制转换器但是需要会原理

十进制转换二进制

二进制转换十进制

同理其他进制转换为十进制也是用这个方法,区别在于底数的不同

二进制转换十六进制

他们之间成倍数关系,相当于四位二进制的数表示一位十六进制的数,可以进一步简化运算得出结果

这个方法很重要,后面程序设计中进制转行使用就是这个方法

计算机中的数和字符的表示

原码

最高位表示符号(正数用 0 ,负数用 1),其他位表示数值位,称为有符号数的原码表示法

1
2
X=45=00101101B [X]原=00101101B
Y=-45 [Y]原=10101101B

反码

正数的反码与原码完全相同,符号位用 0 表示数值位值不变

负数的反码符号位用 1 表示, 数值位为原码数值位按位取反形成,即 0 变 1、1 变 0

负数的反码相当于负数的原码符号位不变,其他位取反

1
2
3
4
X=45=00101101B 	[X]原=00101101B
[X]反=00101101B
Y=-45 [Y]原=10101101B
[Y]反=11010010B

补码

本质:对一个二进制数按位求反、末位加一

正数的补码与原码完全相同,符号位用 0 表示数值位值不变

负数的补码为反码加 1 形成

1
2
3
4
5
6
X=45=00101101B 	[X]原=00101101B
[X]反=00101101B
[X]补=00101101B
Y=-45 [Y]原=10101101B
[Y]反=11010010B
[Y]补=11010011B

计算机计算方法

计算机只有加法器,而减法可转换为补码加法 ,所以计算机统一用补码进行计算

1
2
加法规则:[X+Y]补码 = [X]补码 + [Y]补码
减法规则:[X-Y]补码 = [X]补码 + [-Y]补码

80x86 计算机组织

80x86 微处理器

硬件组成

软件组成

中央处理机CPU

  • 执行存放在存储器里的指令

  • 完成算术逻辑运算

  • 完成在CPU和存储器以及I/O之间的数据传送

  • 由运算器、控制器和寄存器组成

运算器

  • 运算器执行算术运算和逻辑运算
  • 使用寄存器来保存等待处理的数据
  • 在运算中,结果暂时存放在累加器中

控制器

  • 控制器顺序从 RAM 中取出指令,并将他们放到指令寄存器中
  • 控制器翻译指令,并根据翻译结果发送信号给数据总线从 RAM 中取数据,发送信号到运算器 进行处理

总线组成

  • 地址总线:决定最大寻址空间
  • 数据总线:决定机器字长
  • 控制总线:发送控制信号

指令

指令是对计算机进行程序控制的最小单位 所有的指令的集合称为计算机的指令系统

指令周期:计算执行一条指令所需要的时间

80x86 寄存器组详讲

通用寄存器

数据寄存器

数据寄存器共有 4 个寄存器 AX、BX、CX、DX,用来保存操作数或运算结果等信息

数据寄存器 作用
AX 使用频度最高,用于算术运算及与外设传送信息等
BX 常用于存放存储器地址
CX 一般作为循环或串操作等指令中的隐含计数器
DX 常用来存放双字数据的高 16 位;存放外设端口地址

变址指针寄存器

变址和指针寄存器包括 SI、DI、SP、BP 这四个 16 位寄存器,主要用于存放某个存储单元的偏移地址

数据寄存器 作用
SI 和 DI-源变址寄存器和目的变址寄存器 在字符串操作中,SI 和 DI 都具有自动增量或减量的功能,分别与 DS 和 ES 联用
SP-堆栈指针寄存器 用于存放当前堆栈段中栈顶的偏移地址,与 SS 联用进行堆栈寻址
BP-基址指针寄存器 用于存放堆栈段中某一存储单元的偏移地址(基地址),同样与 SS 联用进行堆栈寻址

说明

  • 上述寄存器都是以 16 位形式访问,但是前四个还分为 HIGH 和 LOW 可以进行 8 位访问,但是后四位最低也是 16 位
  • 80386 以及后续的 32 位机还有 E-寄存器 协助帮忙以 32 位访问

专用寄存器

专用寄存器 作用
IP-指令指针寄存器 保存下一次将要从主存中取出指令的偏移地址, 与代码段寄存器 CS 联用确定下一条指令的物理地址。目标程序运行时,IP 的内容由微处理器硬件自动设置,程序不能直接访问IP,但一些指令却可改变 IP 的值,如转移指令、子程序调用指令等
FLAGS-标志寄存器 记录程序中运行结果的状态信息, 根据有关指令的运行结果由 CPU 自动设置,这些状态信息往往作为后续条件转移指令的控制条件

标识符

了解几个 80x86 中标志寄存器的内容,图中未标识的位置暂时不讲

标志符 作用(1/0)
OF(overflow flag)-溢出标志 操作数超出了机器能表示的范围(是/否)
SF(sign flag)-符号标志 记录运算结果的符号(负/正)
ZF(zero flag)-零标志 运算结果为 0(是/否)
CF(carry flag)-进位标志 记录运算时是否1从最高有效位产生进位值(是/否)
DF(direction flag)-方向标志 在串处理指令中控制处理信息的方向(减量 STD /增量 CLD)

说明

  • 上述寄存器都是以 16 位形式访问
  • 同样 80386 以及后续的 32 位机还有 E-寄存器 协助帮忙以 32 位访问

段寄存器

存储器

存储单元的地址

  • 在存储器里以字节为单位存储信息
  • 每一个字节单元给一个唯一的存储器地址称为物理地址
  • 地址从 0 开始编号,顺序加 1。用二进制数表示,是无符号整数,书写格式为十六进制

地址总线和寻址空间

  • 地址总线宽度指专用于传送地址的总线数目,根据这一数值可以确定处理机可以访问的存储器的最大范围,即寻址空间

  • 如 8086/8088 的地址总线宽度为20位,则可以表示 2 20=1048576 个存储单元,即寻址空间为 1M 其地址编号的范围为 00000H ~ 0FFFFFH

    汇编语言中以字母开头的 16 进制数前面需要加 0,否则编译器是不能认出这是个 16 进制数的

存储单元的内容

  • 一个存储单元中存放的信息称为该存储单元的内容
  • 地址为 0004H 的字节的存储 单元中的内容是78H,表示 为 (0004)= 78H

注意这个写法,() 里填的是有效地址,= 后面给出的是其地址对应的内容,如果用 X 表示某存储单元的地址,则 X 单元的内容可表示为 (X)

假如 X 单元中存放着 Y,而 Y 实际上又是另一个地址,那么我们可以用 (Y)=((X)) 来表示 Y 单元的内容(套娃行为)

数据存放方式

  • 一个字存入存储器要占有相继的两个字节,存放时低位字节存入低地址,高位字节存入高地址
  • 字单元的地址采用它的低地址表示,可表示为 (0004)=5678H
  • 0004H 的双字单元的内容:(0004)=12345678H

实模式存储器寻址方式

存储器地址的分段

问题:在 8086CPU 中地址总线的长度是 20 位(即最大寻址空间是 220=1MB),其物理地址从 00000H~FFFFFH。而 8086 CPU 的寄存器都是 16 (即最大寻址空间为 216,地址编号从 0000H~FFFFH)位的那么这传入的 20 位地址怎么用 16 位的寄存器表达呢?

解决:将 1M 的字节地址划分为若干个段,从 0 地址开始每 16 个字节划为一个小段,段的起始地址成为段首址(肯定是 16 的倍数)

上述将 1M 物理地址编号号并分段,每个十六进制数代表四个二进制数

特点:这样可以划分出 220/216=24=16 个段,每个段的起始地址的最低一位都是 0 (即 20 为的地址的低四位为 0)

物理地址的组成

那么根据上面的分法,由于段地址的起始地址最低位为 0 的特点,我们可以只取段地址的起始地址的高 16 位(即舍弃最低位 0)作为段地址,然后我们再记录一个偏移地址指在段内相对段起始地址的偏移值,那么 20 为的物理地址就可以由 16 位的段地址和 16 为偏移地址组成

物理地址的计算方法

段地址左移 4 位(相当于 *16d)再加上偏移地址就形成了物理地址,在指明某个内存单元的时候就是 段地址:偏移地址 形式

3017:000A 的表示的物理地址就是:3017AH

易混淆的概念

  1. 物理地址和逻辑地址是同一个东西,指唯一的 20 位的地址
  2. 有效地址和偏移地址是同一个东西,指的是相对段地址不唯一的 16 位的地址

段寄存器

分类

  • 代码段:存放当前正在运行的程序指令
  • 数据段:存放当前运行程序所用到的数据
  • 堆栈段:定义的堆栈所造的区域
  • 附加段:附加的数据段,是一个辅助的数据段

概念

  • 除非专门的指定,一般情况下各段在存储区中的分配是由操作系统默认负责的
  • 段寄存器用来确认该段在内存单元中的起始位置
  • 程序员在编写程序时应该按照规定程序的各部分放在指定的段区之间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; ================================================================
data segment
message db 'hello$'
data ends
; ================================================================
code segment
assume cs:code,ds:data
start:
; 数据段段地址传给ds
mov ax,data
mov ds,ax
...
code ends
; ================================================================
end start

段寄存器和相应偏移地址寄存器组合表

段寄存器 偏移地址寄存器 缺省选择规则
CS IP 用于取指令
SS SP 和 BP 所有的堆栈进出
DS BX、DI、SI 或者一个 16 位直接数 除相对于堆栈以及串处理指令的目的串以外的所有数据访问
ES DI 串处理指令的目的串

段跨越

80x86 允许程序员用段跨越前缀改变系统所指定的默认段,如允许数据存放在除 DS 段以外的其他段中

1
2
3
4
; 段跨越前缀格式
; 段寄存器:偏移地址
mov ax,[value]
mov ax,es:[value]

注意

  • 串处理指令的目的串必须在 ES 段
  • PUSH 指令的目的和 POP 指令的源必须用 SS 段
  • 指令必须存放在 CS 段

外部设备

主机与外部设备(CPU 和存储器)的通信是通过外设接口进行的,每个接口由一组寄存器组成:

  1. 数据寄存器:存放要在外设和主机间传送的数据
  2. 状态寄存器:保存外部设备或接口的状态信息
  3. 命令寄存器:CPU 给外设或接口的控制命令通过此寄存器送给外部设备

端口地址:主机中的每个寄存器给予一个端口地址,组成了一个独立于内存储器的 I/O 地址空间

主机与外设交换信息是通过 IN、OUT 指令来完成的

80x86 的指令系统和寻址方式

80x86 的寻址方式

指令格式中操作数到底是去取具体数还是存放位置(偏移地址)要根据操作码的用途决定。如果需要的是具体数,那么会将根据寻址方式确定立即数作为操作数,或者到存储器中物理地址找到具体数作为操作数;如果需要的是存放位置那么会将寻址方式确定的立即数作为存放位置或者就是将存储器寻址确定的有效地址作为存放位置

与数据有关的寻址方式

立即数寻址方式

操作数直接放在指令中,紧跟在操作码之后作为指令的一部分存放在代码段中

1
2
mov	al,5		;将5d传给al
mov ax,3064H ;将3064H传给ax

寄存器寻址方式

将操作数给为存放在寄存器里,指令指定寄存器

1
mov	ax,bx

这种寻址方式由于操作数就在寄存器中不需要访问存储器所以运算速度较快

存储器寻址

有效地址

后面的五种寻址方式不同于上述两种,他们都要去存储器中寻找操作数的确切地址,这里涉及到一个操作数的有效地址的概念

有效地址的四种成分:

  1. 位移量:存放在指令中的一个8位、16位或32位数(386及后继机型),但不是立即数而是一个地址
  2. 基址:存放在基址寄存器中,通常用来指向数据段中数组或字符串的首地址(用于基址寄存器的是 BX 和 BP)
  3. 变址:存放在变址寄存器中,通常用来访问数组中的某个元素或字符串中的某个字符(用于变址寄存器的是 SI 和 DI)
  4. 比例因子(386及后继机型):其值为1,2,4, 8,在寻址中可用变址寄存器的内容乘以比例因子来取得变址值。对访问元素长度为2,4,8字节的数组特别有用

有效地址的计算式:EA = 基址 + (变址 * 比例因子) + 位移量

  • 操作数的有效地址只包含一种成分:

    • 直接寻址方式
    • 寄存器间接寻址方式
  • 操作数有效地址包含两种成分:

    • 直接变址寻址方式
    • 基址变址寻址方式
  • 相对基址变址寻址方式

  • 包含比例因子的寻址方式(这里不作讲解)

因为存储器寻址串的是有效地址信息,所以除了字符变量外其他的寄存器都应该加上 [] 就相当于指针的去引用 * 表示将其存储的内容作为传入的有效地址

1
2
add ax,bx  		;将bx本身存储的内容作为立即数和ax相加
add ax,[bx] ;将bx本身的内容作为一个有效地址,将这个指向的有效地址的内容和ax相加
直接寻址方式

操作数的有效地址只包含位移量一种成分,其值存放在代码段中指令的操作码之后,位移量的值就是操作数的有效地址

用途:直接寻址方式适用于处理单个变量

1
mov	ax,[2000H]

根据默认的段寄存器和偏移地址寄存器搭配关系 16 位的直接数和 DS 搭配,若此时 (DS)=3000H,那么执行结果 (ax)=3050H

在汇编语言中可以使用符号地址代替数值地址。即用变量名和变量存储的值是等效的,他们都指的是当前变量自己所处的有效地址

1
2
mov	ax,value
mov ax,[value]

中括号可以看做相当于 C++ 中的取地址符 &,它指向中括号内容指向的偏移地址。这里 value 就是存储位移量的变量名

直接寻址可以使用默认的搭配段地址寄存器和偏移地址寄存器,也可以使用段跨越前缀

1
mov	ax,es:[2000H]
寄存器间接寻址

寄存器间接寻址方式操作数的有效地址只包含基址寄存器或者变址寄存器之一,也就是有效地址改为放在寄存器中,该寄存器的内容就做为偏移地址

用途:寄存器间接寻址方式适用于表格处理(一维存储方式),执行完一条指令后只需要修改寄存器的内容就可以去除表格的下一项

1
2
ds=2000H bx=1000H
mov ax,[bx]

ax 的物理地址 = 20000 + 1000 = 21000H

1
2
(AX)=0 	(BP)=0030H 	(DS)=3000H 	(SS)=2000H	(20030H)=1234H 	(30030H)=5678H
mov ax,[bp]
1
2
3
4
5
6
7
8
        lea si, array   ; 数组首地址送si
mov cx, length ; 数组长度送cx
mov ax, 0
AA:
add ax, [si]
add si, 2
dec cx
jnz AA
直接变址寻址方式(寄存器相对寻址方式)

操作数的有效地址为基址寄存器或变址寄存器的内容与指令中的位移量之和

用途:直接变址寻址方式适用于处理一位数组或者字符串

1
2
3
ds=3000H        si=2000H        count=3000H
mov ax,count[si] 或
mov ax,[count+si]
基址变址寻址方式

操作数的有效地址是一个基址寄存器和一个变址寄存器的内容之和,就是位移量改为存在变址寄存器中

用途:基址变址寻址方式同样适用于处理一位数组或者字符串

1
2
3
ds=2100H	bx=0158H	di=10a5H
mov ax,[bx][di] 或
mov ax,[bx+di]
相对基址变址寄存器

操作数的有效地址是一个基址寄存器和一个变址寄存器的内容与指令中指定的位移量之和

用途:相对基址变址寄存器适二维数组

这不难理解,有效地址又加上了一种组成成分维度肯定上升啊

1
2
ds=3000H	bx=2000H	si=1000H	mask=0250H
mov ax,mask[bx][si]

与转移地址有关的寻址方式

这种寻址方式用来确定转移指令和 CALL 指令的转移地址,都借助的是 JMP 指令实现寻址的,在汇编程序设计中调用子程序有用到,后面控制转移指令的无条件转移指令有详细讲解

  • 段内直接寻址
  • 段内间接寻址
  • 段间直接寻址
  • 段间间接寻址

80x86 的指令系统

数据传送指令

数据传送指令负责把数据、地址或立即数传送到寄存器或存储单元中,它又可分为下述五种数据传送指令

通用数据传送指令

mov 传送指令
mov 普通传送指令
  • 格式:mov dst,src
  • 执行操作:将源操作数的内容传给目的操作数

选择题在这里占很大的分数,一定要把图熟记

  • 说明:mov 指令有 7 中使用格式,总而言之不允许在两个存储单元之间直接传送数据,不允许两个段寄存器将直接传送信息(除源操作数为立即数的情况下,两个操作数中必定有一个是寄存器
  • 值的注意的是当用立即数向存储器传值,或者存储器与寄存器之间传值的操作数不匹配的时候,指令是需要指定操作数类型属性才能成功执行的
movsx 带符号扩展传送指令(适用于带符号数)
  • 格式:mov dst,src

  • 执行操作:源操作数可以是 8 位或者 16 位的寄存器或者存储单元,而目的操作数必须是 16 位或者 32 位的寄存器,传送时把源操作数符号扩展送进目的操作数

  • 说明:movsx 只有两种使用格式,并且影响标志位

    • mov reg1,reg2
    • mov reg,mem
  • 示例:movsx ax,cl

    • 若是 cl1001 0011 那么 ax 就是 1111 1111 1001 0011

    • 若是 cl0001 0011 那么 ax 就是 0000 0000 1001 0011

      扩展的符号根据 src 的最高符号位决定

movzx 带零扩展传送指令(适用于无符号数)

和上述的 movsx 基本类似,差别就是 movsx 的源操作数是带符号数所以扩展的时候根据符号扩展但是 movzx 适用的源操作数是无符号数所以就用 0 扩展

mov 指令都不影响操作数,movsxmovzx 与一般的双操作数指令的差别是一般双操作数的 dstsrc 长度是一致的,但是这两个指令由于扩展的原因 src 的长度一定是小于 dst

堆栈操作指令

本组指令都不影响标志位

堆栈
  • 堆栈是“后进先出”工作方式的一个存储区,必须存在于堆栈段中,段地址存放在 SS
  • 栈顶元素放入低地址,栈底元素放入高地址
  • 堆栈只有一个出入口,所以只有一个堆栈指针寄存器,堆栈地址长度为 16 或者 32 是分别对应 SPESP
  • SP 或者 ESP 的内容在任何时候都是执行当前栈顶的,所有的进栈与出栈指令都是根据 SP 或者 ESP 的内容来确定进栈或者出栈的内存单元,而且每次操作后必须及时修改指针来保证他们仍然指向栈顶
push 进栈指令
  • 格式:push src,隐含目的操作数 SS:SP

  • 执行操作:先是栈顶指针移动到需要压入 src 后的首地址(栈顶)然后压入 src

    • 16 位操作数

      1
      2
      (SP)<-(SP)-2
      ((SP)+1,(SP))<-(SRC)
    • 32 位操作数

      1
      2
      (ESP)<-(ESP)-4
      ((ESP+3),(ESP+2),(ESP+1),(ESP))<-(SRC)
  • 说明:有四种格式 push reg/mem/data/segreg

  • 功能:将寄存器、段寄存器或者存储器中的数据或立即数压入堆栈

  • 示例:

    1
    2
    mov ax,2107H
    push ax
pop 出栈指令
  • 格式:pop dst,隐含源操作数 SS:SP

  • 执行操作:先弹出 src,然后将栈顶指针移动到需要弹出 src 后的首地址(栈顶)

    • 16 位操作数

      1
      2
      (DST)<-((SP)+1,(SP))
      (SP)<-(SP)+2
    • 32 位操作数

      1
      2
      (DST)<-((ESP+3),(ESP+2),(ESP+1),(ESP))
      (ESP)<-(ESP)+4
  • 说明:有四种格式 pop reg/mem/data/segreg

  • 功能:将寄存器、段寄存器或者存储器中的数据或立即数弹出堆栈

  • 示例:

    1
    pop ax

这两个指令对应着记不容易忘掉,同时他们也是不影响标识符的

作用

用到堆栈的可能一般是为了保存某个数:比如内外两层循环时进入内层循环的时候要将外层循环的 cx 保存为了后面使用,或者从主程序调用到子程序的时候某个寄存器的值还需要保存下来,那么我们就可以将他们压入栈内并在需要的时候再弹出

1
2
3
4
5
6
7
push ax
push bx
push cx
...
pop cx ;一定要遵守先进后出的规则
pop bx
pop ax
pusha/pushad 所有寄存器进栈指令

格式:pusha pushad

执行操作:

  • pusha 将 16 位通用寄存器按照 ax,cx,dx,bx 指令执行前的 sp,bp,si,di 顺序进栈,指令执行后再 (SP)<-(SP)-16
  • pushad 将 32 位通用寄存器按照 eax,ecx,edx,ebx 指令执行前的 esp,ebp,esi,edi 顺序进栈,指令执行后再 (ESP)<-(ESP)-32

pusha/pushadpopa/popad 与其他堆栈操作不同在于是最后再一次执行完 sp/esp 的移位操作

另外 pusha/pushad 和其他的进栈指令不同,它是将所有寄存器进栈再更改 sp/esp 的移位操作

popa/popad 所有寄存器出栈指令

格式:popa popad

执行操作:

  • popa/popad 将 16 位通用寄存器按照 di,si,bp,sp,bx,dx,cx,ax 顺序出栈然后再将 (SP)<-(SP)+16
  • popa/popad 将 32 位通用寄存器按照 edi,esi,ebp,esp,ebx,edx,ecx,eax 顺序出栈然再后 (ESP)<-(ESP)+32
xchg 交换指令
  • 格式:schg opr1,opr2

  • 执行操作:opr1opr2 可以是寄存器与寄存器之间或者寄存器与存储单元之间,但是不允许使用段寄存器与操作数,将两者交换内容

  • 示例

    1
    2
    3
    4
    bx=6F30H	bp=0200H	si=0046H	ss=2F00H	2F246H=4154H
    xchg bx,[bp+si]
    ;opr2的物理地址:2F000+0200+0046=2F246H
    ;指令执行后:(BX)= 4154H (2F246H)= 6F30

累加器专用传送指令

地址传送指令

这一组指令都是为了完成将地址传送到指定寄存器的功能

lea 有效地址送寄存器

本组指令都不影响标志位

  • 格式:lea reg,src

  • 执行操作:(reg)<-src 将指定的 src 的有效地址传给 reg

  • 说明:

    • 目的操作数针对的是寄存器,所以只能是 16 位或者 32 位的寄存器
    • 源操作数可以是除了寄存器和立即数之外的任意一种存储器寻址方式
  • 示例

    1
    2
    3
    (BX) = 0400H	(SI) = 003CH	(DS) = 2000H	(2139E) = 2195H
    lea bx,[bx+si+0F62H]
    ;执行指令后 bx=0400+003C+0F62=139EH

    传给的是有效地址,不要受段寄存器的影响算成了物理地址

在这里 bx 寄存器得到的是有效地址而不是存储单元的内容,如果指令是

mov bx,[bx+si+0F62H] 那么 bx 得到的就是偏移地址为 139EH 单元的内容而不是偏移地址,这也进一步说明使用的指令决定了用的是操作数内容还是操作数的地址

如果想用 mov 实现和 lea 一样的效果,可以借助 offset 回送标号或者变量的偏移地址:mov bx,offset list 等效于 lea bx,list

lds les lfs lgs lss 指针送到寄存器和段寄存器的指令

lds 指令为例,其他的指令只不过是寄存器不同而已

  • 格式:lds reg,src

  • 执行操作:

    1
    2
    (reg)<-(src)
    (ds)<-(src+2)
  • 说明:

    • 源操作数同样只能使用存储器寻址方式
    • 当指令中指定的是 16 位寄存器时把该指令中存放的16位数内容作为偏移地址装入寄存器,然后把 (SRC+2) 装入 DS 段寄存器

    注意区分和 lea 的区别,lea 是将源操作数自身的有效地址传给目的操作数,而这些指令是将源操作数自身的内容作为有效地址传给源操作数并将源操作数的下一个位置的内容作为有效地址传给段寄存器

  • 示例

    1
    2
    3
    4
    5
    (DS)=B000H	(BX)=080AH	(0B080AH)=05AEH	(0B080CH)=4000H
    les di,[bx]
    ;执行指令后 (DI)=05AEH, (ES)=4000H
    lea di,[bx]
    ;执行指令后 (DI)=080AH
指令对比

标志寄存器传送指令

标志寄存器传送指令都是无操作数指令,隐含对标志寄存器的操作

lafh 标志送 AH
  • 格式:lafh

  • 执行操作:将 flags 标志寄存器的低字节(8位)传给 ah 寄存器

  • 示例

    1
    2
    3
    (FLAGS)=0485H	(AX)=0FFFFH
    ;执行指令后
    (AX)=88FFH
sahf AH送标志寄存器
  • 格式:sahf
  • 执行操作:将 ah 寄存器传给 flags 标志寄存器的低字节(8位)
pushf/pushfd 标志进栈
  • 格式:pushf pushfd

  • 执行操作:

    • pushf 用于 16 位标志寄存器

      1
      2
      (SP)<-(SP)-2
      ((SP)+1,(SP))<-(FLAGS)
    • pushfd 用于 32 位标志寄存器

      1
      2
      (ESP)<-(ESP)-4
      ((ESP)+3,(ESP)+2,(ESP)+1,(ESP))<-(EFLAGS AND 0FCFFFFH)

      后边的并运算主要是清除 VM 和 RF 位,了解即可

popf/popfd 标志出栈
  • 格式:popf popfd

  • 执行操作:

    • popf 用于 16 位标志寄存器

      1
      2
      (FLAGS)<-((SP)+1,(SP))
      (SP)<-(SP+2)
    • popfd 用于 32 位标志寄存器

      1
      2
      (EFLAGS)<-((ESP)+3,(ESP)+2,(ESP)+1,(ESP))
      (ESP)<-(ESP)+4

lafhpishf/pushfd 不影响标志位,safhpopf/popfh 根据装入的值来确定标志位的值

类型转换指令

类型转换指令也是无操作数指令,隐含着对事先存储到寄存器的操作

注意:该指令用于扩展有符号数

格式都是直接调用指令名字,操作数需要事先放在指定的寄存器中

cbw 字节转换为字
  • 转换类型:db->dw
  • 执行操作:(ax)<-(al) 符号扩展
    • 若 (AL) 的最高有效位为0,则 (AH)= 00H
    • 若 (AL) 的最高有效位为1,则 (AH)= 0FFH
cwd/cwde 字转换为双字
  • 转换类型:dw->dd
  • 执行操作:(dx:ax)<-ax 符号扩展
    • 若 (AX) 的最高有效位为0,则 (DX)= 0000H
    • 若 (AX) 的最高有效位为1,则 (DX)= 0FFFFH
cdq 双字转换为 4 字
  • 转换类型:dd->dq
  • 执行操作:(edx:eax)<-eax
    • 若 (EAX) 的最高有效位为0,则 (EDX)= 00000000H
    • 若 (EAX) 的最高有效位为1,则 (EDX)= 0FFFFFFFFH

算术指令

80x86 算术运算包括二进制和十进制运算指令。算术指令用来执行算术运算,他们中有双操作数指令也有单操作数指令。

其中双操作数指令的两个操作数中除源操作数作为立即数的情况外,必须有一个操作数在寄存器中

单操作数指令不允许使用立即数方式

加法指令

以下三条指令都可以作为字或者字节运算,只要保证两个操作数的字长相同即可

标志位影响
  • ZF 和 SF 位设置比较简单不做详讲

  • CF

    • 执行加法指令时,CF 位根据最高有效位是否有进位设置,有进位时 CF=1,反之 CF=0
    • 表示无符号数的溢出与否:在双字节长数运算中利用 CF 位的值把低位字的进位计入高位字中
  • OF

    • 若两个操作数的符号相同,而结果的符号与之相反时 OF=1,反之 OF=0
    • 表示有符号数的溢出
  • 也就是说加法指令没有有无符号数之分,它会分别设置两个操作位,需要程序员自己明白操作数是否有符号并根据 CF 或者 OF 位做出决策

add 加法指令
  • 格式:add dst,src

  • 执行操作:(dst)<-(dst)+(src) ,将源操作数和目的操作数相加并将结果放入目的操作数

  • 示例

adc 带进位加法
  • 格式:adc dst,src

  • 执行操作:(dst)<-(dst)+(src)+cf,不仅将源操作数和目的操作数相加,而且将前面指令设置的 CF 位加入结果(相当于进位 +1)

  • 示例:一般处理双子节数的高字节运算的时候用的到:设目的操作数存放在 (dx:ax) 中,源操作数存放在 (bx:cx)

这两对寄存器组合常用需要记住,双子字节的存储与处理时常涉及

在 80386 及后续机型中可以直接借助 32 位寄存器解决此问题

inc 加 1
  • 格式:inc opr

  • 执行操作:(opr)<-(opr+1)

  • 说明:

    • 不影响 CF 标志位(硬件设计造成的原因,intel 规定的不必计较)

    • 常用于对计数器进行 +1 操作

      1
      2
      3
      4
      ; 模拟循环
      mov cx,1
      ... ;循环指令
      inc cx

减法指令

可以和加法指令类比,原理是一样的

标志位影响
  • CF

    • 若减数 > 被减数此时有借位则 CF=1,否则 CF=0
    • 表示无符号数的溢出与否,被减数的最高位有效位向高位的借位
  • OF

    • 若是两个操作数的符号相反而结果的符号与减数相同那么 OF=1,否则 OF=0
    • 表示有符号数的溢出与否
  • 也就是说减法指令也没有有无符号数之分,它也会分别设置两个操作位,需要程序员自己明白操作数是否有符号并根据 CF 或者 OF 位做出决策

sub 减法
  • 格式:sub dst,src
  • 执行操作:(dst)<-(dst)-(src)
  • 示例
sbb 带借位减法
  • 格式:sbb dst,src

  • 执行操作:(dst)<-(dst)-(src)-cf

  • 示例

dec 减 1
  • 格式:dec opr

  • 执行操作:(opr)<-(opr)-1

  • 说明:

    • 不影响标志位

    • 一般用于对计数器和地址指针的调整

neg 求补运算

  • 格式:neg opr

  • 操作数:(opr)<- -(opr) 或者 (opr)<-0FFFFH-(opr)+1

  • 说明:操作数按位求反再 +1

cmp 比较
  • 格式:cmp opr1,opr2

  • 执行的操作:(opr1)-(opr2)

  • 说明:可以发现执行的也是减法运算但是并没有保存结果,只是根据结果设置条件标志位。cmp 指令后面往往会跟着一条条转移指令,根据比较结果产生不同的程序分支

  • 示例

    1
    2
    3
    4
    5
    6
        cmp al,50	;(al)-50
    jb below ;(al)<50
    sub al,50 ;(al)>=50
    inc ah ;(ah)+1->ah
    below:
    ...

乘法指令

乘法和除法指令可以与加法和减法指令对比着记,区别是乘法和除法指令对于分为有无符号数两种运算指令,相同点是乘除法也设置 CF 和 OF,但是对于以外的条件码位无定义

无定义和不影响截然不同,不影响是其他条件码保持原状态不变但是无意义是执行这些指令后条件码位的状态不定

mul 无符号数乘法
  • 格式:mul src

  • 执行操作:因为乘法可能会出现更长的数,所以结果需要一个比操作数更长的寄存器存放(二倍)

    • 字节操作数:(ax)<-(al)*(src)
    • 字操作数:(dx:ax)<-(ax)*(src)
    • 双字操作数:(edx:eax)<-(eax)*(src)
  • 说明:

    • 目的操作数必须用累加器,所以要事先将目的操作数存入累加器,两个 8、16、32 位数相乘分别得到 16、32、64 位乘积结果

      如今的机器已经有专门用于乘法的累乘器,但是早先就是借助累加器完成的乘法

    • 源操作数可以用除立即数以外任一一种寻址方式

    • 标志位:如果乘积的高一半为 0,则 CFOF 为 0,否则为 1。用来检查相乘的结果是字节、字、还是 4 字

  • 示例

imul 有符号操作数
  • 格式:imul src
  • 执行操作:和上述的 mul 相同
  • 标志位:如果乘积的高一半是低一半的符号扩展,则 CFOF 为0,否则为 1

值的注意的是在乘法指令中无符号数的乘法和有符号数的单操作数的乘法指令不支持源操作数使用立即数

  • 示例

imul 多操作数乘法

对于 80286 及其后续机型,imul 除上述的单操作数指令(累加器是隐含的),还增加了双操作数和三操作数

imul 双操作数乘法
  • 格式:imul reg,src
  • 执行操作:
    • 字操作数:(reg16)<-(reg16)*(src)
    • 双字操作数:(reg32)<-(reg32)*(src)
  • 说明:
    • 目的操作数必须是 16 位或者 32 位寄存器
    • 源操作数可以用任一一种寻址方式取得和目的操作数长度相同的有符号数或者 8、16、32 位的立即数,若立即数长度不够的则自动进行符号扩展
imul 三操作数乘法
  • 格式:imul reg,src,imm
  • 执行操作:不是三个数相乘,而是
    • 字操作数:(reg16)<-(src)*imm
    • 双字操作数:(reg32)<-(src)*imm
  • 说明:
    • 目的操作数必须是 16 位或者 32 位寄存器,源操作数和目的操作数长度相同
    • imm 表示立即数,可以是8、16、32 位,但是其长度也必须和目的操作数相同,若为长度不够的立即数则自动进行符号扩展
imul 多操作数和 imul 单操作数的区别

imul 单操作数的乘积字长是操作数的二倍,因此即使 OF 位为 1 结果也是对的不存在溢出问题,但是 imul 多操作数乘积的字长和操作数相同,这样就可能会产生溢出问题,此时运算结果就是错的了。所以对于 imul 多操作数当乘积的没有溢出那么 OFCF 位为 0,否则为 1,其他条件码位仍然无定义

除法指令

div 无符号数除法和 idiv 有符号数除法
  • 格式:div src idiv src

  • 执行操作:和乘法相反余数和商的长度减半,分别存在高位和低位

    • 字节操作数

      1
      2
      (al)<-(ax)/(src)的商
      (ah)<-(ax)/(src)的余数
    • 字操作数

      1
      2
      (ax)<-(dx:ax)/(src)的商
      (dx)<-(dx:ax)/(src)的余数
    • 双字操作数

      1
      2
      (eax)<-(edx:eax)/(src)的商
      (edx)<-(edx:eax)/(src)的余数
  • 说明:

    • 除数、商、余数分别为 8、16、32 位时,被除数则为 16、32、64位
    • 目的操作数(被除数)必须存放在 AX 或 (DX:AX) 或 (EDX:EAX) 中,源操作数可以用除立即数以外的任一种寻址方式

    离谱的是在除法指令中有无符号数的指令都不支持源操作数使用立即数了

    • DIV指令中商和余数均为无符号数,IDIV指令中商和余数均为带符号数,且余数的符号和被除数的符号相同

    • 除法指令对所有条件码均无定义

    好理解因为不涉及溢出问题了进而连 CFOF 都不用着了

  • 示例

逻辑指令

逻辑运算指令

下列 5 个指令中 not 不允许使用立即数,其他 4 条指令除非源操作数为立即数,至少有一个操作数必须存放在寄存器中

标志位影响

下列 5 个指令中 not 指令不影响标志位,其它 4 种指令使 CF=0,OF=0 , SF ZFPF 根据结果设置,AF 无定义

and 逻辑与
  • 格式:and dst,src

  • 执行操作:(dst)<-(dst)∧(src)

  • 功能:屏蔽某些位(将指定位置设为 0)

  • 示例:屏蔽 0BF 的 0、1两位,其他位保持不变

or 逻辑或
  • 格式:or dst,src

  • 执行操作:(dst)<-(dst)∨(src)

  • 功能:将某些位置设置为 1

  • 示例:将数 43H 的第 5 位置设为 1,其他位保持不变

not 逻辑非
  • 格式:not opr
  • 执行操作:(opr)<-(opr)按位取反
  • 示例:将 8BH 按位取反
xor 异或
  • 格式:xor dst,src

  • 操作:(dst)<-(dst)∀(src)

  • 功能:某些位按位取反

  • 示例:让数 11H 低0、1位取反,其他位保持不变

test 测试
  • 格式:test opr1,opr2

  • 执行操作:(opr1)∧(opr2)

  • 功能:测试某些位是否为 0

  • 说明:也是将两个操作数进行逻辑与运算,但是不将结果保存而是设置 ZF 位,如果所有测试位全为 0 那么 ZF=1,否则 ZF=0

  • 示例:测试数 40H 的第0、1、2、3、5、7 位是否为0

结果是 SF=0,ZF=1,说明所需的测试位均为 0

移位指令

shl 逻辑左移
  • 格式:shl opr,cnt

  • 执行操作:将 (opr) 向左移动 cnt 指定的次数,空出的低位补入相应个数的 0,CF 的内容为最后移入位的值

  • 说明:cnt 在 8086 中可以是 1 或者 cl 寄存器指定。cnt 为 1 的时候只需要移动一位,如果移动次数大于 1,则可以在该移位指令按把移位次数置于 cl 寄存器中,而移位指令中的 cnt 写为 cl 即可。在其他机型可以使用 clcnt,且 cnt 的值除可使用 1 外,还可以使用 8 位的立即数指定范围从 1 到 31 的移位次数

  • 示例:无符号数的与 2 指数的乘法

shr 逻辑右移
  • 格式:shr opr,cnt

  • 执行操作:将 (opr) 向右移动 cnt 指定的次数,空出的高位补入相应个数的0,CF 的内容为最后移入位的值

  • 示例:无符号数与 2 指数的除法

sal 算术左移
  • 格式:sal opr,cnt

  • 执行操作:不难看出算术左移和逻辑左移的效果是一样的

  • 示例:有符号数与 2 指数的乘法

    huibian-48
sar 算术右移
  • 格式:sar opr,cnt

  • 执行操作:将 (opr) 向右移动 cnt 指定的次数,空出的高位补入相应个数的符号位 ,CF 的内容为最后移入位的值

  • 示例:有符号数与 2 指数的除法

移位指令使用总结
  • 逻辑移位运算适用于无符号数运算

    • shl 用来乘以 2n
    • shr 用来除以 2n
  • 算术移位运算适用于带符号数运算

    • shl 用来乘以 2n
    • shr 用来除以 2n

n 指的是移位次数,相当于 C++ 中的 << n>> n

循环移位指令

rol 循环左移
  • 格式:rol opr,cnt

  • 执行操作:将操作数向左移动 cnt 指定的位数,移出的位不仅要进入 CF,而且还要填补空出的位

  • 示例:如 (AX)=0012H,(BX)=0034H, 要求把它们装配在 一起形成 (AX)=1234H

    1
    2
    3
    mov	cl,8
    rol ax,cl
    add ax,bx
ror 循环右移
  • 格式:ror opr,cnt

  • 执行操作:将操作数向右移动 cnt 指定的位数,移出的位不仅要进入 CF,而且还要填补空出的位

  • 示例:(BX)=84F0H,把 (BX) 中的 16 位数从低到高每 4 位压入堆栈

rcl 带进位循环左移
  • 格式:rcl opr,cnt

  • 执行操作:将操作数向左移动 cnt 指定的位数,用原 CF 的值填补空出的位,移出的位再进入 CF

rcr 带进位循环右移
  • 格式:rcl opr,cnt

  • 执行操作:将操作数向右移动 cnt 指定的位数,用原 CF 的值填补空出的位,移出的位再进入 CF

串处理指令

这里将的串处理指令中除了 repe/repz 重复前缀指令其他的指令都不影响条件码

基础

别忘了前面提到得段寄存器和偏移寄存器的搭配,对串进行操作的时候:

  • 源串地址:DS:SI
  • 目的串地址:ES:DI

所以再进行串处理的时候需要将定义好的 data 数据段地址传给 dses 两个段地址

处理串的时候需要制定方向:

  • DF=0 每次操作后使 SIDI 增大

  • DF=1 每次操作后使 SIDI 减小

  • 建立方向标志的两条指令:

    • CLD:设置 DF=0
    • STD:设置 DF=1

rep 前缀

  • rep 重复操作直至计数寄存器(cx)的内容为 0

  • 格式:rep string primitive

    其中 string primitive 可为 movsstopslodsinsouts 串处理指令

  • 执行操作:

    1. 如果 cx==0 则退出 rep,否则就往下执行
    2. (cx)<-(cx)-1
    3. 执行后面的操作
    4. 重复 1~3 步

movs 串传送

  • 隐含操作数:(es:di)<-(ds:si)

  • 格式 1:movsb; 字节操作

  • 执行操作 1:

1
2
3
((di))<-((si))
(si)<-(si)±1
(di)<-(di)±1
  • 格式 2:movsw; 字操作

  • 执行操作 2:

1
2
3
((di))<-((si))
(si)<-(si)±2
(di)<-(di)±2

movs 指令可以把由源变址寄存器指向的数据段中的一个字(或者双字,或字节)传送到由目的变址寄存器指向的附加段中的一个字(或双字,或字节)中去,同时根据方向标志及数据格式(双字或字节)对源变址寄存器和目的变址寄存器进行修改。当指令与 rep 指令连用时可以将数据段中的整串数据传送到附加段中。这里源串必须在数据段中,目的串必须在附加段中,但是源串允许使用段跨越前缀来修改。在与 rep 指令连用的时候还必须先把数据串的长度送到寄存器中以便控制指令的结束。串处理准备工作总结操作如下:

  1. 把存放在数据段中的源串首地址(如反向传送则应该是末地址)放入源变址寄存器中
  2. 把将要放入数据传中的附加段中的目的串首地址(或反向传送时的末地址)放入目的变址寄存器中
  3. 把数据串长度放入计数寄存器
  4. 设置方向标志

然后再执行性后序的串处理操作

  • 示例:在数据段中有一字符串,其长度为17,要求把它们传送到附加段中的一个缓冲区中

stos 存入串

  • 隐含操作数:(es:di)<-ac

  • 格式 1:stosb; 字节操作

  • 执行操作1:

    1
    2
    ((di))<-al
    (di)<-(di)±1
  • 格式 2:stosw; 字操作

  • 执行操作 2:

    1
    2
    ((di))<-ax
    (di)<-(di)±2
  • 格式 3:stosd; 双字操作

  • 执行操作 3:

    1
    2
    ((di))<-eax
    (di)<-(di)±4
  • 作用:常用于初始化某一个缓冲区

  • 示例:把附加段中的 10 个字节缓冲区置为 20H

lods 从串取

  • 隐含操作数:ac<-(ds:si)

  • 格式 1:lodsb; 字节操作

  • 执行操作 1:

    1
    2
    (al)<-((si))
    (si)<-(si)±1
  • 格式 2:lodsw; 字操作

  • 执行操作 2:

    1
    2
    (ax)<-((si))
    (si)<-(si)±2
  • 格式 3:lodsd; 双字操作

  • 执行操作 3:

    1
    2
    (eax)<-((si))
    (si)<-(si)±4

一般来说该指令不与 rep 指令连用。有时缓冲区的一串字符需要逐次取出测试时可以使用本指令

repe/repz 重复前缀

  • 当相等/为 0 时重复串指令

  • 格式:repe/repz string primitive

    其中 string primitive 一般是 cmps 或者 scas 指令

  • 执行操作:

    1. 如果 cx==0 或者 zf==0(即某次比较的两个操作数不相等)时退出,否则往下执行
    2. (cx)<-(cx)-1
    3. 执行其后的串指令
    4. 重复 1~3 步
  • repnzrepne 同理就是判断终止的结果和上述相反

scas 串扫描

  • 隐含操作数:(ac)-((es:di))

  • 格式 1:scasb; 字节操作

  • 执行操作 1:

    1
    2
    (al)-((di))
    (di)<-(di)±1
  • 格式 2:scasw; 字操作

  • 执行操作 2:

    1
    2
    (ax)-((di))
    (di)<-(di)±2
  • 格式 3:scasd; 双字操作

  • 执行操作 3:

    1
    2
    (eax)-((di))
    (di)<-(di)±4
  • 说明:不保存结果,根据结果设置条件码 ZF,指令的其他特性和 movs 规定相同

  • 示例:从一个字符串中查找指定的字符

    di 指相匹配字符的下一个地址,cx 剩下还未比较的字符个数

    1
    2
    3
    4
    5
    6
    7
    mess db 'COMPUTER'
    lea di, mess
    mov al, 'T'
    mov cx, 8
    cld
    repne scasb
    ;当执行完串搜索指令后,依据ZF的值判别是否找到:ZF=1为找到,ZF=0则没有找到

控制转移指令

转移分类

  • 段内转移:在同一个段的范围内进行转移,只需要修改 IP 寄存器的内容
    • 短转移:short 8位位移量 -128~+127
    • 近转移:near ptr 16位转移量 -32k~+32k
  • 段间转移:转移到另一个段去执行程序,不仅需要修改 IP 的寄存器的内容还需要修改 CS 寄存器的内容
    • 远转移:far ptr

无条件转移指令

借助的都是 JMP 指令 + 跳转指令

段内直接短转移
  • 格式:jmp short opr

  • 说明:opr 为符号地址

  • 执行操作:(ip)<-(ip)+8位移量

段内直接近转移
  • 格式:jmp near ptr opr
  • 说明:opr 为符号地址
  • 执行操作:(ip)<-(ip)+16位移量
段内间接近转移
  • 格式:jmp word ptr opr
  • 说明:opr 为寄存器或存储器寻址
  • 操作:(ip)<-(ea)
段内直接远转移
  • 格式:jmp far ptr opr

  • 说明:opr 为符号地址

  • 操作:

    1
    2
    (ip)<-opr的段内偏移地址
    (cs)<-opr所在段的段地址
段间间接远转移
  • 格式:jmp dword ptr opr

  • 说明:opr 为存储器寻址

  • 操作:

    1
    2
    (ip)<-(ea)
    (cs)<-(ea)+2

条件转移指令

所有的条件转移指令都是不影响条件码的,他们只是根据之前的条件码确定是否进行转移

条件转移指令是根据上一条指令所设置的条件码来判断测试条件,每一种条件转移指令都有它不同的要求的测试条件,满足测试条件则转移到有指令指定的转向地址去执行那里的程序,如果不满足条件则顺序执行下一条指令。因此当满足条件时 ip 或者 eip 与 8、16 或者 32 位移量相加得到转向地址否则保持不变。

  • 格式:条件转移指令 符号地址

  • 只要情况分析清楚合理的组合运用条件转移指令理论上可以实现各种分支跳转或者循环操作(模拟)

根据单个条件标志设置情况转移
指令 功能 条件
jz/je 结果为 0(或相等) ZF=1
jnz/jne 结果不为 0(或不相等) ZF=0
js 结果为负 SF=1
jns 结果为正 SF=0
jp/jpe 奇偶位为 1 PF=1
jnp/jpo 奇偶位为 0 PF=0
jc/jb/jnae 进位(或低于) CF=1
jnc/jnb/jae 无进位(或不低于) CF=0

上下两行为一组可以对比记忆

  • 示例:根据一次加法运算的结果实行不同的处理,如结果为 0 做动作 2,否则做动作 1
根据两个无符号数比较结果转移
指令 功能 条件
jz/je 等于 ZF=1
jnz/jne 不等于 ZF=0
jb/jnae/jc 低于 CF=1
jnb/jae/jnc 不低于,高于或不等于 CF=0
jbe/jna 低于或等于,不高于 CF∨ZF=1
jnbe/ja 高于 CF∨ZF=0

有无符号数比较结果是写程序常用的指令,对于不同情况我们只需要记住一组表示 (大于,小于) 的情况组合再结合 jz/je 指令即可表示所有情况

这里我选择记忆 (jb,ja) 两组来表示上述所有情况,语义 jajmp abovejbjmp blow

  • 示例:已知一个字节变量 char,试编写一程序段,把其所存的大写字母变成小写字母
根据两个带符号数比较结果转移
指令 功能 条件
jz/je 等于 ZF=1
jnz/jne 不等于 ZF=0
jl/jnge 小于 (SF∀OF) =1
jnl/jge 大于等于 (SF∀OF)=0
jle/jng 小于等于 (SF∀OF)∨ZF=1
jnle/jg 大于 (SF∀OF)∨ZF=0

这里我选择记忆 (jg,jl) 两组来表示上述所有情况,语义 jgjmp greaterjljmp less

测试计数器的值为 0 则转移
  • jcxz short opr;
  • jecxz short opr;

循环指令

loop 循环
  • 格式:loop 标号

  • 执行操作

    1
    2
    (CX)<-(CX)-1
    (CX)!=0,转到标号循环执行
loopz/loope 为零或相等时循环
  • 格式:loopz/loope 标号

  • 执行操作

    1
    2
    3
    (CX)<-(CX) –1
    若(CX)!=0并且ZF=1转到标号
    若(CX)=0或ZF=0,停止循环往下执行
loopnz/loopne 不为零或不相等时循环
  • 格式:loopnz/loopne 标号

  • 执行操作

    1
    2
    3
    (CX)<-(CX)-1
    若(CX)!=0并且ZF=0转到标号
    若(CX)=0或ZF=1,停止循环往下执行
实现循环总结

目前所学有两种实现循环的方法:一种是借助上述的 loop 指令,另一种则是借助转移指令和自设的计数器(一般放在 cx(或者 chcl)中

loop 循环指令实现 jmp 转移指令实现
优点 实现简单,设置好计数器后每次循环自动 -1 实现复杂,每次循环需要记得将计数器 -1
缺点 用法死板,必须用到 cx 如果嵌套里有用到 cx 还需要提前保存 灵活自有,上述情况我们可以改用其他寄存器做计数器

dos 功能调用

  • dos 功能调用是 dos 操作系统为用户提供的许多功能的子程序,可以实现输入和输出

这也说明调用 dos 结束之后需要退出 dos 返回

  • 基本的调用方式是:
    1. 设置调用所需的参数
    2. 功能号送入 ah 寄存器
    3. int 21h 用来调用

功能调用 01

  • 功能:从键盘输入一个字符

  • 入口参数:无

  • 出口参数:键入字符的 ascii 码存入 al 寄存器

  • 调用

    1
    2
    mov	ah,01h
    int 21h

功能调用 02

  • 功能:将一个字符送到显示器显示

  • 入口参数:显示字符的 ascii 码存入 dl 寄存器

  • 出口参数:无

  • 调用

    1
    2
    3
    mov dl,'A' ;或者3ah
    mov ah,02h
    int 21h
  • 思考:将输入的大写字母转换为小写字母

    关键是 ascii 码之间的互转

功能调用 0a

  • 功能:从键盘键入一个字符串

  • 入口参数:将字符串在数据缓冲区的首地址存入 dx 寄存器

  • 出口参数:字符串的长度和每个字符的 ascii 码存入数据区的对应位置

  • 调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ;------------------------
    data segment
    ;数据段定义字符串缓冲区如下
    maxlen db 16
    actlen db ?
    string db 16 dup(?)
    data ends
    ;------------------------
    ;输入字符串的指令如下
    lea dx,maxlen
    mov ah,0ah
    int 21h

    键入如下字符:123456789abcdef↙

    注意传入的字符串的缓冲区的首地址是什么?虽然定义是分开写的,但是传的首地址不变。(ds:dx)=缓冲区最大字数,(ds:dx+1)=实际输入的字符数,所以这里传入的首地址应该是 maxlen

    一个字符串在缓冲区各字节存储情况:前两字节分别存了最大长度和实际长度,末尾有一个回车符

功能调用 09

  • 功能:将一个字符串送显示器显示
  • 入口参数:将显示字符串的串地址存入 dx 寄存器

注意和功能调用 0ah 的缓冲区的首地址区分,这个是将串的内容的首个字符的地址传给 dx

  • 出口参数:无

  • 调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ;------------------------
    data segment
    ;数据段定义字符串缓冲区如下
    string db 'hello,world!$'
    data ends
    ;------------------------
    ;输入字符串的指令如下
    lea dx,string
    mov ah,09h
    int 21h

在调用 dos 时字符串要 $ 结尾作为终端标志(本身这个字符不输出),输出的回车换行字符串可以写为 0dh,0ah,'$'

汇编语言格式

汇编程序功能

以目前学习用的 DOSBox 操作指令为例

  1. 汇编源程序 masm program.asm
  2. 连接程序 link program.asm
  3. 运行程序 program.exe
  4. 调试程序 dubug program.exe
    • -t 单步执行
    • -q 退出 debug

伪操作

汇编语言程序的语句组成:

  • 指令:在程序运行期间由计算机执行
  • 伪操作(伪指令):在汇编程序对源程序汇编期间由汇编程序处理的操作,他们可以完成如处理器选择、定义程序模式、定义数据、分配存储区、指示程序结果等内容
  • 宏指令:在汇编程序对源程序汇编期间由汇编程序展开宏操作

处理伪操作

段定义伪操作

完整的段定义伪操作

段定义伪操作

存储器的物理地址是由段地址和偏移地址组合而成的,汇编程序在把源程序转换目标程序时必须确定标号和变量(代码段和数据段的符号地址)的偏移地址,并且需要把有关信息通过目标模块传送给连接程序,以便连接程序是把不同的段和模块连接在一起形成一个可执行程序。因此有如下格式的段定义伪操作

  • 格式

    1
    2
    3
    segment_name segment
    ...
    segment_name ends
  • 说明:删节号部分对于数据段、附加段和堆栈段来说, 一般是存储单元的定义、分配伪操作;对于代码段则是指令及伪操作

  • 之后明确段和段寄存器的关系:assume segreg:seg_name

    • 段寄存器名字必须是 CS、DS、ES、SS 段名
    • 段名必须是由 segment 定义的段中的段名
    • assume nothing 可以取消前面的指定的段寄存器
  • 最后段地址装入段寄存器:代码段比较特殊不需要这样做

    1
    2
    3
    4
    mov	ax,segment_name
    mov ds,ax
    mov ss,ax
    mov es,ax
  • 示例

存储模型与简化段定义伪操作

较新的汇编程序提供的一种新的较为简单的段定义方法

  • 简单易用

  • 不能像 segment 伪操作那样具有完整的表达能力

model 伪操作
  • 格式:.model memory_mode [,mode options]
  • 说明:用来表示存储模型,即说明在存储器中如何安放共各个段的,也就是说它说明代码段在程序中如何安排,代码的寻址是近还是远;数据段在程序中又是如何安排的,数据的寻址是近还是远。根据不同的组合共有7种存储模型。
选项 定义
Tiny 所有数据和代码都放在一个段内
Small 数据和代码各自放在一个 64KB 段内,最常用的一种模型
Medium 代码使用多个段,数据合并成一个 64KB 的段组
Compact 代码放在一个 64KB 的代码段内,数据可放在多个段
Large 代码和数据都可用多个段
Huge 与 Large 模型相同,差别是允许数据段的大小超过64KB
Flat OS/2 或其它保护模式的操作系统下允许使用 32 位偏移量
简化的段定义伪操作
预定义符号

汇编程序给出了与简化段定义有关的一组预定义符号,他们可在程序中出现,并由汇编程序识别。如在完整的段定义中在程序的一开始需要用段名装入数据段寄存器

1
2
mov	ax,data_seg1
mov ds,ax

若用简化段定义数据段只用 .data 来定义,而并未给出段名,此时可以用

1
2
3
4
5
6
7
8
9
10
11
12
.model small
.stack 100h
.data
array db 0, 1, 2, 3, 4, 5, 6, 7, 8
.code
start:
mov ax, @data
mov ds, ax
...
mov ax, 4c00h
int 21h
end start

这里预定义符号 @ 就给出了数据段的段名,另外一些预定义段符号他们也可以与条件汇编伪操作相配合以帮助用户编写一些较为复杂的代码

程序开始和结束伪操作

数据定义及存储器分配伪操作

  • 格式:[Variable] Mnemonic Operand1, ... ,Operandn [;Comments]

  • 说明:

    • variable:作为符号地址使用,其值为操作数第一个字节的偏移地址

    • comments:可有可无,说明该伪操作的功能

    • mnemonic:说明所定义的数据类型

      类型 名称 占用字节 占用位
      db/byte 字节 1字节 8位
      dw/word 2字节 16位
      dd/dword 双字 4字节 32位
      df/fword(386及后续机型) —— 6字节 48位
      dq/qword 四字 8字节 64位
      dt/tbyte —— 10字节 80位
    • operand:操作数可以是常数、表达式、字符串,也可以是 ?(保留存储空间不存入数据)

      1
      2
      3
      data_byte  	db 		10, 3*20 , 10h, ?
      data_word dw 100, 100h, -5, ?
      message db 'HELLO'
    • 操作数可以用复制操作符 dup() 来复制某个数或者某些操作数,括号外指明循环次数,括号里指明一次循环的内容

      1
      2
      array1 	db 	2 dup(0,1,2,?)
      array2 db 100 dup(?)

      复制操作符是可以嵌套的,分析的时候要仔细认真

      1
      array3 db 100 dup(0, 2 dup(1, 2), 0, 3)
变量类型属性
  • 变量的值:伪操作中的第 1 个数据项在当前段内的第 1 个字节的偏移地址

  • 变量的类型属性:语句中每一个数据项的长度(以字节为单位)

  • 汇编程序用隐含的类型属性来确定某些指令的操作类型

  • 示例

    1
    2
    3
    4
    5
    oper1	db	?,?
    oper2 dw ?,?
    ...
    mov oper1,0 ;字节指令
    mov oper2,0 ;字指令
    1
    2
    3
    4
    5
    6
    oper1 	db 	1,2
    oper2 dw 1234h,5678h
    ...
    mov ax,oper1+1 ;×
    mov al,oper2 ;×
    error: invalid instruction operands
指定操作数的类型属性
  • 格式:type PTR variable 其中 type 可以是 byte word dword fword qword tbyte

  • 处理问题:上面的示例报错就是因为变量类型不匹配

    1
    2
    3
    4
    5
    oper1	db	1,2
    oper2 dw 1234h,5678h

    mov ax,oper1+1
    mov al,oper2

    汇编在执行这一程序时能发现两条 mov 指令的两个操作数的类型属性是不相同的,oper1+1 为字节类型而 ax 是子类型属性,oper2 为字类型属性而 al 为字节类型属性。因此汇编程序将指示出错:这两条 mov 指令的两个操作数的类型不匹配

    一个办法就是借助指定操作数的类型属性,它优先于隐含的类型属性,强制指令按照指定的类型属性执行

  • 示例

1
2
3
4
5
oper1 	db 1,2
oper2 dw 1234h,5678h
...
mov ax,word ptr oper1+1 ;(ax)=3402h
mov al,byte ptr oper2 ;(al)=34h
label 伪操作
  • 可以使用 label 伪操作来定义变量的类型属性

  • 格式:name label type,类型可以是 byte word dword fword qword tbyte

    除了对数据项,对可执行的代码中定义 label_name label type,类型可以是 nearfar,对于 16 位,near 2 字节,far 4 字节

  • 示例

    1
    2
    3
    4
    5
    byte_array 	label 	byte
    word_array dw 50 dup(?)
    tos label word
    mov word_array+2,0 ;把数组的第3个和第4个字节置 0
    mov byte_array+2,0 ;把数组的第3个字节置 0

    这样在 100 个字节数组中的第一个字节的地址赋予两个不同类型属性的变量名,字节类型的变量 byte_array 和字类型的变量 word_array

表达式赋值伪操作

有时程序中多次出现同一个表达式为了方便起见可以用赋值伪操作给表达式赋予一个名字

  • 格式:expression_name equ expression

  • 说明:此后程序中凡需要用到该表达式的地方就可以用表达式的名字代替了,表达式可以是任何有效期有效的操作数格式或者任何可求出常数值的表达式,也就是任何有效的助记符

    1
    2
    3
    4
    5
    6
    7
    constant 	equ 	256 		;数赋以符号名
    data equ height+12 ;地址表达式赋以符号名
    alpha equ 7 ;一组赋值伪操作,借助上面的表达式名字进行下面的符号名赋值
    beta equ alpha-2
    addr equ var+beta
    b equ [bp+8] ;变址引用赋以符号名 b
    p8 equ ds:[bp+8] ;加段前缀的变址引用赋以符号p8

    equ 语句的表达式中,如果有变量或符号的表达式,则在该语句前应该给出它们的定义

地址计数器与对准伪操作

地址计数器 $

在汇编程序 masm 对源程序汇编的过程中使用地址计数器保存当前正在汇编的指令的偏移地址。当开始汇编或在每一段开始时,把地址计数器初始化为 0,以后在汇编过程中每处理一条指令地址就其就增加一个值,此值为该指令所需要的字节数。地址计数器的值可用 $ 表示,汇编语言允许用户直接用 $ 来引用地址计数器的值

这也是说明在段中申请用到的字节地址或是指令地址都是连续的,如此地址计数器才能每次 +1 就指向下一个指令的偏移地址

  • $ 在指令中它表示本条指令的第一个字节的地址,所以 jne $+6 就是转型地址是 jne 指令的首地址 +6 的地址,这里和 $ 搭配的必须是指向另一条指令的首地址,否则会报错。

  • $ 用在伪操作的参数字段时,则和它用在指令中的情况不同,他表示的是计数器的当前值

  • 示例

    1
    array	dw	1,2,$+4,3,4,$+4

    第一个 $+4$ 指向的当前地址的值是 0078H,所以 +4 后内容就是 007CH,第二个 $+4$ 指向地址的值是 007EH,所以 +4 后内容就是 0082H

  • 另外在 dos 系统中 $ 还作为字符串终止的标志,它本身也是一个字符占用相应的字节

org 伪操作
  • 格式:org constant expression

  • 执行操作:用来设置地址计数器的当前值

  • 示例:如常数表达式的值为 n,则 org 伪操作可以使下一个字节的地址变为常数表达式的值

    1
    2
    3
    4
    5
    6
    7
    vectors		segment
    org 10
    vect1 dw 47a5H
    org 20
    vect2 dw 0c596H
    ...
    vetors ends

    则 vect1 的偏移地址值为 0aH,而 vect2 的偏移地址值为 14H

  • 常数表达式也可以表示从当前已定义过的符号开始的位移量,或表示从当前地址计数器值 $ 开始的位移量:如 org $+8 可以表示跳过 8 个字节的存储区,则可用 label 伪操作来定义该缓冲区的如下变量名

    1
    2
    buffer	label	byte
    org $+8

    其完成的功能和 buffer db 8dup(?) 是一样的

even 伪操作
  • 执行操作:even 伪操作使下一个变量或指令开始与偶数字节地址,一个字的地址最好从偶地址开始所以对于字数组为保证其从偶地址开始可以先用 even 伪操作来达到这一目的

  • 示例

    1
    2
    3
    4
    DATA_SEG 	SEGMENT
    EVEN
    WORD_ARR DW 100 DUP(?)
    DATA_SEG ENDS
align 伪操作
  • 执行操作:align 伪操作为保证双字数组边界从 4 的倍数开始创建了条件,其格式为 align boundary 其中 boundary 必须是 2 的幂,指定是谁的倍数

  • 示例

    1
    2
    3
    .DATA
    ALIGN 4
    ARRAYDD 100 DUP(?)

    无疑 align 2even 是等价的

基数控制伪操作

汇编程序默认的数为十进制,当使用其它基数表 示的常数时,需要使用标记:

  • 二进制:B
  • 十进制:D
  • 十六进制:H,如果第一个字符时字母应该在前面加上数字 0
  • 八进制:O Q
  • 字符串可以看做是串常数,可以用引号对括起来,例如 ‘ABCD’

汇编语言程序格式

汇编语言程序中每个语句可以由 4 项组成,格式如下:

1
2
3
4
5
6
string	db	'hello'
start:
mov al,string+2
jz match
match:
lea si,string

名字项

  • 名字项可以是标号或者变量(由字母、数字、? . @ - $ 等专用字符组成)
  • 表示语句的符号地址
  • 可有可无,当需要符号地址来访问该语句时才出现

标号

  • 标号在代码段中定义,后面跟着冒号 :,在转移指令中出现可以作为过程名定义,在 CALL 指令的操作数字段出现。二者均表示转向地址
  • 段属性:定义标号的段的起始地址,此值必须在段寄存器中,标号的段总在 CS 寄存器中
  • 偏移属性:从段的起始地址到定义标号的位置之间的字节数
  • 类型属性:near 段内引用 far 段外引用

变量

  • 变量在数据段或附加数据段中定义,经常在操作数字段出现
  • 段属性:定义变量的段的起始地址,此值必须在段寄存器中
  • 偏移属性:从段的起始地址到定义变量的位置之间的字节数
  • 类型属性:定义该变量所保留的字节数

操作项

  • 指令:汇编程序 MASM 将其翻译为机器指令

  • 伪操作:汇编程序 MASM 将根据所要求的功能进行处理

  • 宏指令:宏展开

操作数项

  • 操作数项由一个或者多个表达式组成,多个操作数之间用 , 分开
  • 对于指令一般给出操作数地址,对于伪操作或宏指令给出所要求的参数
  • 可以是常数、寄存器、标号、变量或者表达式

表达式

  • 表达式是常数、寄存器、标号、变量与一些操作符相结合的序列
  • 分为:数字表达式和地址表达式
  • 常用的操作符:
    • 算术操作符
    • 逻辑与移位操作符
    • 关系操作符
    • 数值回送操作符
    • 属性操作符

算术操作符

  • 包括:+ - * /
  • 可用于数字表达式或者地址表达式
  • 当用于地址表达式时运算结果要有明确的物理意义,经常使用的是:地址 + 数字量 或者 地址 - 数字量

数字回送操作符

  • 包括:type length size offset seg
  • 把一些特征或存储器地址的一部分作为数值回送
offset 操作符
  • 格式:offset variable 或者 label

  • 功能:汇编程序回送变量或者标号的偏移地址值

  • 例如:mov bx,offset oper1

    该指令与 lea bx,oper1 等价

seg 操作符
  • 格式:seg variable 或者 label
  • 功能:汇编程序回送变量或者标号的段地址值
  • 示例:mov bx,seg oper1