汇编不是一个语言,而是一群语言(不同架构不一样,相同架构不同版本不一样,相同架构相同版本可能还有不同的格式,如 x86 就有 ATT 和 intel 两种格式)。学汇编前现需要明白自己的学的汇编版本。本篇讲述的是 86/88 16 位的 intel 格式的汇编 ,而 386 及其以后是 32 位乃至更高。
机器码:一串二进制数,由 CPU 执行
汇编指令:由对应机器码的指令,要通过编译器编译为机器码才能够运行
伪指令:对汇编过程进行控制的指令,不能被运行,需要翻译成汇编指令
在内存或磁盘上,指令和数据没有区别,都是一串二进制数据;
CPU 工作时,将有的信息看作指令,有的看做数据;
8086 内部分为两个部分:总线接口单元 BIU 和 执行单元 EU
BIU:负责与存储器和 I/O 接口传送信息(读写数据)
EU:负责所有指令执行,也负责计算内存地址(执行指令)
EU 和 BIU 能够独立工作,两者并行工作减少 CPU 取指而等待的时间,从而提高 CPU 利用率
8086 的指令队列有 6 个字节
当指令队列出现 2 个空字节 BIU 就自动执行一次取指令周期,将下一条要执行的指令从内存单元读入指令队列
采用先进先出原则,按顺序存放,并按顺序取到 EU 中去执行
从而提高取指与其他操作的并行度
存储器被划分成多个单元,存储单元从 0 开始编号,即地址
CPU 要想进行数据读写就必须进行 3 类信息交互
存储单元的地址(地址信息)
是读是写,还是其他(控制信息)
读/写的数据(数据信息)
CPU 如何将上诉三类信息传入存储器呢?总线,按照逻辑则分为:地址总线、控制总线、数据总线
即,通过地址总线获取要访问的存储单元地址,控制总线获取是读还是写操作,数据总线则用以存放读/写数据
地址总线:用以制定存储器单元地址,其宽度(根数)决定寻址能力
数据总线:数据传送通过数据总线,其宽度决定一次传输数据的位数
控制总线:通过控制总线传输控制信息,其宽度决定有多少种控制
地址总线一共 10 根,意味着最大寻址是 2^10 次方,从 0 到 2^10 - 1
数据总线一共 8 根,意味着一次最多传输 8 bit 的数据
控制总线一共 2 根,意味这也就只有 2 种控制
寄存器可以理解为 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
详细看后面
所谓 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
字数据大小由字长决定,所谓字长即一次能处理的最大位数长度
8086 有 20 位地址总线,但是只有 16 位结构的 CPU。所以需要一种将 16 的地址映射为 20 位地址的方案
CPU 提供 2 个地址,一个称为段地址,另一个称为偏移地址,通过地址加法器,映射为物理地址
物理地址 = 段地址 * 16 + 偏移地址
内存没有分段,段的划分来自 CPU
不同的段地址和偏移地址可以组成相同的物理地址
格式如:指令 目标操作数, 源操作
; MOV 将数据存放到一个寄存器/内存单元中
MOV AX, 8 ; 立即数到寄存器
MOV AX, BX ; 寄存器到寄存器
MOV AX, [0] ; 存储单元到寄存器
MOV [0], AX ; 寄存器到存储单元
MOV DS, AX ; 寄存器到段寄存器
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
内存单元地址:
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 中取出;
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 会出现问题
; ADD、SUB操作数类型同MOV
MOV AX, 1
ADD AX, 2 ; AX=1+2=3
SUB AX, 1 ; AX=3-1=2
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 为 商
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
; 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
利用基址寻址和变址寻址,达到数组的操作
关于 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
I/O 端口:点击
8086 采用非统一编制,意味着读写端口需要使用特殊的指令
; 只能使用AX或AL存放从端口读入或向端口写入的数据
IN AL, 60H ; 从60H端口读入一个字节到AL寄存器
OUT 60H, AL ; AL寄存器的数据输出到60H端口
段内转移:只修改 IP。修改范围为 -128 ~ 127 为短转移,修改范围为 -32768-32767 为近转移
段间转移:修改 CS 和 IP
转移指令有无条件转移、条件转移、循环指令、过程、中断
LOOP 循环指令,属于短转移
CPU 执行 LOOP 指令,进行以下两步操作
CX 寄存器中存储的值 - 1
CX 中的值不为零则继续
; 计算2^3
MOV CX, 3
MOV AX, 2
S: ADD AX, AX
LOOP S
标号:S: 标记一条指令,当执行 LOOP S 满足条件时会跳到标记的指令
OFSET 操作符:可以取得标号偏移地址
JCXZ 条件转移指令,属于短转移。在转移前,会先判断 CX 寄存器是否为 0,只有为 0 时才会转移
JMP 无条件转移指令,直接就跳转
; S 为标号
JCXZ S
JMP S
RET 指令:弹出栈顶元素给 IP 寄存器
RETF 指令:弹出栈顶元素给 IP 寄存器,再弹出栈顶元素给 CS 寄存器
RET ; POP IP
RETF ; 先POP IP,再POP CS
CALL S
; 等价于以下
PUSH IP
JMP S
标志寄存器的作用
存储相关指令的结果
CPU 执行指令提供依据
控制 CPU 工作方式
ZF:0 标志位,为 1 时说明结果是 0
PF:奇偶标志位,“1”的个数为偶数,置1,否则置0
SF:符号标志位,为 1 为负数
CF:进位/借位标志位,只有当无符号计算时有效
OF:溢出标志位,只有当有符号计算时有效
AF:辅助进位标志位,BCD 码的进位
DF:方向标志位,决定串操作指令执行时有关指针寄存器调整方向,为 1 时递减
TF:跟踪标志位,为 1 时进入单步跟踪,每执行一条指令就会产生一个中断
IF:中断标志位,为 1 时响应可屏蔽中断
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
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
指令 跳转条件 检测标志位
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 将栈中元素弹出到标志寄存器
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(循环左移、循环右移)
CPU 内部发生除法错误,单步执行,执行 into 指令和 int 指令时,则会产生内中断。
CPU 用中断类型码标识中断信息的来源。中断类型码 8 bit,可表示 256 种中断源
执行 INT N 意味着触发 N 号中断的中断过程,即通过该指令可以调用任意的中断处理程序
执行 INTO ,检测 OF 位是否位 1(是否溢出),当溢出时发生中断
当 CPU 外部需要处理的事情发生时,就会产生外中断。如:外设的数据到达
从 INTR 引脚接入,可屏蔽中断:CPU 可以不响应的外中断。当从标志寄存器的 IF 为 1 时,CPU 才会响应
从 NMI 引脚接入,不可屏蔽中断:CPU 必须响应的外中断。执行完当前指令后,立即响应。中断类型码固定为 2,所以不需要从中断信息中获取中断类型码
外中断的过程和内中断几乎是一样的,只是内中断是 CPU 产生的,而外中断是通过数据总线传入 CPU 的
关于中断、异常的分类
国内教材:把中断分为外部中断和内部中断。一般把外部中断叫做中断。内部中断叫做异常,异常又分为陷阱、故障、终止。
国外教材(csapp):把异常分为同步异常和异步异常。同步异常包含陷阱、故障、终止。异步异常则为中断。
以上两者的共同点是,异常是同步事件,中断则是异步事件。抓住这点,即使是不同教材,也能区分。
中断向量:存放中断处理程序的入口地址。通过更改入口地址,就能当发生中断时执行自定义的程序
中断向量表:由中断向量组成,这张表是一个连续的内存空间
用中断类型码,取得中断向量,从而得到中断处理程序的入口地址
对于 8086 来说,中断向量表存放在内存地址 0 处
一共 256 个中断向量,每个中断向量 4 个字节。即中断向量表范围为 0000H~03FFH
中断的过程
从中断信息中获取中断类型码(不可屏蔽中断不需要)
FLAGS 中的数据入栈。中断要改变标志寄存器,所以先存入栈
TF、IF 置 0(可屏蔽中断不需要)
CS、IP 中的数据依次入栈
通过中断类型码获取中断程序入口地址
根据入口地址设置 CS、IP
中断程序处理的最后,执行 IRET 指令恢复现场,程序继续往下执行
因为栈中保留了中断前的数据,只需要从栈中弹出 IP、CS、FLAGS 的数据即可
参考资料
王爽:《汇编语言(第3版)》