抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

基础知识

  • 地址总线
    • CPU通过地址总线来指定存储单元,N根导线可以传送N位二进制数,可选址$2^N$个内存单元,可表示最大的数为$2^N - 1$。
  • 数据总线
    • CPU与内存或其他器件之间的数据传输是通过数据总线来进行的。数据总线的宽度决定了CPU和外界的数据传输速度。如8根数据总线一次可以传送8位(一个字节)二进制数据。
  • 控制总线
    • CPU对外部器件的控制是通过控制总线来进行的。有多少跟控制总线就意味着CPU提供了对外部器件的多少种控制。
  • 主板
    • 主板上有核心器件和一些主要部件,这些部件通过总线(地址总线、数据总线、控制总线)相连
  • 接口卡
    • CPU通过总线向接口卡发送命令,接口卡根据CPU的命令控制外设进行工作
  • 各类储存器芯片
    • 从读写属性上分为两类:随机储存器(RAM)和只读储存器(ROM)
      • 随机储存器可读可写,关机后储存的内容丢失
      • 只读储存器只读不能写入,关机后内容不会丢失
  • 地址空间
    • 内存地址空间的大小受CPU地址总线宽度的限制,如CPU地址总线宽度是20,可以传送$2^{20}$个不同的地址信息。可定位$2^20$个内存单元,该CPU的内存地址空间大小为1MB

寄存器

寄存器是CPU中程序员可以用指令读写的部件。不同的CPU寄存器的个数、结构是不同的。8086CPU有14个寄存器,每个寄存器有一个名字。这些寄存器是:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。

通用寄存器

8086CPU的所有寄存器都是16位的,可以储存两个字节。AX、BX、CX、DX这4个寄存器通常用来存放一般性数据,被称为 通用寄存器 。这4个寄存器可以分为两个独立使用的8位寄存器来使用:

  • AX可分为AH和AL
    • AX的低8位构成了AL寄存器,高8位构成了AH寄存器,后面的同理
  • BX可分为BH和BL
  • CX可分为CH和CL
  • DX可分为DH和DL

几条汇编命令

汇编命令 控制CPU完成的操作 用高级语言的语法描述
mov ax,18 将18送入寄存器AX AX=18
mov ax,bx 将寄存器bx的数据送入寄存器AX AX=BX
add ax,18 将寄存器AX的数值加上18 AX=AX+18
add ax,bx 将寄存器AX的数值加上寄存器BX的数值 AX=AX+BX

物理地址

CPU访问内存单元时,要给出内存单元的地址。所有的内存单元构成的存储空间是一个一维的线性空间,每个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。

16位结构的CPU

特征:

  • 运算器最多可处理16位的数据
  • 寄存器的最大宽度为16
  • 寄存器和运算器之间的通路为16位

8086CPU给出物理地址的方法

8086CPU有20位地址总线,可以传送20位地址。8086CPU又是16位结构,在内部一次性处理、传输、暂时储存的地址为16位。

8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址

8086CPU读写内存的过程如下:

    1. CPU中相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址
    1. 段地址和偏移地址通过内部总线接入一个称为地址加法器的部件
    1. 地址加法器将两个16位地址合成一个20位的物理地址
    1. 地址加法器通过内部总线20位物理地址送入输入输出控制电路
    1. 输入输出控制电路将20位物理地址送上地址总线
    1. 20位物理地址被地址总线传送到储存器

地址加法器采用 物理地址=段地址×16+偏移地址 的方法合成物理地址。如1230+00C8=12300+00C8=123C8(十六进制表示)

“物理地址=段地址×16+偏移地址”的本质含义

本质含义是:CPU在访问内存时,用一个基础地址(段地址×16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址。(有点类似计网中的网段)

段的概念

其实内存没有分段,段的划分来自于CPU,如我们可以认为地址10000H100FFH的内存单元组成一个段,基础地址为10000H,段地址为1000H;我们也可以认为10000H10007FH、10080H~100FFH的内存单元为两段,基础地址为10000H和10080H,段地址为1000H和1008H。

在编程时根据需要,将若干地址连续的内存单元看作一个段。

段寄存器

8086CPU在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址。段地址在8086CPU的段寄存器中存放。8086CPU有4个段寄存器:CS、DS、SS、ES。

CS和IP

CS和IP是8086CPU中最关键的两个寄存器,它们指示了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器。

在8086PC机中,设CS中内容为M,IP中内容为N,8086CPU将从内存M×16+N单元开始,读取一条命令并执行。过程如下

    1. 8086CPU当前状态:CS中内容为2000H,IP中内容为0000H
    1. 内存20000H~20009H单元中存放着的机器码对应的汇编指令如下:
      • 地址:20000H~20002H,内容:B8 23 01,长度:3Byte,对应汇编指令:mov ax,0123H
      • 等等
    1. CS和IP寄存器的内容传入地址加法器合成物理地址20000H
    1. 输入输出控制电路将物理地址20000H送上地址总线
    1. 从内存20000H单元开始存放的机器指令B8 23 01通过数据总线送入CPU
    1. 输入输出控制电路将机器指令B8 23 01送入指令缓冲器
    1. IP中的值根据指令长度自动增加
  • 跳转到第1步,重复这个过程

CPU工作的时候把有的信息看作指令,有的信息看作数据,那CPU根据什么将内容中的信息看着指令?

  • CPU将CS:IP指向的内存单元中的内容看作指令,因为,在任何时候,CPU将CS、IP中的内容看作指令的段地址和偏移地址,来在内存中读取指令码

修改CS、IP的指令

在CPU中程序员能用指令读写的部件只有寄存器,程序员可以通过寄存器实现对CPU的控制。CPU从何处执行指令是由CS、IP中的内容决定的,通过改变CS、IP中的内容来控制CPU执行目前指令。

8086CPU大部分寄存器的值都可以通过mov指令来改变,mov指令被称为传送指令。但mov指令不能用于设置CS、IP的值,因为8086没有提供这样的功能。要改变CS、IP的内容需要的指令被称为跳转指令。一下简单介绍jmp指令。

若想修改CS、IP的内容,可用jmp 段地址:偏移地址的指令完成,如jmp 2AE3:3执行后CS=2AE3H, IP=0003H

若想仅修改IP的内容,可用形如jmp 某一合法寄存器的指令来完成,如jmp ax执行后IP=ax。含义上好似mov IP,ax

寄存器(内存访问)

内存中字的储存

字单元的概念 :字单元,即存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。高地址内存单元存放字型数据的高位字节,低地址内存单元中存放字型数据的低位字节。

以后我们称起始地址为N的字单元简称为N地址字单元。

例子:
| | |
|—|—–|
| 0 | 20H |
| 1 | 4EH |
| 2 | 12H |
| 3 | 00H |

0地址单元中存放的字节型数据是:20H

0地址字单元中存放的字型数据是:4E20H

DS和[address]

CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086PC中,内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。

mov al, [0],这个指令将内存单元中的内容送入寄存器al。”[address]”表示一个内存单元,其中[0]中的0表示内存单元的偏移地址。执行命令时8086CPU自动读取DS中的数据为内存单元的段地址。

如何把一个数据送入DS寄存器呢?我们以前用过类似mov ax,a这样的指令来完成,但是8086CPU不支持将数据直接放入段寄存器的操作,所有需要先将数据放入一个一般寄存器,然后把一般寄存器的内容送入DS寄存器,mov ds,ax

字的传送

我们用mov指令在寄存器和内存之间进行字节型数据的传送。因为8086CPU是16位结构的,也就是说一次传送一个字。只要在mov指令中给出16位寄存器就可以进行16位的数据传送了,mov指令中给出8位寄存器就进行8位数据传输。

例子:
| | |
|—|—–|
| 0 | 20H |
| 1 | 4EH |
| 2 | 12H |
| 3 | 00H |

mov ax,[1]的结果是ax=124EH

CPU提供的栈机制

8086CPU提供相关的指令来以栈的方式访问内存空间,这意味着基于8086编程时,可以将一段内存当作栈来使用。

8086CPU提供pushpop的入栈和出栈指令。push ax表示将寄存器ax中的数据送入栈中,pop ax表示从栈顶取出数据送入ax。8086CPU的入栈和出栈操作都是以字为单位进行的。

  • CPU如何知道哪段地址空间被当作栈来使用?push和pop时如何知道哪个单元是栈顶单元?
    • 8086CPU中,有两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻SS:SP指向栈顶元素

push ax执行时,由一下两个两步完成

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

因为CPU执行顺序是从地址低到高,而栈的后进先出的结构,所以push时从后方的地址开始
| | |
|—————-|—-|
| 10000H | |
| 10001H | |
| … | |
| SS:SP->1000EH | 23 |
| 1000FH | 01 |

pop ax的执行过程和push ax刚好相反:

    1. 将SS:SP指向的内存单元处的数据送入ax中
    1. SP=SP+2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新栈顶

栈为空时,偏移地址的计算方法:最底部的字单元的偏移地址+2,如:
| | |
|————————|–|
| 10000H | |
| 10001H | |
| … | |
| (最底部的字单元)1000EH | |
| 1000FH | |
| SP=0010H | |

栈段

将一段内存当作栈段仅仅是我们编程时的一种安排

小结

  • 字在内存中存储时,要用两个地址连续的内存单元来存放
  • 用mov指令访问内存单元,可以在mov指令中给出内存单元的偏移地址,此时段地址默认在DS寄存器中
  • [address]表示一个偏移地址为address的内存单元
  • 在内存和寄存器之间传送数据类型时,高地址单元和高8位寄存器、低地址单元个低8位寄存器相对应
  • mov、add、sub是具有两个操作对象的指令,jmp是具有一个操作对象的指令
  • CPU不提供栈顶越界的保护,我们要自己注意

一个程序

一个源程序从写出到执行的过程

  • 编写汇编源程序
    • 产生一个储存源程序的文本文件
  • 对源程序进行编译链接
    • 使用编译程序对源程序进行编译生成目标文件,再用链接工具对目标文件进行链接,生成可在操作系统中直接运行的可执行文件
  • 执行

源程序

伪命令

在汇编语言源程序中,包含两种命令,一种是汇编指令,一种是伪指令。

  • 汇编指令是有对应机械码的指令,可以被编译为机械指令,最终被CPU执行
  • 伪指令没有对应的机械码,最终不被CPU执行
    • 伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作
segment和ends

segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时必须要用的一对为指令。segment和ends的功能是定义一个段,一个段必须有一个名称来标识,格式为:

1
2
3
段名 segment
...
段名 ends

一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。一个程序中所有将被计算机所处理的信息:指令、数据、栈,被划分到不同的段中。

end

end是一个汇编程序的结束标记,如果碰到了伪指令end,就结束对 源程序 的编译。所以我们在写程序的时候,如果程序写完了,要在结尾出加上伪指令end。否则编译器无法知道何时结束。

注意,不要搞混end和ends,ends是和segment成对使用的,标记一个段的结束。而end是标记整个程序的结束。

assume

这个为指令含义为”假设”,它假设某一段寄存器和程序中的某一个用segment…ends定义的段相关联。通过assume说明这种关联,在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系。

如下就是将一个把一个叫做code的段和cs段寄存器联系起来:

1
2
3
4
5
assume cs:code

code segment
...
code ends

源程序中的”程序”

程序最先以汇编指令的形式存在源程序中,经编译、连接后转变为机械码,储存在可执行文件中

程序返回

一个程序结束后,将CPU的控制权交还给使它得以运行的程序,我们称这个过程为:程序返回。

在程序末尾使用两条指令可以实现程序返回:

1
2
mov ax,4c00H
int 21h

[BX]和loop命令

  • 用[address]表示一个内存单元时,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出,如mov al,[0]这个内存单元就是一字节
  • [bx]同样也表示一个内存单元,它的偏移地址在bx中
  • loop进行循环
  • 约定符号idata表示常量
  • 我们将使用符号”()”来表示一个寄存器或一个内存单元中的内容

[BX]

看看如下命令的功能

1
mov ax,[bx]

功能:bx中存放的数据作为一个偏移地址EA,段地址SA默认在ds中,将SA:EA中的数据送入ax,即:(ax)=((ds)*16+(bx))

值得注意的是,bx是寄存器,可以使用一些指令,如:inc bx。就可以方便的完成某些任务,如:把每个内存单元的内容变为1。

Loop指令

loop指令的格式是:loop标号,CPU执行loop指令的时候,要进行两部操作:1. (cx)=(cx)-1; 2. 判断cx中的值,不为零则跳转至标号处执行程序,否则向下执行。

可以看到cx中的值影响着loop指令的结果,通常我们在cx中存放循环次数

例:计算2^12。(N*2=N+N)

1
2
3
4
5
6
7
8
9
10
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00H
int 21h
code ends
end

执行loop时(cx)先减1,然后若(cx)不为0,则跳转到s处。从上面的例子可以总结出用cx和loop配合实现循环的3个要点:

    1. 在cx中存放循环次数
    1. loop指令中的标号所标识地址要在前面
    1. 要循环的程序段,要写在标号和loop指令中间

用cx和loop指令配合的框架如下:

1
2
3
4
    mov cx,循环次数
s:
循环执行的程序段
loop s

loop和[bx]的联合应用

若我们想要计算ffff:0~ffff:b单元中的元素的和,结果储存在dx中,分析:

    1. dx是16位的寄存器,内存单元不能直接加到dx中,因为如果直接加会自动区一个字的大小,不满足题意
    1. 不能用dl来做累加操作,因为dl大小不足以容纳所有内存单元的和导致进位丢失

所以我们需要引入一个16位寄存器ax作为中介,先把内存单元的内容送如al,在用ax和dx相加,通过bx和loop就可以完成。

段前缀

段地址默认在ds中,我们可以在访问的内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。如:mov ax,ds:[bx]

一段安全的空间

任意向一段内存空间写入内容是很危险的,因为这段内存空间可能存放着系统数据或代码。

包含多个段的程序

程序取得所需空间的方法有两种,一种是加载程序的时候为程序分配,另一种是程序在执行的过程中向系统申请。我们将介绍第一种。

我们若要一个程序在被加载的时候取得所需的空间,则必须要在源程序中做出说明。

在代码段中使用数据

考虑这样一个问题,编程计算一下8个数据的和,结果存在ax寄存器中:2134h、5342h、6563h、…。如何将这些数据储存在一组地址连续的内存单元中?又在哪找到这段内存空间?

我们可以在程序中,定义我们希望处理的数据,这些数据会被编译、连接程序作为程序的一部分写到可执行文件中。当可执行文件中的程序被加载入内存时,这些数据也会被加载如内存中。这些数据自然而然地获得了储存空间。

具体看下面的例子

1
2
3
4
5
6
7
8
9
assume cs:code

code segment
dw 3422,2346h,5643h,3422,2346h,5643h,3422h,3422

mov ax,2345h
some code...
code ends
end

程序开头”dw”的含义是定义字型数据(define word)。那这些个数据在哪里呢?由于它们在代码段中,程序在运行的时候CS中存放代码段的地址,所以可以从CS中的到他们的段地址。那它们的偏移地址是多少?因为用dw定义的数据处于代码最开始,所以偏远地址为0~E。

但是编译、连接成可执行文件后,在系统直接运行可能出现问题,因为在程序的入口处不是我们希望执行的指令(而是一些数据)。因此,我们可以在源程序中指明程序的入口所在:

1
2
3
4
5
6
7
8
9
10
assume cs:code

code segment
dw 3422,2346h,5643h,3422,2346h,5643h,3422h,3422

start: mov ax,2345h ; <---- 在程序的第一条指令的前面加上标号start

some code...
code ends
end start ; <----

在程序的第一条指令的前面加上标号start,这个标号在伪命令end的后面出现,用于通知编译器程序的入口在什么地方。

回顾可执行文件中的程序执行过程如下:

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

现在问题是,根据什么设置CPU的CS:IP指向程序的第一条要执行的指令?这一点,是由可执行文件中的描述信息指明的。可执行文件由描述信息和程序组成:程序来自源程序中的汇编指令和定义的数据;描述信息则主要是通过编译、连接程序对源程序中相关的伪指令进行处理所得到的信息。

在代码段中使用栈

我们首先要有一段可当作栈的空间,可在程序中通过定义数据来获取一段空间,然后将这段空间当作栈空间来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
assume cs:code

code segment
dw 3422,2346h,5643h,3422,2346h,5643h,3422h,3422
dw 0,0,0,0,0,0,0,0
;用dw定义8个字型数据,加载程序后将获得8个字的内存空间,这段空间可当作栈使用

start: mov ax,cs
mov ss,ax
mov sp,20h ;将栈顶ss:sp指向cs:20h

some code...
code ends
end

这段程序中定义了8个字型数据,它们的值都是0。这8个数据的值是多少对程序来说没有意义,只是用它们来开辟内存。

将数据、代码、栈放入不同的段

我们在编程的时候要主要何处是数据,何处是代码,何处是栈。这样显然就有两个问题:

  • 把它们放在一个段中使程序显得混乱
  • 一个段的容量是受限的,如果数据、栈和代码都放在一个段中,空间可能就不够

所以用多个段来存放数据、代码和栈。我们用定义代码段一样的方法定义多个段,然后在这些段里面定义需要的数据,或通过定义数据来获取栈空间。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code,ds:data,ss:stack

data segment
dw 3422,2346h,5643h,3422,2346h,5643h,3422h,3422
data ends

stack segment
dw 0,0,0,0,0,0,0,0
stack ends

code segment
start: mov ax,stack
mov ss,ax
mov sp,20h ;设置栈顶ss:sp指向stack:20
...
code ends

end start
  • 定义了多个段方法
    • 对于不同的段,要用不同的段名
  • 对段地址引用
    • 段名就相当于一个符号,它代表这段地址

更灵活的定位内存地址的方法

and和or指令

  • and指令:逻辑与指令,按位进行运算
    • 通过该指令可将操作对象相应位设备0,其他位不变
  • or指令:逻辑或指令,按位进行运算
    • 通过该指令可将操作对象相应位设备1,其他位不变

以字符形式给出的数据

可以在汇编程序中,用’…’的方式指明数据是以字符的形式给出的,编译器将它们转化成对应的ASCII码。如

1
2
3
4
5
assume cs:code
data segment
db 'unIX' ;; db是define binary,用一个字节储存,与dw同理
data ends
end

“db ‘unIX’”就相当于”db 75H, 6EH, 49H, 58H”

大小写转换的问题

常规的解法是ASCII码加上或减去一个数,但是我们还没说到条件控制,该怎么办呢?寻找新的规律可以看到,就ASCII码的二进制形式来看,除了第5位外,大写字母和小写字母的其他位都一样。因此我们要了新的方法:将第5位变为0或1就能改变成大小写了。

用[bx+idata]的方式进行数组的处理

我们知道在C语言中,数组实际上就是一段连续的内存空间。假设第一个数长5个字节,第二个数组有5个字节:

1
2
3
4
mov al,[bx]  ;; 定位第一个数组
mov al,[5+bx] ;; 定位第二个数组
;;或写成
mov al,5[bx]

和C语言对比:

  • C语言
    • a[i], b[i]
  • 汇编语言
    • 0[bx], 5[bx]

SI和DI

si和di是8086CPU中和bx功能近似的寄存器,si和di不能分成两个8位寄存器来使用。

我们可以灵活使用[bx+si+idata]和[bx+di+idata]来表示一个内存单元。

-对于[bx+si]和[bx+di],有指令mov ax,[bx+si]等,该指令也可以写出如下格式(常用)
- mov ax,[bx][si]
-对于[bx+si+idata]和[bx+di+idata],有指令mov ax,[bx+si+200]等,该指令也可以写出如下格式(常用)
- mov ax,200[bx][si]
- mov ax,[bx][si].200
- mov ax,[bx].200[si]

数据处理的两个基本问题

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

我们定义两个描述性符号:reg和sreg。reg表示寄存器;sreg表示段寄存器。

bx、si、di和bp

前面三个寄存器已经讲过,现在来进行一下总结:

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

汇编语言中数据位置的表达

汇编语言中用3个概念来表达数据的位置:

  • 立即数(idata)
    • 对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中),在汇编语言中称为:立即数,在汇编指令中直接给出
  • 寄存器
    • 指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名
  • 段寄存器(SA)和偏移地址(EA)
    • 指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中(如ds)

指令要处理的数据有多长

8086CPU的指令,可以处理两种尺寸的数据:byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。汇编语言用以下方法处理:

  • 根据寄存器名指明要处理的数据的尺寸
  • 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte
    • 如:mov word ptr ds:[0],1
  • 其他方法
    • 这些指令默认了访问的是字单元还是字节单元,如push [1000H]push指令只进行字操作

div指令

div是除法指令,使用div做除法时应注意以下问题:

  • 除数:有8位和16位两种,在一个reg或内存单元中
  • 被除数:默认放在AX或DX和AX中
    • 如果除数是8位,被除数则为16位,默认在AX中存放
    • 如果除数是16位,被除数则为32位,默认在DX和AX中存放,DX存放高16位,AX存放低16位
  • 结果
    • 如果除数为8位,则AL储存除法操作的商,AH储存除法操作的余数
    • 如果除数为16位,则AX储存除法操作的商,DX储存除法操作的余数

格式如下:

1
2
div reg
div 内存单元

例:计算100001/100

被除数100001远大于65535,所以只能用dx和ax两个寄存器联合存放100001(32位)。除数小于255,可以在一个8位寄存器中存放。但是因为被除数是32位的,除数应该为16位,所以用一个16位的寄存器储存100.

1
2
3
4
mov dx,1
mov ax,86A1H
mov bx,100
div bx

伪指令dd

dd是用来定义dword(double word,双字)类型数据的

dup

dup是一个操作符,在汇编语言中和db、dw、dd等一样,也是由编译器识别处理的符号。它是和db、dw、dd等数据定义伪指令配合使用的,用来进行数据的重复。如:

1
db 3 dup (0,1,2)

定义了9个字节,相当于db 0,1,2,0,1,2,0,1,2

dup的使用格式如下:

  • db 重复的次数 dup (重复的字节型数据)
  • dw 重复的次数 dup (重复的字型数据)
  • dd 重复的次数 dup (重复的双字型数据)

转移指令的原理

可以修改IP,或同时修改CS和IP的指令统称为转移指令

8086CPU的转移指令行为有以下几类:

  • 只修改IP时,称为段内转移,如:jmp ax
  • 同时修改CS和IP时,称为段间转移,如:jmp 1000:0

操作符offset

offset是由编译器处理的符号,它的功能是取得标号的偏移地址。如:

1
2
3
4
5
6
assume cs:codesg
codesg segment
start:mov ax,offset start ;; 相当于mov ax,0
s:mov ax,offset s ;; 相当于mov ax,3
codesg ends
end start

上面的程序中,offset操作符取得了标号start和s的偏移地址0和3。

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

jmp short 标号转到标号处执行指令

这种格式的jmp指令实现的是段内短转移,它对IP修改的范围为-128~127,即最多可以向前跨越128个字节,向后127个字节。指令中的”标号”是代码段中的标号,指明了指令要转移的目的地,跳转指令结束后,CD:IP应该指向标号处的指令。

1
2
3
4
5
6
7
8
assume cs:codesg
codesg segment
start:mov mov ax, 0
jmp short s
add ax, 1
s:inc ax
codesg ends
end start

上面的程序就跳过了add ax, 1

jmp short依据位移进行转移,也就是说CPU执行jmp指令的时候并不需要转移的目的地址,只需要转移的位移,编译器后计算jmp到标号的位移。

jmp short 标号的功能为:(IP)=(IP)+8位位移

  • 8位位移=标号处的地址-jmp指令后的第一个字节的地址
  • short指明此处的位移为8位位移
  • 8位位移的范围为-127~128,用补码表示
  • 8位位移由编译程序在编译时算出

还有一种与jmp short功能类似的指令,jmp near ptr 标号,它实现的是段内转移,功能为:(IP)=(IP)+16

  • 17位位移=标号处的地址-jmp指令后的第一个字节的地址
  • near ptr指明此处的位移为16位位移
  • 16位位移的范围为-32768~32767,用补码表示
  • 16位位移由编译程序在编译时算出

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

jmp far ptr 标号实现的是段间转移,又称远转移,功能如下:

  • (CS)=标号所在的段地址;(IP)=标号所在段中的偏移地址。
  • far ptr指明了指令用标号的段地址和偏移地址修改CS和IP

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

转移地址在内存中的jmp指令有两种格式:

  • jmp word ptr 内存单元地址(段内地址)
    • 功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址
  • jmp dword ptr 内存单元地址(段内地址)
    • 功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处的转移的目的偏移地址
    • (CS)=(内存单元地址+2)
    • (IP)=(内存单元地址)

jcxz指令

jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,在对应的机器码中包含转移的位移,而不是转移的目的地址。对IP的修改范围都是:-127~128

指令格式:jcxz 标号如果(cx)=0,则转移到标号处执行

  • 操作:当(cx)=0时,(IP)=(IP)+8位位移
    • 8位位移=标号处的地址-jcxz指令后的第一个字节的地址
    • 8位位移的范围为-127~128,用补码表示
    • 8位位移由编译程序在编译时算出
  • 当(cx)!=0时,什么也不做,程序向下执行

用C语言的话说,jcxz 标号的功能相当于:

1
if((cx)==0)jmp short 标号;

loop指令

所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是转移的目的地址。对IP的修改范围都是:-127~128

指令格式:loop 标号(cx)=(cx)-1,如果(cx)!=0,则转移到标号处执行

  • 操作:(cx)=(cx)-1;如果(cx)!=0,(IP)=(IP)+8位位移
    • 8位位移=标号处的地址-jcxz指令后的第一个字节的地址
    • 8位位移的范围为-127~128,用补码表示
    • 8位位移由编译程序在编译时算出
  • 当(cx)=0时,什么也不做,程序向下执行

用C语言的话说,loop 标号的功能相当于:

1
2
(cx)--;
if((cx)!=0)jmp short 标号;

根据位移转移的意义

这种设计方便了程序段在内存中的浮动配置。这段程序在内存中的不同位置都可以正确执行,只需要位移,而不需要具体的地址。因为当指令不存在具体地址处时,程序执行就会出错。

CALL和RET指令

call和ret指令都是转移指令,它们修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序(函数)的设计

ret和retf

  • ret指令用栈中的数据修改IP的内容,从而实现转移。执行ret指令时,进行下面两步操作:
      1. (IP)=((ss)*16+(sp))
        • 出栈
      1. (sp)=(sp)+2
    • 相当于pop IP
  • retf指令用栈中的数据修改cs和ip的内容,从而实现转移。执行retf指令时,进行下面4步操作:
      1. (IP)=((ss)*16+(sp))
      1. (sp)=(sp)+2
      1. (CS)=((ss)*16+(sp))
      1. (sp)=(sp)+2
    • 相当于pop IP; pop CS

call指令

CPU执行call指令时,进行两步操作:

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

call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令了原理相同

一下介绍call指令的主要应用格式

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

call 标号,将当前的IP压入栈后,转到标号处执行指令

CPU执行此种格式的call指令时,进行的操作如下:

    1. (sp)=(sp)-2
      ((ss)*16+(sp))=(IP)
      • 入栈,栈顶指针上移
    1. (IP)=(IP)+16位位移
      • 根据标号
  • 相当于:push IP;jmp near ptr 标号

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

call far ptr 标号,实现的是段间转移,CPU执行这种格式的call指令时,操作如下:

    1. (sp)=(sp)-2
      ((ss)*16+(sp))=(CS)
      (sp)=(sp)-2
      ((ss)*16+(sp))=(IP)
      • 入栈
    1. (CS)=标号所在段的段地址
      (IP)=标号所在段中的偏移地址
  • 相当于
    push CS
    push IP
    jmp far ptr 标号

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

格式:call 16位reg。功能:

  • (sp)=(sp)-2
    ((ss)*16+(sp))=(IP)
    (IP)=(16位reg)
  • 相当于:
    push IP
    jmp 16位reg

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

转移地址在内存中的call指令有两种格式:

  • call word ptr 内存单元地址
    • 相当于:
      push IP
      jmp word ptr 内存单元地址
  • call dword ptr 内存单元地址
    • 相当于:
      push CS
      push IP
      jmp dword ptr 内存单元地址

call和ret的配合使用

我们可以写一个具有一定功能的程序段,我们称之为子程序,在需要的时候用call指令转去执行。执行完后用ret指令,用栈中的数据设置IP值,从而跳到call的下一条指令继续执行。

这样,我们可以利用call和ret来实现子程序的机制。子程序的框架如下:

1
2
3
标号:
指令
ret

具有子程序的源程序的框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume cs:code
code segment
main:
:
call sub1 ;;调用子程序1
:
:
mov ax,4c00h
int 21h

sub1:
:
call sub2
:
:
ret

sub2:
:
:
ret
code ends
end main

使用call和ret可以实现模块化设计

mul指令

mul是乘法指令,使用mul时注意以下两点:

    1. 两个相乘的数:两个数要么都是8位,要么都是16位
      • 如果是8位乘法,一个默认放在AL中,另一个放在8位reg或内存字节单元中
      • 如果是16位乘法,一个默认在AX中,另一个放在16为reg或内存字单元中
    1. 结果:
      • 8位乘法结果默认放在AX中
      • 16位乘法结果高位默认放在DX中,低位放在AX中

格式如下:

1
2
3
4
5
6
mul reg
mul 内存单元

;; 内存单元可以用不同的寻址方式给出:
mul byte ptr ds:[0]
mul word ptr [bx+si+8]

参数和结果传递的问题

既然知道了如何调用子程序,那应该如何储存子程序需要的参数和产生的返回值呢?

  • 显然可以用寄存器来存。

那么如果有N个参数和结果,寄存器的个数是有限的,该怎么存放呢?

  • 这个时候没将批量的数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给子程序。对于批量的结果也如此的方法。

看下面一个例子: 设计一个子程序,将一个全是字母的字符串转换为大写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
assume cs:code

data segment
db 'conversation'
data ends

code segment
start: mov ax,data
mov ds,ax
mov si,0 ;ds:si指向字符串所在空间的首地址
mov cx,12 ;cx存放字符串的长度
call capital ;调用子程序,就像调用函数

mov ax,4c00h
int 21h

capital:and byte ptr [si],11011111b
inc si
loop capital
ret
code ends
end start

除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数

用栈来传递参数

由调用者将要传递给子程序的参数压入栈中,子程序从栈中取得参数。

  • 编写一个函数:计算(a-b)^3,a、b为字型数据,参数为a、b
  • 栈顶存放IP、后面依次是a、b(注意参数顺序)
1
2
3
4
5
6
7
8
9
func:   push bp         ;子程序用到寄存器bp,先保存原来的值
mov bp,sp
mov ax,[bp+4] ;将栈中a送入ax
sub ax,[bp+6] ;减栈中b的值
mov bp,ax
mul bp
mul bp
pop bp
ret 4

指令ret 4的含义用汇编语法描述为:

1
2
pop ip      ;即返回ip,并使将栈顶指针该为调用前的值。应为这个例子的参数是两个字,所以是4.
add sp,n

看一下对这个函数是如何调用的,设a=3、b=1

1
2
3
4
5
mob ax,1
push ax
mov ax,3 ;注意顺序
push ax
call func
低地址单元
BP
IP
3 [bp+4]
1 [bp+6]
高地址单元

寄存器冲突的问题

问题在于:子程序中的寄存器,很可能在组程序中也要使用,造成寄存器使用上的冲突

我们希望:

  • 编写调用子程序的时候不必关心子程序到底使用了那些寄存器
  • 编写子程序的时候不必关心调用者使用了哪些寄存器
  • 不会发生寄存器冲突

解决这个问题的间捷方法是,在子程序的开始将子程序中所有用到的内容保存起来,在子程序返回前恢复。可以用栈来保存寄存器中的内容

以后我们编写子程序的标准框架如下:

1
2
3
4
子程序开始: 子程序使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret、retf)

看下面一个例子,将一个全是字母,以0结尾的字符串转换为大写的子程序

1
2
3
4
5
6
7
8
9
10
11
12
13
capital:    push cx  ;使用的寄存器入栈
push si

change: mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change

ok: pop si ;使用的寄存器出栈
pop cx
ret

标志寄存器

在CPU内部的寄存器中,有一种特殊的寄存器(不同的CPU,个数结构可能不同)具有以下3种作用:

  • 用来存储相关指令的某些执行结果
  • 用来为CPU执行相关指令提供行为依据
  • 用来控制CPu的相关工作方式

这种特殊的寄存器在8086CPU中,被称为 标志寄存器 。以下称为flag寄存器。

8086CPU的flag寄存器的结构如下:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
OF DF IF TF SF ZF AF PF CF

ZF标志

flag的第6位是ZF,零标志位。它记录相关指令执行后其结果是否为0。如果结果为0,则zf=1;否则zf=0。如:

1
2
3
4
mov ax,1
sub ax,1

;zf=1

在8086CPU的指令集中,有的指令执行是影响标志位寄存器的,如:add、sub、mul、div、and、or等,它们大多都是运算指令;有的指令是不影响的,如mov、push、pop等,它们大多都是传送指令。

PF标志

flag的第2位是PF,奇偶标志位。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。如果1的个数是偶数,pf=1。否则pf=0。如:

1
2
3
4
mov al,1
add al,10

;结果是00001011b,有奇数个1,pf=0

SF标志

flag的第7位是SF,符号标志位。它记录相关指令执行后,其结果(对于有符号数)是否为负。如果结果为负,sf=1,否则sf=0。

计算机通常使用补码来表示有符号数,对于无符号数SF的值没有意义,虽然相关指令影响了它的值。

CF标志

flag的第0位是CF,进位标志。一般情况下,进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。

对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N-1位,就是它的最高有效位。

OF标志

由于进行有符号数运算的时候,可能发生溢出造成结果错误。则CPU需要对指令执行后产生的溢出进行记录。

flag的第11位是OF,溢出标志。一般情况下,OF记录了有符号数运算的结果是否溢出了。如果溢出,OF=1,否则OF=0。

注意CF和OF的区别:CF是对无符号数有意义的标志位,OF是对无符号数有意义的标志位。

adc指令

adc是带进位加法指令,它利用了CF上记录的进位值。

  • 指令格式:adc 操作对象1 操作对象2
  • 功能:操作对象1 = 操作对象1 + 操作对象2 + CF
1
2
3
4
mov ax,2
mov bx,1
sub bx,ax ;1-2借位,CF=1
adc ax,1 ;执行后(ax)=4=(ax)+1+CF

在执行adc指令的时候加上的CF的值的含义,由adc指令前的指令决定的。

CPU提供adc指令是有目的的,就是来进行加法的第二步运算。

例:计算1EF0001000H+2010001EF0H,结果放在ax(最高16位),bx(次高16位),cx(低16位)中。

1
2
3
4
5
6
mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
adc bx,1000H
adc ax,0020H

sbb指令

sbb是带借位减法指令,它利用CF位上记录的借位值。应用思路类似adc

cmp指令

cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果,而是cmp指令执行后对标志寄存器产生影响。

  • cmp指令格式:cmp 操作对象1 操作对象2
  • 功能:操作对象1-操作对象2但不保留结果,仅仅根据结算结果来对标志寄存器进行设置
  • 如,cmp ax,ax,结果为0,那么标志寄存器:zf=1,pf=1,sf=0,cf=0,of=0。

cmp的使用非常灵活,如要判断两个数相减的结果是不是负数:cmp将结果在flag中记录,通过判断sf(正负)和of(溢出)就可得知逻辑上真正结果的正负。

检测比较结果的条件转移指令

除了jcxz指令之外,CPU还提供了其他条件转移指令,大多数条件转移指令通过检测相关的标志位,根据检测的结果修改IP。通常和cmp配合使用。

下面是常用的条件转移指令

指令 含义 检测的相关标志位
je 等于则转移 zf=1
jne 不等于则转移 zf=0
jb 低于则转移 cf=1
jnb 不低于则转移 cf=0
ja 高于则转移 cf=0且zf=0
jna 不高于则转移 cf=1或zf=1

DF标志和串传送指令

flag的第10位是DF,方向标志位。在串处理指令中,控制每次操作后si、di的增减。

  • df=0,每次操作后si、di递增
  • df=1,每次操作后si、di递减

看下面一个串传送指令:

  • 格式:movsb
  • 功能:将ds:si指向的内存单元中的字节送入es:di中。执行movsb指令相当于进行下面几步操作
      1. ((es)*16+(di))=((ds)*16+(si))
      1. 如果df=0,则(si)=(si)+1;(di)=(di)+1
      1. 如果df=1,则(si)=(si)-1;(di)=(di)-1
  • 相当于:
    mov es:[di], byte ptr ds:[si]
    如果df=0:inc si;inc di
    如果df=1:dec si;dec di
  • 传送一个字的指令是movsw

movsb和movsw进行的是串传送操作中的一个步骤,一般来说movsb和mobsw都是配合rep使用的,个是如下

  • rep movsb
    • 类似于:
      s:movsb
      loop s
  • rep的作用是根据cx的值,重复后面的串传送指令

由于df位决定着串传送指令执行后,si和di是递增还是递减,所以CPU应提供对df进行设置的操作。在8086CPU中:

  • cld指令:将标志位寄存器的df设置为0
  • std指令:将标志位寄存器的df设置为1

pushf和popf

pushf的功能是将标志寄存器的值压入栈中,而popf是从栈中弹出数据,送入标志寄存器中。这也是为什么前面讲标志寄存器结构时,强调什么是第几位的原因。

pushf和popf,为直接访问标志寄存器提供了一种方法。

内中断

任何一个通用CPU都具备一种能力:在执行完当前正在执行的指令之后,检测到从CPU外发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种信息称为中断信息。中断的意思是指,CPU不再接着向下执行,而是转去处理这个特殊的信息。

内中断的产生

当CPU内部有什么事情发生时会马上处理中断信息呢?对于8086CPU,有以下情况发生时,将产生中断信息:

  • 除法错误
  • 单步执行
  • 执行into指令
  • 执行int指令

不同的信息需要不同的处理方式。中断信息中包含识别来源的编码,8086CPU用称为中断类型码来识别信息的来源。中断类型码为一个字节,可以表示256种中断信息来源。在8086CPU中:

  • 除法错误:0
  • 单步执行:1
  • 执行into指令:4
  • 执行int指令:该指令的格式为int n,n是立即数,是提供给CPU的中断类型码

中断处理程序

CPU收到中断信息后,需要对信息进行处理。如何处理可以由我们编程决定。我们所编写的中断信息处理程序称为中断处理程序。

CPU在收到中断信息后,应该转去(改变CS:IP指向)对应的中断处理程序中。中断类型码就是用来定位中断处理程序的。

中断向量表

如何根据8位的中断类型码得到中断处理程序的段地址和偏移地址呢?

  • CPU通过中断向量表找到相应的中断处理程序入口地址。中断向量表就是中断处理程序入口地址的列表。

CPU如何找到中断向量表?

  • 中断向量表在内存中存放,在8086CPU中,中断向量表指定放在内存地址0处。从内存0000:0000到0000:03FF的1024个内存单元中存放着中断向量表。

中断过程

CPU在执行完中断处理程序后,应该返回原来的执行点继续执行下面的指令。所以在中断过程中,在设置CS:IP之前,还要将原来的CS和IP的值保存起来。与call指令调用子程序同理。

下面是8086CPU收到中断信息后,所引发中断的过程:

    1. 获取中断类型码
    1. 标志寄存器入栈
    1. 设置标志寄存器的第8位TF和第9位IF值为0,因为这也两个标志寄存器也可以是触发中断的因素
    1. CS内容入栈
    1. IP内容入栈
    1. 从内存地址为中断类型码*4中断类型码*4+2的两个字单元中读取中断处理程序的入口地址设置IP和CS

中断处理程序和iret指令

由于CPU随时可能检测到中断信息,所以中断处理程序必须一直储存来内存的某段空间之中,而中断向量必须存储在对应的中断向量表项中。

中断处理程序的编写方法和子程序比较相似,步骤如下:

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

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

1
2
3
pop IP
pop CS
popf

编程处理0号中断

当除法溢出的时候,产生0号中断信息,从而引发中断过程。此时,CPU将进行如下工作:

    1. 获取中断类型码0
    1. 标志寄存器入栈,TF、IF设置为0
    1. CS、IP入栈
    1. (IP)=(0*4),(CS)=(0*4+2)

那么现在的问题是,中断处理程序(我们设为do0)应该放在哪里?

  • 我们需要找到一块别的程序不会用到的内存区,将do0送入其中
  • 前面说过,8086支持256个中断,但实际中并不会用到那么多。一般情况下,从0000:0200至0000:02FF的256个字节所对应的中断向量表项是空的,操作系统和其他程序都不会占用,所以我们可以使用这段空间

程序框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code

code segment

start: do0安装程序
设置中断向量表
mov ax,4c00h
int 21h

do0: 一些操作如:显示字符串"overflow!"
mov ax,4c00h
int 21h

code ends
end start

可以看到,上面的程序分为两部分:

  • 安装do0,设置中断向量的程序
      1. 将do0的代码复制到内存0:200处
      1. 设置中断向量表,将do0的入口地址保存到0号表项中
      1. 返回
  • do0

安装

可以使用movsb指令,将do0的代码送入0:200处。程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
assume cs:code

code segment

start: mov ax,cs
mov ds,ax
mov si,offset do0 ;设置ds:si指向源地址

mov ax,0
mov es,ax
mov di,200h ;设置es:di指向目的地址

mov cx,offset do0end-offset do0 ;设置cs为传输长度,可利用编译器计算do0长度

cld ;设置传输方向为正向
rep movsb

设置中断向量表
mov ax,4c00h
int 21h

do0: 一些操作如:显示字符串"overflow!"
mov ax,4c00h
int 21h

do0end: nop

code ends
end start

do0

do0程序的主要任务是显示字符串,如下

需要注意的是:

  • 因为do0程序随时可能被执行,而它要用到的字符串”overflow!”,所以该字符串也应该放在一段不会被覆盖的空间中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
assume cs:code

code segment

start: mov ax,cs
mov ds,ax
mov si,offset do0 ;设置ds:si指向源地址

mov ax,0
mov es,ax
mov di,200h ;设置es:di指向目的地址

mov cx,offset do0end-offset do0 ;设置cs为传输长度,可利用编译器计算do0长度

cld ;设置传输方向为正向
rep movsb

设置中断向量表
mov ax,4c00h
int 21h

do0: jmp short do0start
db "overflow!"

do0start:mov ax,cs
mov ds,ax
mov si,202h ;设置ds:si指向字符串,因为do0主程序第一跳指令是跳转,占用两个字节,所以字符串的地址是202h

mov ax,0b800h
mov es,ax
mov di,12*160+36*2 ;设置es:di指向显存空间的中间位置

mov cx,9 ;设置字符串长度
s: al,[si]
mov es:[di],al
inc si
add di,2
loop s

mov ax,4c00h
int 21h

do0end: nop

code ends
end start

设置中断向量

0号表项的地址为0:0,其中0:0字单元存放偏移地址,0:2字单元存放段地址。程序如下:

1
2
3
4
mov ax,0
mov es,ax
mov word ptr es:[0\*4],200h
mov word ptr es:[0\*4+2],0

单步中断

基本上,CPU在执行一条指令之后,如果检查到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的类型码为1,它引发的中断过程如下:

    1. 取得中断类型码
    1. 标志寄存器入栈,TF、IF设置为0
    1. CS、IP入栈
    1. (IP)=(1*4), (CS)=(1*4+2)

CPU为什么要提供这样的功能呢?

  • 我们在debug的时候CPU执行一条指令后就显示各个寄存器的状态,然后等待输入

当TF=1时,CPU在执行完一条指令后将引发单步中断,转去执行中断处理程序。 注意,中断处理程序也是由一条条指令组成的 ,如果在执行中断处理程序之前,TF=1,则CPU执行完终端处理程序的第一条指令后,有产生单步中断,就这样死循环下去。

所以在进入中断处理程序之前,设置TF=0.从而避免CPU在执行中断处理程序的时候发生单步中断。

响应中断的特殊情况

一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就响应中断,引发中断过程。可在某些情况,即便是发生了中断,也不会响应。用一个例子说明:

在执行完向ss寄存器传送数据的指令后,即便发生中断,CPU也不会响应。因为ss:sp联合指向栈顶,对它们的设置应该连续完成。如果在执行完设置ss的指令后,CPU响应中断,引发中断过程,要在栈中压入标志寄存器、CS和IP的值。而ss改变,sp没变,ss:sp不能指向正确的栈顶,引起错误。

int指令

中断信息可以来自CPU的内部和外部,当CPU内部需要处理的事情发生时,将产生需要马上处理的中断信息,引发中断过程。

接下来将介绍另一种重要的内中断,由int指令引发的中断。

int指令

int指令的格式为:int n,n为中断类型码,它的功能是引发中断过程。

CPU执行int n指令,相当于引发一个n号中断的中断过程,执行过程如下:

    1. 取中断类型码n
    1. 标志寄存器入栈,IF=0,TF=0
    1. CS、IP入栈
    1. (IP)=(n*4),(CS)=(n*4+2)
      • 跳转去n号中断处理程序

编写提供应用程序调用的中断例程,与中断处理类似:

  • 编写功能程序
  • 安装程序到内存中
  • 设置中断向量,将程序的入口地址保存到对应的表项中

对int、iret和栈的深入理解

问题:编写名为7ch的中断例程来完成loop指令的功能

分析:loop s的执行需要两个信息,循环的次数和到s的位移

例:屏幕中间显示80个”!”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code
code segment

start :mov ax,0b800h
mov es,ax
mov di,160*12

mov bx,offset s-offset se ;设置从标号se到标号s的转移位移
mov cx,80
s :mov byte ptr es:[di],'!'
add di,2
int 7ch ;如果(cx)!=0,转移到s处
se :nop

mov ax,4c00h
int 21h

code ends
end start

7ch中断例程如下:

  • int 7ch引发中断后,在中断过程中将当前的标志寄存器、CS和IP入栈
  • 通过修改栈中的CS和IP就能让程序返回标号s所在的位置
    • 用标号se的偏移地址加上bx中存放的转移位移就可以得到标号s的偏移地址
    • 如果7ch和主程序在同一段中,则栈中的段寄存器CS就不用修改
1
2
3
4
5
6
7
  7ch   :push bp                        
mov bp,sp
dec cx
jcxz lpret ;如果cx等于0则不修改偏移地址,直接返回
add [bp+2],bx ;[bp+2]处是IP的内容,栈顶处是bp的内容,下面是se的偏移地址
lpret :pop bp
iret

BIOS和DOS所提供的中断例程

在系统板的ROM中存放着一套程序,称为BIOS(基本输入输出系统),主要包含以下几个部分:

  • 硬件系统的检测和初始化程序
  • 外部中断和内部中断的中断例程
  • 用于对硬件设备进行I/O操作的中断例程
  • 其他和硬件系统相关的中断例程

端口

各种储存器都和CPU的地址线、数据线、控制线相连。CPU在操控它们的时候,把它们当作内存来对待,把它们总地看做一个由若干储存单元组成的逻辑储存器。

PC机的芯片中,都有一组可以由CPU读写的寄存器,这些寄存器在物理上可能处于不同的芯片中,但是它们都在以下两点上相同:

  • 都和CPU的总线相连
  • CPU对它们进行读写的时候都通过控制总线向它们所在的芯片发出端口读写命令

从CPU的角度,将这些寄存器都当作端口,对它们进行统一编址,从而建立一个统一的端口地址空间。每个端口在地址空间中都有一个地址

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

  • CPU内部的寄存器
  • 内存单元
  • 端口

以下讨论端口的读写

端口的读写

在访问端口时,CPU通过端口地址来定位端口。因为端口所在的芯片和CPU通过总线相连,所以,端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口。则端口地址的范围是0~65535

对端口的读写不能用mov、push、pop等内存读写指令。端口的读写指令只有两条:in和out

  • in表示从端口读取数据
  • out表示往端口写入数据

访问端口

1
in al,60h       ;从60h号端口读入一个字节

执行时与总线相关的操作如下:

  • CPU通过地址线将地址信息60h发出
  • CPU通过控制线发出端口读命令,选中端口所在的芯片,并通知它,将要从中读取数据
  • 端口所在的芯片将60h端口中的数据通过数据线送入CPU

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

对0~255以内的端口进行读写:

1
2
in al,20h   
out 20h,al

对256~65535的端口进行读写时,端口号放在dx中:

1
2
3
mov dx,3f8h
in al,dx
out dx,al

CMOS RAM芯片

PC机中,有一个CMOS RAM芯片,一般简称为CMOS。有如下特征:

  • 包含一个实时钟和一个128个储存单元的RAM储存器
  • 该芯片靠电池供电。所以关机后内部实时钟仍可正常工作,RAM中信息不会丢失
  • 128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元保存系统配置信息
  • 该芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口来读写CMOS RAM
  • 70h为地址端口,存放要访问的CMOS RAM单元地址;71h为数据端口,存放从选定的CMOS RAM单元中读取的数据或要写入其中的数据,对CMOS操作时读写分为两步:如读CMOS RAM的2号单元
      1. 将2送入段偶70h
      1. 从端口71h读出2号单元的内容

shl和shr指令

shl和shr是逻辑位移指令,shl是逻辑左移,shr是逻辑右移

以shl为例,它的功能为:

  • 将一个寄存器或内存单元中的数据向左位移
  • 将最后移出的一位写入CF中
  • 最低位用0补充
1
2
3
mov al,01001000b
shl al,1 ;将al数据左移一位
;结果al=10010000b, CF=0

如果位移数大于1时,必须将位移数放在cl中

1
2
3
4
mov al,01001000b
mov cl,2
shl al,cl ;将al数据左移2位
;结果al=00100000b, CF=1

逻辑左移相当于执行X=X*2

shr是逻辑右移,和shl的操作相反

CMOS RAM中存储的时间信息

在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息的长度为1个字节。这些数据以BCD码的方式存放。

存放单元 0 2 4 7 8 9
内容

CMOS RAM储存时间信息的单元中,储存了用两个BCD码表示的两位十进制数,高4位的BCD码表示十位,低4位的BCD码表示个位。如00010100b表示14。

实战 :

编程:在屏幕中显示当前的月份

分析:这个程序主要做一下两个部分工作:

  • 从CMOS RAM的8号单元中读出当前月的BCD码
      1. 向地址端口70h写入要访问的单元的地址
        mov al,8; out 70h,al
      1. 从数据端口71h中取得指定单元中的数据
        in al,71h
  • 将用BCD码表示的月份用十进制的形式显示
    • 因为BCD码值=十进制码值,则BCD码值+30h=十进制对应的ASCII码
    • 从CMOS RAM的8号内存单元读出的一个字节中,包含了用两个BCD码表示的两位十进制数,高4位为十为,低4位为个位
      • 取出这两个BCD码
        mov ah,al
        cl,4 移位数
        shr ah,cl ah中为月份的十位
        and al,00001111b al中为月份的个位数
      • 显示(ah)+30h和(al)+30h对应的ASCII码字符

完整程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
assume cs:code
code segment
start: mov al,8
out 70h,al
in al,71h

mov ah,al
mov cl,4
shr ah,cl
and al,00001111b

add ah,30h
add al,30h

mov bx,0b800h
mov es,bx
mov byte ptr es:[160*12+40*2],ah ;显示月份的十位数码
mov byte ptr es:[160*12+40*2+2],al ;接着显示月份的个位数码

mov ax,4c00h
int 21h

code ends
end start

外中断

要及时处理外设的输入,显然需要解决两个问题:

  • 外设的输入随时可能发生,CPU如何得知?
  • CPU从何处得到外设的输入?

接口芯片和端口

外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。CPU还可以向外设输出控制命令,而这些命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设设施控制。

外中断信息

外设的输入随时可能发生,CPU如何得知?

  • 当CPU外部需要处理的事情发生时,相关的芯片将向CPU发出相应的中断信息,引发中断过程。

在PC系统中,外中断源一共有以下两类:

  • 可屏蔽中断
    • 可屏蔽中断是CPU可以不响应的外中断
    • CPU是否响应可屏蔽中断看标志寄存器IF位的设置,IF=1则响应
    • sti设置IF=1
    • cti设置IF=0
  • 不可屏蔽中断
    • 不可屏蔽中断的中断类型码固定为2

PC机键盘的处理过程

    1. 键盘输入
      • 键盘上每个键相当于一个开关,键盘中有一个芯片对键盘上每个键的开关状态进行扫描
      • 按下一个键,开关接通,芯片产生一个能说明按下键的位置的扫描码。扫描码送入主板上相关接口芯片的寄存器中,该寄存器的端口地址为60h
      • 松开按键时也会产生扫描码,也被送入60h端口
      • 一般称按下产生的扫描码为通码,松开产生的扫描码为断码,通码的第7位为0,断码的第7位为1,即:
        • 断码=通码+80h
    1. 引发9号中断
      • 键盘输入到达60h端口时,相关的芯片就会向CPU发出中断类型为9的可屏蔽中断信息
    1. 执行int 9中断例程
      • BIOS提供了int 9中断例程,用来进行基本的键盘输入处理

编写int 9中断例程

键盘输入的处理过程:

    1. 键盘产生扫描码
    1. 扫描码送入60h端口
    1. 引发9号中断
    1. CPU执行int 9中断例程处理键盘输入
      • 从端口60h读入输入:in al,60h
      • 调用BIOS的int 9中断例程

BIOS提供的int 9中断例程已经对一些硬件细节进行了处理,我们只要在自己编写的中断例程中调用BIOS的int 9中断例程就可以自定义操作了。

编程:在屏幕中间依次显示a~z,按下Esc后改变显示的颜色

  • 首先为了能够看清,应该在显示一个字母后延时一段时间
  • 将我们自己写的9号中断写入向量表,同时保存BIOS的int 9中断例程,以便之后调用
    • 这里将原来的int 9中断例程的偏移地址和段地址保存在ds:[0]和ds:[2]单元中
  • 模拟int来实现对我们写的新中断例程进行调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
assume cs:code

stack segment
db 128 dup (0)
stack ends

data segment
dw 0,0
data ends

code segment
start: mov ax,stack
mov ss,ax
mov sp,128

mov ax,data
mov ds,ax

mov ax,0
mov es,ax

push es:[9*4]
pop ds:[0]
push es:[9*4+2]
pop ds:[2] ;将原来的int 9中断例程的入口地址保存在ds:0和ds:2单元中

mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2] ;显示
call delay
inc ah
cmp ah,'z'
jna s

mov ax,0
mov es,ax

push ds:[0]
pop es:[9*4]
push ds:[2]
pop es:[9*4+2] ;将中断向量表中的int 9中断例程的入口地址恢复为原来的地址

mov ax,4c00h
int 21h

delay: push ax
push dx
mov dx,1000h
mov ax,0
s1: sub ax,1
sbb dx,0
cmp ax,0
jne s1
cmp dx,0
jne s1
pop dx
pop ax
ret
; -------新int 9中断例程--------
int9: push ax
push bx
push es

in al,60h

pushf
pushf
pop bx
and bh,111111100b
push bx
popf
call dword ptr ds:[0] ;对int指令进行模拟,调用原来的int 9中断例程

cmp al,1
jne int9ret

mov ax,0b800h
mov es,ax
inc byte ptr es:[160*12+40*2+1] ;段地址控制的文本显示的信息,改变颜色

int9ret:pop es
pop bx
pop ax
iret
code ens
end start

直接定址表

描述了单元长度的标号

之前的程序中,标号仅仅表示了内存单元的地址。但是我们还可以使用一种标号,这种标号不但表示内存单元的地址,还表示了内存单元的长度,即表示此单元是一个字节单元,还是双字单元。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
assume cs:code
code segment
a db 1, 2, 3, 4, 5, 6, 7, 8 ;a、b后面没有":",它们是同时描述内存地址和单元长度的标号
b dw 0 ;标号a描述了地址code:0,以后的单元都是字节单元;标号b表述了地址code:8,以后的字单元

start: mov si,0
mov cx,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start

因此这种标号包含了对单元长度的描述,所以在指令中,它可以代表一个段中的内存单元。对于程序中的b dw 0

  • 指令:mov ax,b相当于:mov ax,cs:[8]
  • 指令:mov b,2相当于:mov word ptr cs:[8],2
  • 指令:inc b相当于:inc word ptr cs:[8]

使用这种包含单元长度的标号,可以使我们以简洁的形式访问内存中的数据。我们称这种标号为数据标号。

在其他段中使用数据标号

下面程序将data段中a标号处8个数据累加,结果储存在b中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume cs:code,ds:data
data segment
a db 1, 2, 3, 4, 5, 6, 7, 8
b dw 0
data ends

code segment
start: mov ax,data
mov ds,ax

mov si,0
mov cx,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s

mov ax,4c00h
int 21h

code ends
end start
  • 注意:后面加有”:”的地址标号只能在代码段中使用,不能在其他代码段中使用。
  • 如果想在代码段中直接使用数据标号访问数据,则需要用伪命令assume将标号所在的段和一个段寄存器联系起来
    • 只是编译器的工作需要,类系c语言中要有函数原型,assume并没有将段寄存器和某个段相联系
    • 我们在程序中还要使用指令对寄存器进行设置

对于这个程序,编译器对相关指令的编译如下:

  • 指令:mov al,a[si],编译为mov al,ds:[si+0]
  • 指令:mov b,ax,编译为mov ds:[8],ax
  • 在执行这些指令前,ds必须为data的段地址mov ax,data;mov ds,ax

直接定址表

我们希望对数据建立某种映射关系,如果我们直接使用条件判断语句明显是可行的。但程序将要执行多条比较、转移指令。程序混乱。

因此我们可以建立一张表。假设我们要一次储存字符”0”“F”,我们可以通过015直接查找对应字符。使用如下

1
2
3
4
table db '0123456789ABCDE'

mov ah,table[bx]
等等

以数值N为table表中的偏移,可以找到对应的字符

利用表,两个数据集合之间建立了一种映射关系,使我们可以用查表的方法根据给出的数据的到其在另一个集合中的对应数据。这样做的目的一般有以下3个:

  • 为了算法的清晰和简洁
  • 为了加快运算速度
    • 如我们可以直接保存常见的三角函数,而不必计算
  • 为了使程序易于扩充

杂项

  • seg操作符,功能为取得某一标号的段地址
  • lea操作符,是mov的变种,功能为取有效地址(Load effect address),即取偏移地址
    • 格式: lea 目的,源
    • leaw,两个字节
    • leal,4个字节
    • leaq,8个字节
  • leave操作符,功能为将寄存器ebp(保留栈底指针)的内容复制到esp(保留栈顶指针)中,然后从栈中恢复ebp寄存器的旧值

评论