第一章:基础知识

汇编语言的组成

  1. 汇编指令:机器码助记符,有对应的机器码
  2. 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
  3. 其他符号:如 +, -, *, / 等,由编译器识别,没有对应的机器码

指令和数据

指令和数据,是应用上的概念。在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。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位的物理地址。

过程:

  1. CPU中相关的部件提供两个16位的地址,一个称为 段地址 ,另一个称为 偏移地址
  2. 段地址和偏移地址通过内部总线送入一个称为地址加法器的部件
  3. 地址加法器将两个16位地址合成为一个20位的物理地址
  4. 地址加法器通过内部总线将20位物理地址送入输入输出控制电路
  5. 输入输出控制电路将20位物理地址送上地址总线
  6. 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的工作过程简要如下:

  1. 从 CS:IP 指向的内存单元读取指令,读取的指令进入指令缓冲器
  2. IP=IP+所读取指令的长度,从而指向下一条指令
  3. 执行指令。转至步骤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 步骤:

  1. SP=SP-2 ,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
  2. 将AX中的内容送入 SS:SP 指向的内存单元。SS:SSP 此时指向新栈顶

空栈

以 10000H~1000FH 这段空间看作栈, 此时 SS=1000H,则SP=?

换个角度看,任意时刻, SS:SP 指向栈顶元素,当栈为空的时候,栈中没有元素,也就不存在栈顶元素,所以 SS:SP 只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的字单元的偏移地址+2,栈最底部字单元的地址为 1000:000E ,所以栈空时, SP=0010H

POP

pop ax 过程

  1. 将 SS:SP 指向的内存单元处的数据送入 AX
  2. 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 的加载过程

  1. 找到一段起始地址为: SA:0000(即起始地址的偏移地址为0)的容量足够的空闲内存区
  2. 在这段内存区的前 256 个字节中,创建一个称为程序段前缀(PSP)的数据区, DOS 要利用 PSP 来和被加载程序进行通信
  3. 从这段内存区的 256 字节处开始(在PSP后面),将程序装入,程序的地址被设置为 SA + 10H:0 (即 PSP的内容范围就是: SA:0 ~ SA+10H:0
  4. 将该内存的段地址存入 DS 中,初始化其他相关寄存器后,设置 CS:IP 指向程序的入口

PSP的内容为:

img

  1. 程序加载后, DS 中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为 DS:0
  2. 这个内存区的前 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

单任务执行过程

  1. 由其他程序(debug, command或其他程序),将可执行文件中的程序加载入内存
  2. 设置 CS:IP 指向程序的第一条要执行的指令(即程序的入口),从而使程序得以运行
  3. 程序运行结束后,返回到加载者

第五章:[BX] 和 loop指令

要完整地描述一个内存单元,需要两种信息

  1. 内存单元的地址:DS 为段地址, [N] N 为偏移地址
  2. 内存单元的长度(类型):可以由具体指令中其他操作对象(比如说寄存器)指出

[BX] 同样也表示一个内存单元,它的偏移地址在 BX 中

loop 指令

下面2步是自动CPU处理的

  1. CX = CX - 1
  2. 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保护模式所提供的功能全面而严格地管理了。

第六章:包含多个段的程序

程序取得所需空间的方法有两种

  1. 在加载程序的时候为程序分配
  2. 程序在执行的过程中向系统申请

程序第一条指令

这是由可执行文件中描述信息指明的。

可执行文件由描述信息和程序组成 - 程序来自于源程序中的汇编指令和定义的数据 - 描述信息则主要是编译、连接程序对源程序中相关伪指令进行处理所得到的信息

只用一个段来写代码

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] : 用两个变量和一个常量表示地址

第八章:数据处理的两个基本问题

  1. 处理的数据在什么地方
  2. 要处理的数据有多长

bx, si, di 和 bp

  1. 只有这4个寄存器可以在 [..] 中进行内存单元的寻址
  2. 在 [..] 中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si, bx和di, bp和si, bp和di
  3. 在 [..] 中使用寄存器 bp, 而指令中没有显性地给出段地址,段地址默认就在 SS 中

机器指令处理的数据在什么地方

在机器指令这一层,并不关心数据的值是多少,而关心 指令执行前一刻 ,它将要处理的数据所在的位置。可以在3个地方:

  1. CPU 内部
  2. 内存
  3. 端口

数据位置的表达

立即数

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

  1. 通过寄存器名指明处理的数据的尺寸
  2. 没有寄存器名存在的情况下, 用 X ptr 指明内存单元的长度,X 可以为 word 或 byte
  3. 其他方法。有些指令默认访问的是字单元,还是字节单元。比如 push 就只能进行字单元操作

div 指令

  1. 除数:8位或16位,在一个寄存器或内存单元中
  2. 被除数:默认放在AX或DX和AX中。如果除数为8位,被除数则为16位,默认在AX中存放;如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
  3. 结果:如果除数为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的内容,从而实现近转移

  1. IP = SS * 16 + SP
  2. SP = SP + 2

相当于:

pop IP

retf => 用栈中的数据,修改CS和IP,从而实现远转移

  1. IP = SS * 16 + SP
  2. SP = SP + 2
  3. CS = SS * 16 + SP
  4. SP = SP + 2

相当于:

pop IP pop CS

call 指令

  1. 将当前 IP 或 CS 和 IP 压入栈
  2. 转移

它不能实现短转移,除此之餐,它和 JMP 指令的原理相同。

依据位移进行转移的 call 指令

call 标号(将当前IP压入栈后,转移到标号处执行指令)
  1. SP = SP + 2; SS *16 + SP = IP
  2. IP = IP + 16位位移

转移的目的地址在指令中的 call 指令

call far prt 标号

它是段间转移。

  1. SP = SP - 2; SS * 16 + SP = CS; SP = SP - 2; SS * 16 + SP = IP
  2. CS = 标号所在段的段地址; IP = 标号所在段中的偏移地址

相当于:

push CS push IP jmp far prt 标号

转移地址在寄存器中的 call 指令

call 16位寄存器
  1. SP = SP - 2
  2. SS * 16 + SP = IP
  3. IP = 16位寄存器的值

转移地址在内存中的 call 指令

两种格式:

  1. call word prt 内存单元地址

push ip jmp word ptr 内存单元地址

  1. 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 硬件完成这个工作的过程,称为 中断过程

整个过程如下:

  1. 取得中断类型码 N
  2. pushf
  3. TF = 0, IF = 0
  4. push CS
  5. push IP
  6. IP = N * 4, CS = N * 4 + 2

中断处理程序和 iret 指令

中断处理程序的常规步骤:

  1. 保存用到的寄存器
  2. 处理中断
  3. 恢复用到的寄存器
  4. 用 iret 指令返回

iret 指令用汇编语法表述为:

POP IP POP CS popf

响应中断的特殊情况

举例:CPU在执行完 SS 指令后,不响应中断。

这给连续设置 SS 和 SP 指向正确的栈顶提供了一个时机。

第十三章:int 指令

int n

它的功能是:引发中断号为 N 的中断过程

BIOS 和 DOS 中断例程的安装过程

  1. 开机后, CPU一加电,初始化 CS = 0FFFFH, IP=0 ,自动从 FFFF:0 单元开始执行程序。FFFF:0 有一条转跳指令,CPU执行后,转去执行BIOS中的硬件系统检测和初始化程序。
  2. 初始化程序将建立 BIOS 所支持的中断向量,即将 BIOS 提供的中断例程的入口地址登记在中断向量表中。注意,对于BIOS所提供的中断例程,只需要将入口地址登记在中断向量表中即可,因为它们是固化到 ROM 中的程序,一直在系统内存中存在。
  3. 硬件系统检测和初始化完成后,调用 int 19H 进行操作系统的引导,从此将计算机交由操作系统控制
  4. DOS 启动后,除完成其他工作以外,还将它提供的中断例程装入内存,并建立相应的中断向量

BIOS 和 DOS 提供的中断例程,都是用 AH 来传递 内部子程序的编号

第十四章:端口

CPU可以直接读写以下3个地方的数据

  1. CPU内部的寄存器
  2. 内存单元
  3. 端口

端口的读写

它和内存地址一样,通过地址总线来传送。在PC中,CPU最多可定位64KB个不同的端口。则端口地址的范围为 0 ~ 65535

它只有两个指令:

in :读 out : 写

在 in 和 out 指令中,只能使用 ax 或 al 来存放从端口中读入的数据或要发送到端口中的数据。8位时,使用 AL, 16位时, 使用 AX

SHL 和 SHR 指令

SHL 的功能:

  1. 将一个寄存器或内存单元中的数据向左移位
  2. 将最后移出的一位写入 CF 中
  3. 最低位用 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(实际是我们一般人理解的星期四,只是不同的起始计数不同)

img

执行完后,可以看到al值为 05 (它是以星期天为开始计数的,即星期一~星期天为, 01~07)