《汇编语言》学习笔记
Contents
第一章:基础知识
汇编语言的组成
- 汇编指令:机器码助记符,有对应的机器码
- 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
- 其他符号:如 +, -, *, / 等,由编译器识别,没有对应的机器码
指令和数据
指令和数据,是应用上的概念。在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。CPU在工作的时候,把有的信息看作指令,有的信息看作数据,为同样的信息赋予了不同的意义。
存储单元
每个存储单元从0开始顺序编号,这些编号可以看作是存储单元在存储器中的地址。
一个存储单元可以存储一个 Byte(8 bit)
CPU 对存储器的读写
必须要进行下面3类信息的交互:
- 存储单元的地址(地址信息)=> 地址总线
- 器件的选择,读或写的命令(控制信息) => 控制总线
- 读或写的数据(数据信息)=> 数据总线
数据的读写
- 读: CPU -> 地址总线:3号地址 -> 控制总线信息:读 -> 内存则将3号地址上的数据通过数据总线返回给CPU
- 写: CPu -> 地址总线:3号地址 -> 控制总线信息:写 -> CPU通过数据总线发送要写的数据给内存的3号单元
命令计算机读写
即机器码就是驱动CPU来进行读写的,例如:
MOV AX, [3]
表示传送3号单元的内容到AX
地址总线
CPU是通过地址总线来指定存储单元的,可见地址总线上能传送多少个不同的信息,CPU就可以对多少个存储单元进行寻址。(即 CPU 的寻址单位是:一个存储单元,即一个字节)
一个CPU有N根地址线,则可以说这个CPU的地址总线的宽度为N。这样的CPU最多可以寻找2^N次方个内存单元(即 2^N 个字节,注意,不是位)
数据总线
数据总线的宽度,决定了CPU与外界的数据传送速度。
控制总线
控制总线是一些不同控制线的集合。有多少根控制总线,就意味着CPU提供了外部器件的多少种控制。所以,控制总线的宽度,决定了CPU对外部器件的控制能力。
内存地址空间
各类存储器,在物理上是独立的器件,但在以下两点上相同:
- 都和CPU的总线相连
- CPU对它们进行读或写的时候,都通过控制线发出内存读写命令。也就是说,CPU在操控它们的时候,把它们都当作内存对待 ,把它们总的看作一个由若干存储单元组成的逻辑存储器,这个逻辑存储器就是我们所说的 内存地址空间
在汇编中,我们面对的是 内存地址空间 。CPU对这段内存地址空间中读写数据,实际上就是在相应的物理存储器中读写数据。
即,对于CPU来说,系统中所有存储器中的存储单元,都处于一个统一的逻辑存储器中,它的容量受CPU寻址能力的限制。这个逻辑存储器即是我们所说的 内存地址空间
大小
内存地址空间的大小,受CPU地址总线宽度的限制。
内存地址空间分配
基于一个计算机硬件系统编程时,必须知道这个系统中的内存地址空间分配情况。因为当我们想在某类存储器中读写数据的时候,必须知道它的第一个单元的地址和最后一个单元的地址,才能保证读写操作是在预期的存储器中进行。
不同的计算机系统的内存地址空间的分配情况是不同的。
第二章:寄存器
8086 CPU寄存器
共14个(16位)
这四个是通用寄存器(存放一般性的数据),为了兼容性,可以分开使用两个独立的8位寄存器(H后缀表示高8位,L后缀表示低8位) - AX(AH, AL) - BX(BH,BL) - CX(CH,CL) - DX(DH,DL)
- SI
DI
SP
BP
IP
CS
SS
DS
ES
PSW
字在寄存器中的存储
出于兼容性的考虑,8086CPU一次性处理以下两种尺寸的数据:
- 字节:byte, 即8 bit ,可以存在8位寄存器中
- 字:word,即2个byte,这两个字节分别称为这个字的高位字节和低位字节
物理地址
所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为 物理地址 。
CPU通过地址总线送入存储器的,必须是一个内存单元的 物理地址 。
在CPU向地址总线上发出物理地址之前,必须要在内部先形成这个物理地址。不同的CPU可以有不同的形成物理地址的方式。
16位的CPU
描述了CPU的结构特性:
- 运算器一次最多可以处理16位的数据
- 寄存器的最大宽度为16位
- 寄存器和运算器之间的通路为16位
8086 CPU给出物理地址的方法
8086 CPU有20位地址总线,达1MB寻址能力。但 8086CPU又是16位结构的,如果单纯地发出地址的话,那它只能送出16位的地址,表现出的寻址能力只有64KB。
所以,8086CPU 采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
过程:
- CPU中相关的部件提供两个16位的地址,一个称为 段地址 ,另一个称为 偏移地址
- 段地址和偏移地址通过内部总线送入一个称为地址加法器的部件
- 地址加法器将两个16位地址合成为一个20位的物理地址
- 地址加法器通过内部总线将20位物理地址送入输入输出控制电路
- 输入输出控制电路将20位物理地址送上地址总线
- 20位物理地址被地址总线传送到存储器
地址加法器采用: *物理地址=段地址 * 16 + 偏移地址* (即段地址左移4位) 的方法来合成物理地址。
段地址 * 16 + 偏移地址 = 物理地址 的本质含义
即:CPU在访问内存时,用一个基础地址(段地址 * 16)和一个相对于基础地址的偏移地址相加,给出内存单元的 物理地址 。即:CPU在访问内存时
基础地址 + 偏移地址 = 物理地址
为什么需要 *段地址 * 16* 呢?这是因为需要进一位,然后在此基础上再加上偏移地址,这才是正确的物理地址。(这是 8086 CPU 对地址计算的约定)
段寄存器
8086 CPU有四个段寄存器: CS, DS, SS, ES
段地址在 8086 CPU中由段寄存器存放
CS 和 IP 是8086 CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址。
CS :为代码段寄存器 IP :为指令指针寄存器
在 8086 PC机中,任意时刻,假设 CS 中的内容为M, IP中的内容为N,8086CPU将从内存 M*16 + N 单元开始读取一条指令并执行。 即任意时刻,CPU将 CS:IP 指向的内容当作指令执行
即8086 CPU的工作过程简要如下:
- 从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲器
- IP=IP+所读取指令的长度,从而指向下一条指令
- 执行指令。转至步骤1,重复这个过程
在8086CPU加电启动或复位位, CS和IP被设置为 CS=FFFFH, IP=0000H ,即在8086PC机刚启动时,CPU从内存FFFF0H单元中读取指令执行,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
修改CS,IP的指令
程序员能够用指令读写的部件只有寄存器,可以通过改变寄存器中的内容实现对CPU的控制。CPU从何处执行指令,是由CS,IP中的内容决定的,程序员可以通过改变CS,IP中的内容来控制CPU执行目标指令。
能够改变 CS,IP 的内容的指令被统称为 转移指令 ,比如 JMP 指令。
同时改变CS和IP:
jmp 段地址:偏移地址
只改变IP:
jmp 某一合法寄存器
对于CPU来说,它只认为 CS:IP 指向的内存单元中的内容为指令。
Windows下的 Debug 工具
- R: 查看、改变CPU寄存器的内容. r 寄存器 回车,然后输入值,再回车即可修改 寄存器 的值
- D: 查看内存中的内容. d 段地址:偏移地址 来查看内存内容。或 d 段地址:偏移地址 结尾偏移地址 或 d 段寄存器:偏移地址
- E: 改写内存中的内容. e 起始地址 数据1 数据2 数据3 …
- U: 将内存中的机器翻译为汇编指令。 u 段地址:偏移地址 结尾偏移地址
- T: 执行一条机器指令。直接 t 即可,它会执行 CS:IP 指向的指令。
- A: 以汇编指令的格式在内存中写入一条机器指令。 a 段地址:偏移地址 后回车,就可以开始以汇编指令的形式,直接向内存写入数据。
注意,要想让CPU执行我们的指令,向内存写好汇编指令后,要将 CS和IP 的值指向相应的内存地址。
第三章:寄存器(内存访问)
内存中字的存储
CPU中,用16位寄存器来存储一个字。高8位放在高位字节,低8位放在低位字节。在内存中存储时,由于内存单元是字节单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低字节放在低地址单元中,高位字节放在高位地址单元中。
DS和[address]
8086CPU中,内存地址=段地址+偏移地址 组成。 DS 寄存器通常用来存放要访问数据的段地址。[…] 表示一个内存单元、表示内存单元的偏移地址。
8086CPU中,自动取 DS 中的数据为内存单元的段地址。
MOV 指令中的 [] 说明操作对象是一个内存单元。[] 中的0,说明这个内存单元的偏移地址是0,它的段地址默认放在 DS 中。
内存的读写
读
MOV 寄存器, [内存单元地址]
写
MOV [内存单元地址],寄存器
数据段
将一段内存当作数据段,是我们在编程时的一种安排,可以在具体操作的时候,用 DS 存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元。
栈
LIFO(Last In First Out ,后进先出)
CPU提供的栈机制
8086CPU中提供了相关的指令来以栈的方式访问内存空间。
- PUSH(入栈)
- POP(出栈)
它们都是以 字 为单位进行的。
栈顶
8086CPU中,有两个寄存器,段寄存器 SS 和 寄存器 SP,栈顶的段地址放在 SS 中,偏移地址放在 SP 中。
任意时刻, SS:SP 指向栈顶元素。 PUSH 和 POP 指令执行时,CPU从 SS 和 SP 中得到栈顶的地址。
8086CPU中,入栈时, 栈顶从高地址向低地址方向增长 .
PUSH
PUSH AX 步骤:
- SP=SP-2 ,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
- 将AX中的内容送入 SS:SP 指向的内存单元。SS:SSP 此时指向新栈顶
空栈
以 10000H~1000FH 这段空间看作栈, 此时 SS=1000H,则SP=?
换个角度看,任意时刻, SS:SP 指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以 SS:SP 只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为 1000:000E ,所以栈空时, SP=0010H
POP
pop ax 过程
- 将 SS:SP 指向的内存单元处的数据送入 AX
- SP=SP+2, SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶
注意,POP 之后,只是将 SP 偏移了而已,原数据仍然会在内存中的,但它已经不在栈中了。当再次 PUSH 后,将会写入新的数据。
栈顶超界的问题
8086CPU中,并不会保证我们对栈的操作不会超界。这也就是说,8086CPU,只知道栈顶在何处(由 SS:SP 指示),而不知道我们安排的栈空间有多大。这点好像CPU只知道当前要执行的指令在何处(由 CS:IP 指示),而不知道要执行的指令有多少。
我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致超界;执行出栈的操作时也要注意,以防栈空的时候继续出栈而导致的超界。
POP 和 PUSH 指令
它们与 MOV 指令不同, CPU执行 MOV 指令只需要一步。而 PUSH,POP 指令却需要两步。
PUSH:先改变 SP,后向 SS:SP 处传送 POP:先读取 SS:SP 处的数据,后改变 SP
栈段
将一段内存当作栈段,仅仅是我们在编程时的一种安排,CPU并不会由于这种安排,就在执行 PUSH 、POP 等栈操作指令时自动地将我们定义的栈段当作栈空间来访问。它只是简单地访问 SS:SP 指向的地址当作栈段。
这些完全是我们自己安排的:
我们可以用一个段存放数据,将它定义为 “数据段”;(CS:IP) 我们可以用一个段存放代码,将它定义为 “代码段”;(DS:[]) 我们可以用一个段当作栈,将它定义为 “栈段”;(SS:SP)
我们可以这样子安排,但要让CPU按照我们的安排来访问这些段,就要:
对于数据段,将它的段地址放在 DS 中 对于代码段,将它的段地址放在 CS 中 对于栈段,将它的段地址放在 SS 中,将栈顶单元的偏移地址放在 SP
注意: Debug 中的 T 命令在执行修改寄存器 SS 的指令时,下一条指令也紧接着被执行。
第四章:第一个程序
编译
默认的源码后缀为 .asm ,非这个后缀的,则要写全文件名,包括后缀。
假设源文件为: c:\hello.asm
masm c:\hello;
链接
link hello;
调试
debug hello.exe
注意,要 hello.exe 是要全名的,不能省略 .exe
整个过程
编程 -> 1.asm -> 编译(masm) -> 1.obj -> 链接(link) -> 1.exe -> 加载(command) -> 内存中的程序 -> 运行(CPU)
DOS 的加载过程
- 找到一段起始地址为: SA:0000(即起始地址的偏移地址为0)的容量足够的空闲内存区
- 在这段内存区的前 256 个字节中,创建一个称为程序段前缀(PSP)的数据区, DOS 要利用 PSP 来和被加载程序进行通信
- 从这段内存区的 256 字节处开始(在PSP后面),将程序装入,程序的地址被设置为 SA + 10H:0 (即 PSP的内容范围就是: SA:0 ~ SA+10H:0
- 将该内存的段地址存入 DS 中,初始化其他相关寄存器后,设置 CS:IP 指向程序的入口
PSP的内容为:
- 程序加载后, DS 中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为 DS:0
- 这个内存区的前 256 个字节中存放的是 PSP , DOS 用来和程序进行通信。从 256 字节处向后的空间存放的是程序
所以, 从 DS 中可以得到 PSP 的段地址 SA, PSP 的偏移地址为 0,则物理地址为 SA * 16 + 0 因为 PSP 占 256 (100H)字节,所以程序的物理地址为:
SA * 16 + 0 + 256 = SA * 16 + 16 * 16 + 0 = (SA+16) * 16 + 0
用段地址和偏移地址表示为:
SA + 10H:0
单任务执行过程
- 由其他程序(debug, command或其他程序),将可执行文件中的程序加载入内存
- 设置 CS:IP 指向程序的第一条要执行的指令(即程序的入口),从而使程序得以运行
- 程序运行结束后,返回到加载者
第五章:[BX] 和 loop指令
要完整地描述一个内存单元,需要两种信息
- 内存单元的地址:DS 为段地址, [N] N 为偏移地址
- 内存单元的长度(类型):可以由具体指令中其他操作对象(比如说寄存器)指出
[BX] 同样也表示一个内存单元,它的偏移地址在 BX 中
loop 指令
下面2步是自动CPU处理的
- CX = CX - 1
- CX 不为0,则转至标号处执行; 如果为0,则向下执行
汇编源程序中,数据不能以字母开头
debug 中的 G 命令
它可以让 CPU 一直执行到指定的地址中为止:
G 0012
即将程序执行到 IP 地址为 0012H 的地方
debug 中的 P 命令
它会自动重复执行循环中的指令,直到 CX = 0 为止 。即在 CS:IP 指向 loop xxx 时,输入 p 命令即可。
当然,也可以用 G 命令来间接实现这个目的
debug 和汇编器 masm 对指令的不同处理
debug 中:
mov ax, [0]
它表示将 ds:0 处的数据送入 ax 中
masm 源代码中:
mov ax,[0]
编译后,生成的是
mov ax, 0
所以,在 masm 这样子的要显式出给段地址:
mov ax, ds:[0]
或
mov ax, [bx]
这样子它就会默认是 DS:[bx] 了
一段安全的空间
不能确定一段内存空间中是否存放着重要的数据或代码的时候,不能随意向其中写入内容。
不要忘记,我们是在操作系统的环境中工作,操作系统管理所有的资源,也包括内存。如果我们需要向内存空间写入数据的话,要使用操作系统给我们分配的空间,而不应直接用地址任意指定内存单元,向里面写入。
在CPU保护模式下的操作系统(Windows 2000, Unix)中,硬件已经被这些操作系统利用CPU保护模式所提供的功能全面而严格地管理了。
第六章:包含多个段的程序
程序取得所需空间的方法有两种
- 在加载程序的时候为程序分配
- 程序在执行的过程中向系统申请
程序第一条指令
这是由可执行文件中描述信息指明的。
可执行文件由描述信息和程序组成 - 程序来自于源程序中的汇编指令和定义的数据 - 描述信息则主要是编译、连接程序对源程序中相关伪指令进行处理所得到的信息
只用一个段来写代码
assume cs:code
code segment
...
数据
...
start:
...
代码
...
code ends
end start
多段
assume cs:code, ds:data, ss:stack
data segment
dw 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0,0,0
stack ends
code segment
start:
mov ax, stack
mov ss, ax
mov sp, 20h
mov ax, data
mov ds, ax
mov bx, 0
mov cx, 8
s:
push [bx]
add bx, 2
loop s
mov bx,0
mov cx,8
s0:
pop [bx]
add bx,2
loop s0
mov ax, 4c00
int 21h
code ends
end start
第七章:更灵活的定位内存地址的方法
and 和 or 指令
mov al,01100011B
add al,00111011B
or al,00111011B
[bx+idata]
[bx+idata] 表示一个内存单元,它的偏移地址为 [bx] + idata 。也可以写成如下格式:
mov ax, [200+bx]
或
mov ax, 200[bx]
或
mov ax, [bx].200
SI 和 DI
它们和 BX 功能相近。但 SI 和 DI 不能够分成两个8位寄存器来使用。
[bx+si] 和 [bx+di]
指明内存单元:
- [bx(si或di)]
- [bx(si或di) + idata]
也可以:
- [bx+si]
- [bx+di]
表示一个内存单元,它的偏移地址为 [bx] + [si] 或 [bx] + [di]
[bx+si+idata] 和 [bx+di+idata]
表示一个内存单元,它的偏移地址为: [bx] + [si] + idata
寻址方式
- [idata] : 用一个常量来表示地址,可用于直接定位一个内存单元
- [bx] : 用一个变量来表示内存地址,可用于间接定位一个内存单元
- [bx+idata] : 用一个变量和常量表示地址,可在一个起始地址的基础上,用变量间接定位一个内存单元
- [bx+si或di] : 用两个变量表示地址
- [bx+si或di+idata] : 用两个变量和一个常量表示地址
第八章:数据处理的两个基本问题
- 处理的数据在什么地方
- 要处理的数据有多长
bx, si, di 和 bp
- 只有这4个寄存器可以在 [..] 中进行内存单元的寻址
- 在 [..] 中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si, bx和di, bp和si, bp和di
- 在 [..] 中使用寄存器 bp, 而指令中没有显性地给出段地址,段地址默认就在 SS 中
机器指令处理的数据在什么地方
在机器指令这一层,并不关心数据的值是多少,而关心 指令执行前一刻 ,它将要处理的数据所在的位置。可以在3个地方:
- CPU 内部
- 内存
- 端口
数据位置的表达
立即数
mov ax, 1
寄存器
mov ax, bx
段地址(SA)和偏移地址(EA)
mov ax, [0]
寻址方式
直接寻址
[idata]
寄存器间接寻址
[bx]
[si]
[di]
[bp]
寄存器相对寻址
[bx+idata]
[si+idata]
[di+idata]
[bp+idata]
基址变址寻址
[bx+si]
[bx+di]
[bp+si]
[bp+di]
相对基址变址寻址
[bx+si+idata]
[bx+di+idata]
[bx+si+idata]
[bp+di+idata]
要处理数据的长度
8086CPU 可以处理两种尺寸的数据: byte 和 word
- 通过寄存器名指明处理的数据的尺寸
- 没有寄存器名存在的情况下, 用 X ptr 指明内存单元的长度,X 可以为 word 或 byte
- 其他方法。有些指令默认访问的是字单元,还是字节单元。比如 push 就只能进行字单元操作
div 指令
- 除数:8位或16位,在一个寄存器或内存单元中
- 被除数:默认放在AX或DX和AX中。如果除数为8位,被除数则为16位,默认在AX中存放;如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
- 结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数
dup
db 重复次数 dup (重复的字节型数据)
dw 重复次数 dup (重复的字型数据)
dd 重复次数 dup (重复的双字型数据)
第九章:转移指令的原理
可以修改 IP 或同时修改 CS 和 IP 的指令统称为 转移指令
只修改IP,称为段内转移,如: jmp ax
- 短转移: IP 修改的范围为: -128 ~ 127
- 近转移:IP 修改的范围为: -32768 ~ 32767
同时修改 CS 和 IP 称为段间转移,如: jmp 1000:0
转移指令分类
- 无条件转移指令,如 jmp
- 条件转移指令
- 循环指令, 如 loop
- 过程
- 中断
offset
它是由编译器处理的符号,功能是: 取得标号的偏移地址
jmp 指令
要出给两种信息:
- 转移的目的地址
- 转移的距离(段间转移、段内短转移、段内近转移)
依据位移进行转移的 jmp 指令
jmp short 标号
它对IP的修改范围为 -128 ~ 127 ,即向前最多 128 个字节, 向后最多 127 个字节。
注意:CPU 执行 JMP 指令的时候,并不需要转移的目的地址。
jmp short 标号
并不包含转移的目的地址,而包含的是 转移的位移 。这个位移,是编译器根据汇编指令中的 标号 计算出来的。
转移的目的地址在指令中的 jmp 指令
jmp far ptr 标号
它是段间转移,又称为远转移。它会同时修改 CS 和 IP
转移地址在寄存器中的 jmp 指令
jmp 16位寄存器
功能: IP = 16位寄存器的值
转移地址在内存中的 jmp 指令
jmp word ptr 内存单元地址(段内转移)
jmp dword prt 内存单元地址(段间转移)
jcxz 指令
它是条件转移指令。 所有的条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为: -128 ~ 127
如果 cx = 0 ,则转移到标号处执行,否则程序向下执行。
loop 指令
它是循环指令。 所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为:-128 ~ 127
CX = CX-1,然后 CX != 0 ,则转移到标号处执行。
第十章:CALL 和 RET 指令
ret 和 retf
ret => 用栈中的数据,修改IP的内容,从而实现近转移
- IP = SS * 16 + SP
- SP = SP + 2
相当于:
pop IP
retf => 用栈中的数据,修改CS和IP,从而实现远转移
- IP = SS * 16 + SP
- SP = SP + 2
- CS = SS * 16 + SP
- SP = SP + 2
相当于:
pop IP pop CS
call 指令
- 将当前 IP 或 CS 和 IP 压入栈
- 转移
它不能实现短转移,除此之餐,它和 JMP 指令的原理相同。
依据位移进行转移的 call 指令
call 标号(将当前IP压入栈后,转移到标号处执行指令)
- SP = SP + 2; SS *16 + SP = IP
- IP = IP + 16位位移
转移的目的地址在指令中的 call 指令
call far prt 标号
它是段间转移。
- SP = SP - 2; SS * 16 + SP = CS; SP = SP - 2; SS * 16 + SP = IP
- CS = 标号所在段的段地址; IP = 标号所在段中的偏移地址
相当于:
push CS push IP jmp far prt 标号
转移地址在寄存器中的 call 指令
call 16位寄存器
- SP = SP - 2
- SS * 16 + SP = IP
- IP = 16位寄存器的值
转移地址在内存中的 call 指令
两种格式:
- call word prt 内存单元地址
push ip jmp word ptr 内存单元地址
- call dword prt 内存单元地址
push cs push ip jmp dword prt 内存单元地址
mul 指令
- 两个相乘数:要么都是8位,要么都是16位。如果是8位,则一个默认在 AL 中,另一个在 8 位寄存器或内存字节单元中。要么是 16 位,一个默认在 AX 中,另一个在 16 位或内存字节单元中。
结果:8位的话,结果默认在 AX 中; 16 位,则默认高位在 DX 中,低位在 AX 中
mul 寄存器 mul 内存单元
第十一章:标志寄存器
特殊的寄存器作用:
- 存储相关指令的某些执行结果
- 为CPU执行相关指令提供行为依据
- 控制CPU的相关工作方式
其他寄存器是用来存放数据的,都是整个寄存具有一个含义。而标志寄存器,是按拉起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。
0:CF
进位标志位。一般情况下,在进行 无符号 运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值
2:PF
奇偶标志位。它记录相关指令执行后,其结果的所有 bit 位中 1 的个数是否为偶数。如果1的个数为偶数,则 PF = 1, 如果为奇数,则 PF = 0
4:AF
6:ZF
零标志位。它记录相关指令执行后,结果是否为0.如果为0,ZF=1,否则 ZF=0
7:SF
符号标志位。它记录相关指令执行后,其结果是否为负。如果为负,SF = 1, 如果非负, SF = 0
8:TF
9:IF
IF = 0,在进入中断处理程序后,禁止其他的可屏蔽中断。 IF = 0,表示中断处理程序需要处理可屏蔽中断。
sti :设置 IF = 1
cli : 设置 IF = 0
10:DF
方向标志位。在串处理指令中,控制每次操作后 si, di 的增减。
DF = 0 :每次操作后, si, di 递增 DF = 1 : 每次操作后, si,di 递减
cld 指令:将标志寄存器的 DF 位置0
std 指令:将标志寄存器的 DF 位置1
11:OF
溢出标志位。一般情况下,OF 记录了 有符号 运算的结果是否发生了溢出。如果溢出了, OF = 1 ,如果没有, OF = 0
注意, OF 是对有符号的。CF 是对无符号的
adc 指令
adc 是带进位加法指令。它利用了 CF 位上记录的进位值。
adc 操作对象1,操作对象2
结果为
操作对象1 = 操作对象1 + 操作对象2 + CF
利用它,可以对任意大的数据进行加法运算。
sbb 指令
它是带借位减法指令,它利用了 CF 位上记录的借位值。
sbb 操作对象1,操作对象2
功能
操作对象1 = 操作对象1 - 操作对象2 - CF
利用它,可以对任意大的数据进行减法运算。
cmp 指令
它相当于减法,只是不保存结果。将对标志寄存器产生影响。
cmp 操作对象1, 操作对象2
功能
操作对象1 - 操作对象2 ,但并不保存结果,仅仅是对标志寄存器进行设置
检测比较结果的条件转移指令
下面是常用的根据 无符号 数的比较结果进行转移的条件转移指令
je
equal
等于则转移。检测的是 ZF = 1
jne
not equal
不等于则转移,检测的是 ZF = 0
jb
below
低于则转移,检测的是 CF = 1
jnb
not below
不低于则转移,检测的是 CF = 0
ja
above
高于则转移。 CF = 0 且 ZF = 0
jna
not above
不高于则转移。 CF = 1 或 ZF = 1
pushf 和 popf
pushf :将标志寄存器的值压栈 popf : 从栈中弹出数据,送入标志寄存器
第十二章:内中断
中断信息可以来自 CPU 的内部和外部。
中断的产生
- 除法错误:中断类型码为0
- 单步执行:中断类型码为1
- 执行 into 指令:中断类型码为4
- 执行 int 指令: int N,N为字节型立即数,即提供给CPU的中断类型码
8086CPU用称为中断类型码的数据来标识中断信息的来源。
中断类型码为一个字节型数据,可以表示 256 种中断信息的来源。
中断向量表
CPU用8位的中断类型码通过中断向量表,找到相应的中断处理程序的入口地址。
所谓中断向量,就是中断处理程序的入口地址。展开来讲,中断向量表,就是中断处理程序的入口地址的列表。
中断向量表在内存中存放。对于 8086CPU,中断向量表指定放在内存地址 0 处。从内存 0000:0000 到 0000:03FF 的1024个单元中存放着中断向量表。
一个表项存放着一个中断向量,也就是一个中断处理程序的入口地址,对于 8086CPU, 这个入口地址包括段地址和偏移地址,所以,一个表项占两个字,高地址存放段地址,低地址存放偏移地址。
中断过程
找到中断入口地址的最终目的是用它设置 CS 和 IP ,使CPU执行中断处理程序。
用中断类型码找到中断向量,并用它设置 CS 和 IP 这个工作是由 CPU 硬件自动完成的。CPU 硬件完成这个工作的过程,称为 中断过程
整个过程如下:
- 取得中断类型码 N
- pushf
- TF = 0, IF = 0
- push CS
- push IP
- IP = N * 4, CS = N * 4 + 2
中断处理程序和 iret 指令
中断处理程序的常规步骤:
- 保存用到的寄存器
- 处理中断
- 恢复用到的寄存器
- 用 iret 指令返回
iret 指令用汇编语法表述为:
POP IP POP CS popf
响应中断的特殊情况
举例:CPU在执行完 SS 指令后,不响应中断。
这给连续设置 SS 和 SP 指向正确的栈顶提供了一个时机。
第十三章:int 指令
int n
它的功能是:引发中断号为 N 的中断过程
BIOS 和 DOS 中断例程的安装过程
- 开机后, CPU一加电,初始化 CS = 0FFFFH, IP=0 ,自动从 FFFF:0 单元开始执行程序。FFFF:0 有一条转跳指令,CPU执行后,转去执行BIOS中的硬件系统检测和初始化程序。
- 初始化程序将建立 BIOS 所支持的中断向量,即将 BIOS 提供的中断例程的入口地址登记在中断向量表中。注意,对于BIOS所提供的中断例程,只需要将入口地址登记在中断向量表中即可,因为它们是固化到 ROM 中的程序,一直在系统内存中存在。
- 硬件系统检测和初始化完成后,调用 int 19H 进行操作系统的引导,从此将计算机交由操作系统控制
- DOS 启动后,除完成其他工作以外,还将它提供的中断例程装入内存,并建立相应的中断向量
BIOS 和 DOS 提供的中断例程,都是用 AH 来传递 内部子程序的编号 。
第十四章:端口
CPU可以直接读写以下3个地方的数据
- CPU内部的寄存器
- 内存单元
- 端口
端口的读写
它和内存地址一样,通过地址总线来传送。在PC中,CPU最多可定位64KB个不同的端口。则端口地址的范围为 0 ~ 65535
它只有两个指令:
in :读 out : 写
在 in 和 out 指令中,只能使用 ax 或 al 来存放从端口中读入的数据或要发送到端口中的数据。8位时,使用 AL, 16位时, 使用 AX
SHL 和 SHR 指令
SHL 的功能:
- 将一个寄存器或内存单元中的数据向左移位
- 将最后移出的一位写入 CF 中
- 最低位用 0 补充
SHR 与 SHR 的操作刚好相反
第十五章:外中断
PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CPU将这些寄存器当作端口来访问。
外中断源
可屏蔽中断
如果 IF = 1,则CPU执行完当前指令后响应中断,引发中断过程; 如果 IF = 0,则不响应可屏蔽中断
不可屏蔽中断
是CPU必须响应的外中断。对于 8086CPU,不可屏蔽中断的中断类型码固定为 2 ,所以,中断过程中,不需要取中断类型码。
几乎所有由外设引发的外中为,都是 可屏蔽中断 。
PC 机键盘的处理过程
按下一个键时产生的扫描码称为 通码 松开一个键时产生的扫描码称为 断码
扫描码长度为:一个字节。通码的第7位为0,断码的第7位为1 。即:
断码 = 通码 + 80H
杂项
显存的物理地址
B8000~BFFFF
ASCII 与 显卡文本模式显示
ASCII 是7位代码,只用了一个字节中的低7比特,最高位通常置0 。这意味着,ASCII只包含 128 个字符的编码。
屏幕上的每个字符,对应着显存中的两个连续字节。
前一个字节是字符的ASCII代码,后一个字节是显示属性,包括字符颜色(前景色)和底色(背景色)。
显示属性分为两部分,低4位定义的是前景色,高4位是背景色。 RGBK(背景色),其中K是闪烁位,0为不闪烁,1为闪烁。 RGBL(前景色),其中L是亮度位,0为正常亮度,1为高亮。
MOV
目的操作数不能为立即数,而且目的操作数和源操作数不允许同时为内存单元。
汇编地址
它是在源程序编译期间,编译器为每条指令确定的汇编位置,指示该指令相对于程序或段起始处的距离,以字节计算。当编译后的程序装入物理内存后,它又是该指令在内存段内的偏移地址。
jmp near xxx
jmp near 的操作数并非目标位置的偏移地址,而是目标位置 相对于当前指令处的偏移量(以字节为单位)
编译器对它的处理: 用标号(目标位置)处的汇编地址减去当前指令的汇编地址,再减去当前指令的长度(3),就得到了 jmp near xxx 指令的实际操作数。
movsb 或 movsw
源数据:DS:SI 目的地:ES:DI
CX: 次数
DF标志位: cld => 清0,表示传送的是正向(从内存低地址到高地址) std => 置1,表示传送的是反向(从内存高地址到低地址)
MOVSB 或 MOVSW 通常与 rep (表示 repeat) 结合使用。
偏移地址
如果要用寄存器来提供偏移地址,只能使用:
bx, si, di, bp 不能使用其他寄存器。
cbw 或 cwd
cbw => (convert byte to word),将 AL 中的有符号数扩展到整个AX。如果AL为 10001101,执行完这指令后,AX为 1111111110001101 cbd => (convert word to double word),将AX中的有符号数扩展到 DX:AX 。
call 指令
相对近调用
这个是通过符号或立即数(立即数也要减去当前指令的汇编地址)给出的。
计算过程:
用目标过程的汇编地址送去当前 call 指令的汇编地址,于减去当前 call 以字节为单位的长度(3),保留16位的结果。
call near xxx
near 不是必须的,如果不提供任何关键字,则默认就是 near
绝对近调用
这个是通过寄存器或内存单元给出目标地址的。
绝对远调用
call xxxx:yyyy
间接绝对远调用
call far [xxx]
它会使用 xxx 处的2个字(注意是字,第一个字是偏移地址,第二个字是段地址)来分别代替IP和CS的内容。
ret 和 retf
ret 和 retf 经常用做 call 和 call far 的配对指令。
ret 是近返回:它只做一件事,就是从栈中弹出一个字到指令指针寄存器IP中
retf 是远返回:处理器分别从栈中弹出两个字到指令指针寄存器IP和代码段寄存器CS中
call 和 ret , retf 不会影响任何的标志寄存器
jmp
相对短转移
jmp short 标号或数值
用目标地址减去当前指令的汇编地址,再减去当前指令的长度(2),保留一个 字节 的结果(结果是有符号数)
16位相对近转移
jmp near 标号或数值
用目标地址减去当前指令的汇编地址,再减去当前指令的长度(3),保留一个字(16位)的结果。(结果是有符号数)
16位间接绝对近转移
jmp near bx
它也是近转移,即只是段内转移。但目标地址是通过寄存器或内存地址(该地址里的内存内容)给出的
16位直接绝对远转移
jmp xxxx:yyyy
16位间接绝对远转移
标号 dw yyyy, xxxx jmp far [标号]
注意,第一个字是偏移地址,第二个才是段地址
即相当于转移到 jmp far xxxx:yyyy
iret
这个是中断返回指令,它会导致处理器依次从栈中弹出(恢复)IP、CS 和 Flags 的原始内容。
CMOS RAM
前14个字节分别为:
0x0:秒 0x1:闹钟秒 0x2:分 0x3:闹钟分 0x4:时 0x5:闹钟时 0x6:星期 0x7:日 0x8:月 0x9:年 0xa: 寄存器A 0xb: 寄存器B 0xc: 寄存器C 0xd: 寄存器D
访问需要通过两个端口:
0x70或0x74 => 它是索引端口,用于指定CMOS RAM的内存单元 0x71或0x75 => 它是数据端口,用来读写相应的内存单元
比如下面代码就是读取星期几:
MOV al,0x06 OUT 0x70, al IN al, 0x71
我现在是星期四(因为我系统设置了每周第一天为星期天),所以下面的数值为05(实际是我们一般人理解的星期四,只是不同的起始计数不同)
执行完后,可以看到al值为 05 (它是以星期天为开始计数的,即星期一~星期天为, 01~07)