8086版汇编

汇编不是一个语言,而是一群语言(不同架构不一样,相同架构不同版本不一样,相同架构相同版本可能还有不同的格式,如 x86 就有 ATT 和 intel 两种格式)。学汇编前现需要明白自己的学的汇编版本。本篇讲述的是 86/88 16 位的 intel 格式的汇编 ,而 386 及其以后是 32 位乃至更高。

1. 基础知识

1.1 汇编语言的组成

机器码:一串二进制数,由 CPU 执行

汇编指令:由对应机器码的指令,要通过编译器编译为机器码才能够运行

伪指令:对汇编过程进行控制的指令,不能被运行,需要翻译成汇编指令

1.2 指令和数据

在内存或磁盘上,指令和数据没有区别,都是一串二进制数据;

CPU 工作时,将有的信息看作指令,有的看做数据;

8086 内部分为两个部分:总线接口单元 BIU 和 执行单元 EU

BIU:负责与存储器和 I/O 接口传送信息(读写数据)
EU:负责所有指令执行,也负责计算内存地址(执行指令)
EU 和 BIU 能够独立工作,两者并行工作减少 CPU 取指而等待的时间,从而提高 CPU 利用率

8086 的指令队列有 6 个字节
当指令队列出现 2 个空字节 BIU 就自动执行一次取指令周期,将下一条要执行的指令从内存单元读入指令队列
采用先进先出原则,按顺序存放,并按顺序取到 EU 中去执行
从而提高取指与其他操作的并行度

1.3 CPU 对存储器的读写

存储器被划分成多个单元,存储单元从 0 开始编号,即地址

CPU 要想进行数据读写就必须进行 3 类信息交互

存储单元的地址(地址信息)
是读是写,还是其他(控制信息)
读/写的数据(数据信息)
CPU 如何将上诉三类信息传入存储器呢?总线,按照逻辑则分为:地址总线、控制总线、数据总线

即,通过地址总线获取要访问的存储单元地址,控制总线获取是读还是写操作,数据总线则用以存放读/写数据

1.4 总线

地址总线:用以制定存储器单元地址,其宽度(根数)决定寻址能力

数据总线:数据传送通过数据总线,其宽度决定一次传输数据的位数

控制总线:通过控制总线传输控制信息,其宽度决定有多少种控制

地址总线一共 10 根,意味着最大寻址是 2^10 次方,从 0 到 2^10 - 1

数据总线一共 8 根,意味着一次最多传输 8 bit 的数据

控制总线一共 2 根,意味这也就只有 2 种控制

1.5 寄存器基本介绍

寄存器可以理解为 CPU 内部的存储器。对所有的数据操作,都必须读入寄存器才能交由 CPU 操作

8086 所有寄存器都是 16 bit

通用寄存器:常用放一般数据,可拆分为高低 8 位寄存器,如:AX,BX,CX,DX

MOV AX, 1234H ; 写AX,AH=12H,AL=34H
MOV AL, 12H ; 写AL,AX=1212H
MOV BX, AX ; 写BX,BX=1212H

段寄存器:存放段地址信息,如:CS(代码段)、DS(数据段)、SS(堆栈段)、ES(附加段)

指令指针寄存器 IP

变址寄存器:DI(目的变址)、SI(源变址)

指针寄存器:SP(堆栈指针)、BP(基址指针)

标志寄存器 FLAGS

详细看后面

1.6 16 位结构的 CPU

所谓 16 位结构的 CPU,即

运算器最多处理 16 bit 数据
寄存器最大 16 bit
寄存器和运算器的通路为 16 bit
1.7 8086 数据类型
字节:byte,大小 8 bit,可存在 8 位寄存器

字:word,大小 16 bit,有高低两个字节组成,存在 16 位寄存器上

8086 采用小端存储,即字数据的高 8 位存储在高 8 位寄存器,低 8 位存储在低 8 位寄存器(最低有效为在前)

如:0x01234567 在 0x100 位置存储

0x100 0x101 0x102 0x103
67 45 23 01
字数据大小由字长决定,所谓字长即一次能处理的最大位数长度

1.8 物理地址

8086 有 20 位地址总线,但是只有 16 位结构的 CPU。所以需要一种将 16 的地址映射为 20 位地址的方案

CPU 提供 2 个地址,一个称为段地址,另一个称为偏移地址,通过地址加法器,映射为物理地址

物理地址 = 段地址 * 16 + 偏移地址

内存没有分段,段的划分来自 CPU

不同的段地址和偏移地址可以组成相同的物理地址

2 寄存器

2.1 MOV 指令

格式如:指令 目标操作数, 源操作

; MOV 将数据存放到一个寄存器/内存单元中
MOV AX, 8	; 立即数到寄存器
MOV AX, BX  ; 寄存器到寄存器
MOV AX, [0] ; 存储单元到寄存器
MOV [0], AX ; 寄存器到存储单元
MOV DS, AX  ; 寄存器到段寄存器

2.2 执行指令地址: CS 和 IP

CPU 如何知道当前 需要执行的指令所在位置?

即通过CS, IP 中存放着当前指令的段地址 和偏移地址。
CS 代码段寄存器,IP 指令指针寄存器。

可以通过-r ip 命令, 修改当前ip的数值;

IP 存放的是在代码段中的偏移的地址,
任意时刻 CS:IP 指向的物理地址的数据,都将解释为当前执行的指令;
读取一条指令后,IP 中的值会自动增加,增加多少取决于该指令占了多少字节;

; 通过修改CS和IP所存储的数据,就可以改变要执行的指令
JMP 1234H:4321H ; CS=1234H,IP=4321H
JMP AX          ; 相当于 MOV IP, AX

2.3 内存单元地址: DS 和[];

内存单元地址:

CPU 要读写一个内存单元的时候, 必须先给出一个内存单元的地址,

8086PC 中, 内存地址 由段地址 和偏移地址组成, 指令执行时, 8086PC 自动取ds中的数据为内存单元的 段地址,
mov 指令中的 [] 说明操作对象是一个内存单元, [] 中的0 表明这个内存单元的偏移地址是0, 它的段地址默认放在 ds 中, 指令执行时, 自动取 ds 中的值 作为段地址;

数据段中的所有二进制位都将解析为普通的数据

物理地址是通过段地址和偏移地址构成,但是你发现使用 MOV 指令只需要指定偏移地址,段地址却没有指定是因为,8086cpu 自动取ds 中的数据作为内存单元的段地址。

存储器寻址默认的段地址为数据段地址,也就是 DS 寄存器所存储的地址

MOV AX, [1234H]     ; 默认为DS段
MOV AX, ES[1234H] 	; 段重设为ES段
MOV DS, 4321H       ; 同样也可修改DS中所存储的地址

mov 指令中的 [] 说明操作对象,是一个内存单元, [] 中的数值, 比如取0 , 则表明这个内存单元的偏移地址是0, 它的段地址默认放在ds 中, 指令执行时, 8086CPU会自动从ds 中取出;

2.4 栈段 SS:SP

栈:后进先出的数据结构

8086CPU 中,就有相应的寄存器来存放栈顶的地址,
段寄存器SS和 寄存器SP, 栈顶的段地址存放在SS中, 偏移地址存放在SP中;

任意时刻, SS:SP 指向栈顶元素, push 指令 和pop 指令执行时, CPU 从SS和 SP 中得到栈顶的地址。

push 和 Pop 指令执行时, CPU从SS 和SP中得到栈顶的地址。

push 和 pop 的区别:
push: 1. 先更新sp, 令 sp = sp - 2 , 2. 然后将 ax 中的内容输入到 ss:sp 所指的内存单元处。

Pop : 1. 先将ss:sp 指向的内存单元处的数据输入到 axzhong

MOV  AX, 1234H 
; PUSH和POP操作数,可以是寄存器、内存单元、段寄存器
PUSH AX ; 将AX存放的1234H,压入栈
POP  BX ; 将栈顶元素弹出,放入BX

同样的并没有指定段地址,是因为对栈操作的段地址是 SS 堆栈段寄存器所存储的值

为了标识栈顶元素,所以还会有一个 SP 栈顶指针寄存器指向栈顶元素的偏移地址

SS:SP 指向栈顶元素。因为栈的进出方式,所以 PUSH 和 POP 操作本质是在改变 SP 中所存储的偏移地址

栈顶超界问题:因为只有 SS 和 SP 两个寄存器,无法标志栈从哪开始到哪结束,所以一味着 PUSH 和 POP 会出现问题

3 常用汇编指令

3.1 ADD、SUB

; ADD、SUB操作数类型同MOV
MOV AX, 1
ADD AX, 2 ; AX=1+2=3
SUB AX, 1 ; AX=3-1=2

3.2 DIV

MOV AX, 5
DIV AX, 2 ; AH=1,AL=2,AX=00010010B

除数为 8 位,则被除数为 16 位

除数位 16 位,则被除数为 32 位,其中高 16 位在 DX,低 16 在 AX(目标操作数的寄存器)

结果为 8 位,则高 8 位为余数,低 8 位为商

结果为 16 位,则 DX 为余数,AX 为 商

3.3 MUL

MOV AX, 2
MOV DX, 3
MUL DX    ;AX=DX*AX=2*3=6

两个数相乘只能是 8 位和 8 位乘或 16 位和 16 位乘

若 8 位乘,则用 AL 和寄存器乘

若 16 位乘,则用 AX 和寄存器乘

结果 8 位默认放在 AX

结果 16 位,则低位在 AX,高位在 DX

3.4 AND 和 OR

; AND按位与,OR按位或
MOV AL, 01100011B
AND AL, 00111011B ; AL=00100011B 
OR  AL, 01110110B ; AL=00110010B

关于 ASCII 码:汇编中用单引号引住的字符,会将里面的所有字符变为 ASCII 码存储


MOV AX, ‘unIX’ ; AX=756E49H
1
大写 二进制 小写 二进制
A 0100 0001 a 0110 0001
B 0100 0010 b 0110 0010
… … … …
观察上表发现,大写与小写字母的二进制,只是在第 6 位不同

即将第 3 位数为 0 则是大写,1 则是小写

; 大小写转换
MOV AX, 'A'
OR  AX, 00100000B ; 转为小写a
AND AX, 00000000B ; 转为大写A

3.5 数组操作

利用基址寻址和变址寻址,达到数组的操作

关于 8086 寻址方式:点击

MOV DS   , BX
MOV BX[0], 1 ; 2000H:0 的位置存了1
MOV BX[1], 2 ; 2000H:1 的位置存了2'
MOV BX[2], 3 ; 2000H:2 的位置存了3

3.6 IN 和 OUT

I/O 端口:点击

8086 采用非统一编制,意味着读写端口需要使用特殊的指令

; 只能使用AX或AL存放从端口读入或向端口写入的数据
IN  AL, 60H ; 从60H端口读入一个字节到AL寄存器
OUT 60H, AL ; AL寄存器的数据输出到60H端口

4 转移指令

段内转移:只修改 IP。修改范围为 -128 ~ 127 为短转移,修改范围为 -32768-32767 为近转移

段间转移:修改 CS 和 IP

转移指令有无条件转移、条件转移、循环指令、过程、中断

4.1 LOOP

LOOP 循环指令,属于短转移

CPU 执行 LOOP 指令,进行以下两步操作


CX 寄存器中存储的值 - 1
CX 中的值不为零则继续

; 计算2^3
   MOV CX, 3
   MOV AX, 2
S: ADD AX, AX
   LOOP S

标号:S: 标记一条指令,当执行 LOOP S 满足条件时会跳到标记的指令

OFSET 操作符:可以取得标号偏移地址

4.2 JCXZ 和 JMP

JCXZ 条件转移指令,属于短转移。在转移前,会先判断 CX 寄存器是否为 0,只有为 0 时才会转移

JMP 无条件转移指令,直接就跳转

; S 为标号
JCXZ S
JMP S

4.3 RET 和 RETF

RET 指令:弹出栈顶元素给 IP 寄存器

RETF 指令:弹出栈顶元素给 IP 寄存器,再弹出栈顶元素给 CS 寄存器

RET ; POP IP
RETF ; 先POP IP,再POP CS

4.4 CALL

CALL S
; 等价于以下
PUSH IP
JMP S

5 标志寄存器

5.1 标志寄存器介绍

标志寄存器的作用

存储相关指令的结果
CPU 执行指令提供依据
控制 CPU 工作方式

ZF:0 标志位,为 1 时说明结果是 0

PF:奇偶标志位,“1”的个数为偶数,置1,否则置0

SF:符号标志位,为 1 为负数

CF:进位/借位标志位,只有当无符号计算时有效

OF:溢出标志位,只有当有符号计算时有效

AF:辅助进位标志位,BCD 码的进位

DF:方向标志位,决定串操作指令执行时有关指针寄存器调整方向,为 1 时递减

TF:跟踪标志位,为 1 时进入单步跟踪,每执行一条指令就会产生一个中断

IF:中断标志位,为 1 时响应可屏蔽中断

5.2 ADC 和 SBB

ADC 带进位的加法,SBB 带借位的减法

其作用就是可以将超 16 位的数据进行相加减

; 俩32位数据相加,如计算1EF000H+201000H
MOV AX, 001EH ; 高4位
MOV BX, 0F00H ; 低4位
ADD BX, 1000H ; 低4位相加,产生进位信息给CF
ADC AX, 0020H ; 高4位相加,再加进位信息CF

; 俩32位数据相减,如计算3E1000H-202000H
MOV AX, 003EH
MOV BX, 1000H
SUB BX, 2000H
SBB AX, 0020H

5.3 CMP

CMP 比较指令,其原理是使用减法指令,但是不会存储结果,只会影响标志位信息

通过读取标志位信息就可以得到俩操作数大小比较结果

MOV AX, 1
MOV BX, 2
CMP AX, BX ; 1-2,产生借位,则CF=1
CMP BX, AX ; 2-1=1,则CF=0,ZF=0
CMP AX, AX ; 1-1=0,则ZF=1

5.4 检测标志位的条件转移

指令 跳转条件 检测标志位
JE 等于 ZF=1
JNE 不等于 ZF=0
JB 小于 CF=1
JNB 不小于 CF=0
JA 大于 CF=0 且 ZF=1
JNA 不大于 CF=1 或 ZF=1
5.5 PUSHF 和 POPF
PUSHF 将标志寄存器的值压入栈

POPF 将栈中元素弹出到标志寄存器

5.6 SHL 和 SHR

SHL 是逻辑左移指令,SHR 是逻辑右移指令

逻辑左移和逻辑右移动会将缺失补 0,并且会将移出的最后一位写入 FLAGS 中的 CF 标志位

MOV AL, 10011000B
SHL AL, 1 ; 逻辑左移4位,AL=00110000B,CF=1
SHR AL, 1 ; 逻辑右移4位,AL=00011000B,CF=0

还有 SAL、SAR(算术左移、算术右移) 和 ROL、ROR(循环左移、循环右移)

6 中断

6.1 内中断

CPU 内部发生除法错误,单步执行,执行 into 指令和 int 指令时,则会产生内中断。

CPU 用中断类型码标识中断信息的来源。中断类型码 8 bit,可表示 256 种中断源

执行 INT N 意味着触发 N 号中断的中断过程,即通过该指令可以调用任意的中断处理程序
执行 INTO ,检测 OF 位是否位 1(是否溢出),当溢出时发生中断

6.2 外中断

当 CPU 外部需要处理的事情发生时,就会产生外中断。如:外设的数据到达

从 INTR 引脚接入,可屏蔽中断:CPU 可以不响应的外中断。当从标志寄存器的 IF 为 1 时,CPU 才会响应

从 NMI 引脚接入,不可屏蔽中断:CPU 必须响应的外中断。执行完当前指令后,立即响应。中断类型码固定为 2,所以不需要从中断信息中获取中断类型码

外中断的过程和内中断几乎是一样的,只是内中断是 CPU 产生的,而外中断是通过数据总线传入 CPU 的

关于中断、异常的分类
国内教材:把中断分为外部中断和内部中断。一般把外部中断叫做中断。内部中断叫做异常,异常又分为陷阱、故障、终止。
国外教材(csapp):把异常分为同步异常和异步异常。同步异常包含陷阱、故障、终止。异步异常则为中断。
以上两者的共同点是,异常是同步事件,中断则是异步事件。抓住这点,即使是不同教材,也能区分。

6.2 中断向量表

中断向量:存放中断处理程序的入口地址。通过更改入口地址,就能当发生中断时执行自定义的程序

中断向量表:由中断向量组成,这张表是一个连续的内存空间

用中断类型码,取得中断向量,从而得到中断处理程序的入口地址

对于 8086 来说,中断向量表存放在内存地址 0 处

一共 256 个中断向量,每个中断向量 4 个字节。即中断向量表范围为 0000H~03FFH

6.3 中断过程和恢复现场

中断的过程

从中断信息中获取中断类型码(不可屏蔽中断不需要)
FLAGS 中的数据入栈。中断要改变标志寄存器,所以先存入栈
TF、IF 置 0(可屏蔽中断不需要)
CS、IP 中的数据依次入栈
通过中断类型码获取中断程序入口地址
根据入口地址设置 CS、IP
中断程序处理的最后,执行 IRET 指令恢复现场,程序继续往下执行

因为栈中保留了中断前的数据,只需要从栈中弹出 IP、CS、FLAGS 的数据即可

参考资料
王爽:《汇编语言(第3版)》

你可能感兴趣的:(#,汇编,汇编语言)