汇编语言概要学习

1 绪论

1.1 这门课学什么

  • 定位:理解硬件结构,掌握指令集,理解程序的运行过程。
  • 内容:选取8088、8086指令集进行汇编语言程序设计的学习。

1.2 机器语言与汇编语言

机器语言是机器指令的集合。
机器指令是一台机器可以正确执行的命令。
机器指令由一串二进制数表示,例 01010000。

汇编语言的主体是汇编指令。
汇编指令和机器指令的差别在于指令的表示方法上:① 汇编指令是机器指令便于记忆的书写格式; ② 汇编指令是机器指令的助记符。

机器指令 汇编指令 操作
1000100111011000 MOV AX, BX 将寄存器BX的内容送到AX中

用汇编语言编写程序的工作过程:程序员编写汇编指令→编译器编译为二进制机器码→计算机执行程序。

1.3 计算机的组成

CPU是计算机的核心部件,它控制整个计算机的运作并进行运算。要想让一个CPU工作,就必须向它提供指令和数据。指令和数据在存储器(内存,不是外存)中存放。离开了内存,性能再好的CPU也无法工作。

  • 指令和数据的表示

计算机中的数据和指令,存储在内存或磁盘(外存)上。数据和指令都是二进制信息。

问题是如何判断一串二进制数字表示的是数据还是指令呢?由CPU决定。

  • 计算机中的存储单元

存储器被划分为多个存储单元,存储单元从0开始编号。例如:8086有20条数据线,寻址空间为$2^{20} = 1\text{MB}$。

存储单元编号

简单补充:寄存器、缓存、内存、硬盘、存储器的理解

寄存器内置于CPU内部,是CPU内部的小型存储区域。它们通常由多个触发器构成,可以分为内部寄存器和外部接口寄存器。而内存则位于处理器外部是独立的硬件设备。它由内存芯片、电路板、金手指等部分组成,通过数据线与CPU相连。

存取速度的比较:CPU(包含寄存器,缓存)>内存 > 硬盘
内存和硬盘之间的速度,差 3~4 个数量级;寄存器和内存之间的速度也差了 3~4 个数量级。由于寄存器和内存的速度差异很大,所以现代的 CPU 往往还提供了“缓存”模块。

寄存器和内存的区别 - 电子发烧友
寄存器、缓存、内存、硬盘、存储器的理解 - CSDN
硬盘、内存、缓存(CPU)和寄存器 空间大小与存取速度的区别及设计原理 - CSDN

  • 计算机中的总线(BUS)

在计算机中专门有连接CPU和其他芯片的导线,通常称为总线。物理上:一根根导线的集合。逻辑上课划分为:地址总线、数据总线、控制总线

地址总线:CPU是通过地址总线来指定存储单元的。地址总线宽度决定了可寻址的存储单元大小,即$N$根地址总线(宽度为$N$)对应寻址空间$2^N$。

数据总线:CPU与内存或其它器件之间的数据传送是通过数据总线来进行的。数据总线的宽度决定了CPU和外界的数据传送速度。例:向内存中写入数据89D8H时的数据传送。

控制总线:CPU通过控制总线对外部器件进行控制。控制总线是一些不同控制线的集合控制总线宽度决定了CPU对外部器件的控制能力。

1.4 内存的读写与地址空间

1.4.1 CPU对存储器的读写
  • CPU要想进行数据的读写,必须和外部器件进行三类信息的交互
    • 存储单元的地址(地址信息 );
    • 器件的选择,读或写命令(控制信息 );
    • 读或写的数据(数据信息 );

例如:

机器指令 汇编指令 操作
101000000000001100000000 MOV AL, [3] 从内存存储单元的3号单元读取数据送入寄存器AL
1.4.2 内存地址空间
  • 什么是内存地址空间

CPU地址总线宽度为$N$,寻址空间为$2^N$个字节。8086CPU的地址总线宽度为20,那么可以寻址$1\text{MB}$个内存单元,其内存地址空间为$1\text{MB}$。

  • 从CPU角度看地址空间分配

将各类存储器看作一个逻辑存储器——进行统一编址,即所有的物理存储器被看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。

内存地址空间的分配方案——以8086PC机为例(640+128+256=1024KB = 20根地址总线):

00000H~9FFFFH(RAM) A0000H~BFFFFH(RAM) C0000H~FFFFFH(ROM)
主存储器地址空间 640KB 显存地址空间 128KB 各类ROM地址空间 256KB

RAM指的是随机存取存储器,ROM指的是只读存储器。

1、RAM:

随机存取存储器,缩写:RAM,也叫主存,是与CPU直接交换数据的内部存储器。它可以随时读写,而且速度很快,通常作为操作系统或其他正在运行中的程序的临时数据存储介质。

2、ROM:

只读存储器以非破坏性读出方式工作,只能读出无法写入信息。其中保存的数据是在制造计算机的时候就写好的,信息一旦写入后就固定下来,即使切断电源,信息也不会丢失,所以又称为固定存储器。ROM所存数据通常是装入整机前写入的,整机工作过程中只能读出,不像随机存储器能快速方便地改写存储内容。

2 访问寄存器和内存

2.1 寄存器以及数据存储

  • 寄存器是CPU内部的信息存储单元,以8086CPU为例,8086CPU有14个寄存器:
    • 通用寄存器:AX(AH + AL)、BX(BH + BL)、CX(CH + CL)、DX(DH + DL)
    • 变址寄存器:SI、DI
    • 指针寄存器:SP、BP
    • 指令指针寄存器:IP
    • 段寄存器:CS、SS、DS、ES
    • 标志寄存器:PSW
  • 8086CPU所有的寄存器都是16位的,可以存放两个字节。

“字”在寄存器中的存储

8086 CPU是16位微处理器,数据总线为16位,地址总线为20位。表明8086的字长为16bit

一个字(word)可以存在一个16位寄存器中,这个字的高位字节存在这个寄存器的高8位寄存器,这个字的低位字节存在这个寄存器的低8位寄存器。

2.2 mov和add指令

汇编指令 对应操作 人为描述
mov ax, 18 将18送入AX AX = 18
mov ah, 78 将78送入AH AH = 78
add ax, 8 将寄存器AX中的数值加上8 AX = AX + 8
mov ax, bx 将寄存器BX中的数据送入寄存器AX AX = BX
add ax, bx 将AX,BX中的内容相加,结果存在AX中 AX = AX + BX

2.3 确定物理地址的方法

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

8086有20位地址总线,可传送20位地址,寻址能力为1M。但是,8086是16位结构的CPU,运算器一次最多可以处理16位的数据,寄存器的最大宽度为16位。在8086内部处理的、传输、暂存的地址也是16位,寻址能力也只有64KB

问题:8086如何处理在寻址空间上的这个矛盾?
解决:用两个16位地址(段地址、偏移地址)合成一个20位的物理地址。

地址加法器合成物理地址的方法:

段地址乘以16就相当于向右移动4位,空出的位置补0。

2.4 内存的分段表示法

分段的方式管理内存,8086CPU用“(段地址x16)+偏移地址=物理地址”的方式给出内存单元的物理地址。
内存并没有分段,段的划分来自于CPU!!!。

  • 段地址 ×16 必然是16的倍数,所以一个段的起始地址也一定是16的倍数。
  • 偏移地址为16位,16位地址的寻址能力为64K,所以一个段的长度最大为64K。
  • 同一物理地址,可以由不同的段地址和偏移地址组成。

在8086PC机中存储单元地址的表示方法:例:数据在21F60H内存单元中,段地址是2000H,说法:

(a) 数据存在内存$\text{ 2000 : 1F60 }$单元中;
(b) 数据存在内存的$\text{2000H}$段中的$\text{1F60H}$单元中;

段地址很重要 —— 用专门的寄存器存放段地址:4个段寄存器
CS:代码段寄存器;SS:栈段寄存器;DS:数据段寄存器;ES:附加段寄存器。

偏移地址可以用多种方法提供——8086丰富的取址方式。

2.5 CS、IP与代码段

CS:代码段寄存器,IP:指令指针寄存器,CS:IP:CPU将内存中CS:IP指向的内容当作指令执行(也即CS相当于段地址,IP相当于偏移地址)。

执行何处的指令,取决于CS:IP,可以通过改变CS、IP中的内容,来控制CPU要执行的目标指令,那么如何改变CS、IP的值呢?由于8086CPU不提供对CS和IP修改的指令,因此需要使用jmp指令:

转移指令jmp
jmp 段地址:偏移地址 —— 这是同时修改CS和IP的内容
jmp 某一寄存器 —— 仅仅改变偏移地址IP中的内容

2.6 内存中字的存储

事实:对8086CPU而言,16位作为一个字。
问题:16位的字存储在一个16位的寄存器中,如何存储?
回答:高8位放高字节,低8位放低字节。
问题:16位的字在内存中需要2个连续字节存储,怎么存放?
回答:低位字节存在低地址单元,高位字节存在高地址单元,例:20000D(4E20H)存放在0、1两个单元,18D(0012H)存放在2、3两个单元,这两个单元怎么存储可分为:大端存储 VS 小端存储

  • 字单元

由两个地址连续的内存单元组成,存放一个字型数据(16位)

  • 用DS和[address]实现字的传送

要解决的问题:CPU从内存单元中要读取数据
要求:CPU要读取一个内存单元的时候,必须先给出这个内存单元的地址
原理:在8086PC中,内存地址曲段地址和偏移地址组成(段地址:偏移地址)
解决方案:DS和[address]配合 —— 用DS寄存器存放要访问的数据的段地址,偏移地址用[…]形式直接给出。

汇编程序示例 解释
mov bx, 1000H
mov ds, bx
mov al, [0]
将10000H(1000:0)中的数据读到al寄存器中

8086CPU不支持将数据直接送入段寄存器(这是硬件设计的问题)。
套路:数据→ 通用寄存器 → 段寄存器

2.7 DS与数据段

用DS存放数据段的段地址用相关指令访问数据段中的具体单元,单元地址由[address]指出。

2.8 栈及栈操作的实现

栈是一种只能在一端进行插入或删除操作的数据结构。

栈有两个基本的操作:① 入栈 ② 出栈。

入栈:将一个新的元素放到栈顶;出栈:从栈顶取出一个元素。

栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中取出。

栈的操作规则:LIFO(LastIn First out,后进先出)

CPU提供的栈机制:现今的CPU中都有栈的设计,8086CPU提供相关的指令,支持用栈的方式访问内存空间。基于8086CPU的编程,可以将一段内存当作栈来使用。

汇编指令 释义
push ax 将ax中的数据送入栈中【以字(16位=2字节)为单位对栈进行操作】
pop ax 从栈顶取出数据送入ax【以字(16位=2字节)为单位对栈进行操作】

问题
1、CPU如何知道一段内存空间被当作栈使用?
2、执行push和pop的时候,如何知道哪个单元是栈顶单元 ?

回答:
8086CPU中,有两个与栈相关的寄存器 —— 栈段寄存器SS存放栈顶的段地址,栈顶指针寄存器SP存放栈顶的偏移地址。

在汇编语言中使用栈要特别注意溢出问题:

8086CPU不保证对栈的操作不会超界。
8086CPU 只知道栈顶在何处(由SS:SP指示),不知道程序安排的栈空间有多大。
程序员在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;防止出栈时栈空了仍然继续出栈而导致的超界。

2.9 “段”的小结

  • 三种段

    • 数据段
      将段地址放在DS中
      用mov、add、sub等访问内存单元的指令时,CPU将我们定义的数据段中的内容当作数据段来访问;

    • 代码段

      将段地址放在CS中,将段中第一条指令的偏移地址放在IP中
      CPU将执行我们定义的代码段中的指令;

    • 栈段

      将段地址放在SS中,将栈顶单元的偏移地置放在SP中
      CPU在需要进行栈操作(push、pop)时,就将我们定义的栈段当作栈空间来用。

CS 是代码段寄存器,DS 是数据段寄存器,ES 是附加段(Extra Segment)寄存器。附加段的意思是,它是额外赠送的礼物,当需要在程序中同时使用两个数据段时,DS 指向一个,ES 指向另一个。可以在指令中指定使用 DS 和 ES 中的哪-个,如果没有指定,则默认是使用 DS。

SS 是栈段寄存器。IP 是指令指针(Instruction Pointer)寄存器,它只和 CS 一起使用,而且只有处理器才能直接改变它的内容。当一段代码开始执行时,CS 指向代码段的起始地址,IP则指向段内偏移。这样,由 CS 和 IP 共同形成逻辑地址,并由总线接口部件变换成物理地址来取得指令。然后处理器会自动根据当前指令的长度来改变 IP 的值,使它指向下一条指令。

3 汇编语言程序

3.1 汇编程序概况

汇编程序:包含汇编指冷和伪指令的文本。

汇编程序的组成

汇编程序中包含的三种伪指令

3.2 Loop指令

功能:实现循环(计数型循环)

指令的格式:loop 标号

CPU执行loop指令时要进行的操作:① (cx)=(cx)-1; ② 判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。

1
2
3
4
5
6
7
8
9
10
11
;loop指令示例程序
assume cs:code
code segment
mov ax,2
mov cx,11 ; 存放循环次数
s: add ax ax ; s为标号,循环跳转到此处
1oop s
mov ax,4c00h
int 21h
code ends
end

3.3 段前缀的使用

代码 释义
mov ax, 2000
mov ds, ax
mov bx, 0
mov al, ds:[bx]
这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址
的“ds:” “cs:” “ss:” 或 “es:”,在汇编语言中称为段前缀

3.4 在代码段中使用数据

数据不能随意地在内存中存放,这是很危险的,容易导致重要内存存放的内容被修改。

应用案例:编程计算以下8个数据的和,结果存在ax存器中
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
只要求数据本身,并未指定在哪些内存单元中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
assume cs:code
code segment
dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H
; 在代码段中定义数据
; dw: define word, 表示定义字型数据
; dw 定义一个字
; db 定义一个字节
; dd 定义一个双字
; 前面是人为定义在代码段中的数据,因此前面这部分肯定是不能被翻译为指令,而IP默认是为0的,怎么办呢?
; 解决方法:定义一个标号start,指示代码开始的位置。
start: mov bx, 0
mov ax, 0
mov cx, 8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start ; end的作用:除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。

3.5 在代码段中使用栈

问题:利用栈,将程序中定义的数据逆序存放。

程序的思路大致如下:
程序运行时,定义的数据存放在cs:0-cs:8单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这8个字单元中,从而实现数据的逆序存放。

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
assume cs:codesg
codesg segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
dw 0,0,0,0,0,0,0,0,0,0,0,0.0,0,0,0

start:
; 设置栈地址和指针
mov ax,cs
mov ss,ax
mov sp,30h
;入栈
mov bx,0
mov cx,8
s0: push cs:[bx]
add bx,2
loop s0
;出栈
mov bx,0
mov cx,8
s1: pop cs:[bx]
add bx,2
loop s1

mov ax,4c00h
int 21h
codesg ends
end start

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

  • 上面这个将数据逆序存放的程序有如下特点和缺陷:

    • 特点:数据、栈和代码都在一个段。

    • 问题:程序显得混乱,编程和阅读时都要注意何处是数据,何处是栈,何处是代码。

      只适合应用于要处理的数据很少,用到的栈空间也小,加上没有多长的代码。

处理方法:数据段、代码段、栈端分开

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
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, 0, 0, 0, 0, 0, 0
stack ends
; 代码段
code segment
start:
; 初始化各段寄存器
; 注意:cs代码段不用手动初始化,编译器自动完成代码段的初始化
mov ax, stack
mov ss, ax
mov sp, 20h
mov ax, data
mov ds, ax
;入栈
mov bx, 0
mov cx, 8
s0: push cs:[bx]
add bx, 2
loop s0
;出栈
mov bx, 0
mov cx, 8
s1: pop cs:[bx]
add bx, 2
loop s1
mov ax, 4c00h
int 21h
code ends
end start
  • 程序段前缀PSP

程序段前缀PSP(Pogram segment Prefix)是DOS 加载一个外部命令或应用程序(EXE、COM类型)时,在该程序段之前自动设置的一个具有256(100H)个字节的信息区域。

当 DOS 把控制权转交给外部命令或应用程序时,数据段寄存器DS和附加段寄存器ES首先被设置为指向程序段前缀,即与PSP含有相同的段值,而不是一开始就指向程序的数据段和附加段。堆栈段寄存器SS和代码段寄存器CS的段地址要比 DS 和 ES 高/大0100H。

PSP 含有许多的可用信息,其中偏移地址000H~007FH 范围为格式化区域,即加载程序时自动设置的各种信息区域;0080H~00FFH 区域为PSP的非格式化区域,即用来存储被加载程序的输入参数(DOS 加载一个外部命令或应用程序时,允许在被加载的程序名之后输入包括回车符在内的最多127个字符参数)。其中偏移地址0000H~0001H 内容为程序终止退出命令INT20H,0080H单元存储命令行参数的长度(字节数),由0081H地址开始存储命令行参数,如无命令行参数则为0。

当一个.EXE可执行文件加载到内存时,内存会被划分为几个区域:程序段前缀(PSP)、用户数据区、用户堆栈区以及用户代码段。PSP区域包含了关于可执行文件的控制信息,其开头的INT 20H指令用于结束程序。程序的执行通常始于将DSES段寄存器设置为PSP的地址,并在程序结束时使用RET指令将控制权转回DOS。 链接

4. 内存寻址方式

4.1 处理字符问题

汇编程序中,用'...'的方式指明数据是以字符的形式给出的,编译器将把它们转化为相对应的ASCII码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
assume cs:code, ds:data
; 数据段
data segment
db 'unIx'
db 'foRk'
data ends
code segment
start:
mov al, 'a'
mov bl, 'b'
mov ax,4c00h
int 21h
code ends
end start

4.2 [bx+idata]方式寻址

[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata(即bx中的数值加上idata)。

mov ax, [bx+200] / mov ax, [200+bx]的含义:将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,内存单元的段地址在ds中,偏移地址为200加上bx中的数值。

4.3 SI和DI寄存器

首先,简单回顾一下CPU内部的寄存器

  • 8086CPU有14个寄存器
    • 通用寄存器:AX、BX、CX、DX
    • 变址寄存器:SI、DI
    • 指针寄存器:SP、BP
    • 指令指针寄存器:IP
    • 段寄存器:CS、SS、DS、ES
    • 标志寄存器:PSW

Sl和DI常执行与地址有关的操作:SI和DI是8086CPU中和Bx功能相近的寄存器;区别在于SI和DI不能够分成两个8位寄存器来使用。

BX:通用寄存器,在计算存储器地址时,常作为基址寄存器用。
Sl(source index):源变址寄存器。
Dl(destination index):目标变址寄存器。

下面的三组指令实现了相同的功能

指令1 指令2 指令3
mov bx, 0
mov ax, [bx]
mov si, 0
mov ax, [si]
mov di, 0
mov ax, [di]
  • [bx+si]和[bx+di]方式寻址

指令mov [bx+si]的含义:将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值,段地址在ds中。

  • [bx+si+idata]和[bx+di+idata]方式寻址

指令mov [bx+si+idata]的含义:将一个内存单元的内容送入ax,这个内存单元的长度为2字节(字单元),存放一个字,偏移地址为bx中的数值加上si中的数值再加上idata,段地址在ds中。

对内存的寻址方式小结

4.4 用于内存寻址的寄存器

只有bx、bp、si、di可以用在[…]对内存单元寻址,通用寄存器除了bx,其他(ax、cx、dx)都不可用于寻址。

bx、bp区别:bx默认指ds段,bp默认指ss段。

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

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

指令要处理的数据有多长

4.6 用div指令实现除法

  • div是除法指令,使用div作除法的时候
    • 被除数:默认放在AX 或 DX和AX中
    • 除数:8位或16位,在寄存器或内存单元中
    • 结果:……
被除数 除数 余数
AX 8位内存或寄存器 AL AH
AX和DX 16位内存或寄存器 AX DX

切记提前在默认的寄存器中设置好被除数,且默认寄存器不作别的用处。

4.7 用dup设置内存空间

功能:dup和db、dw、dd 等数据定义伪指令配合使用,用来进行数据的重复。

指令 功能 相当于
db 3 dup (0) 定义了3个字节,它们的值都是0 db 0,0,0
db 3 dup (0,1,2) 定义了9个字节,由0、1、2重复3次构成 db 0,1,2,0,1,2,0,1,2
db 3 dup (‘abc’,ABC’) 定义了18个字节,构成’abcABCabcABCabcABC db ‘abcABCabcABCabcABC’

5. 流程转移与子程序

5.1 转移综述

  • 背景:一般情况下指令是顺序地逐条执行的,而在实际中,常需要改变程序的执行流程
  • 转移指令
    • 可以控制CPU执行内存中某处代码的指令
    • 可以修改IP,或同时修改CS和IP的指令

转移指令的分类

转移指令的分类

5.2 操作符offset

用操作符offset取得标号的偏移地址

1
2
3
4
5
6
7
8
; offset的使用:offset 标号
assume cs:codeseg

codeseg segment
start: mov ax,offset start ; 相当于mov ax,0
s: mov ax,offset s ; 相当于mov ax,3
codeseg ends
end start

5.3 jmp指令——无条件转移

jmp指令的功能:无条件转移,可以只修改IP,也可以同时修改CS和IP。

  • jmp指令要给出两种信息
    • ① 转移的目的地址;
    • ② 转移的距离
      • 段间转移(远转移):jmp 2000:1000
      • 段内短转移:jmp short 标号,IP的修改范围为 -128到127,8位的位移
      • 段内近转移:jmp near ptr 标号,IP的修改范围为-32768到32767,16位的位移

image.png

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

    • 指令格式:jmp 16位寄存器
    • 功能:IP=(16位寄存器)
    • 举例:jmp ax;jmp bx

5.4 其他转移指令

5.4.1 jcxz指令

指令格式:jcxz 标号
功能:如果(cx)=0,则转移到标号处执行;若(cx)≠0,什么也不做(程序向下执行 )

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

5.4.2 loop指令

指令格式:loop 标号
功能:循环,如果(cx)=0,跳出循环,;若(cx)≠0,则cx = cx-1,则转移到标号处执行。

5.5 call指令和ret指令

调用子程序:cal指令
返回:ret指令
实质:流程转移指令,它们都修改P,或同时修改CS和IP

1
2
3
4
5
6
7
mov ax, 0
call s
mov ax, 4c00h
int 21h

s: add ax, 0
ret

(一)call指令介绍

  • CPU执行cal指令,进行两步操作
    • (1) 将当前的IP或 CS和IP 压入栈中;
    • (2) 转移到标号处执行指令。
  • call 标号
    • 16位位移 = “标号”处的地址-cal指令后的第一个字节的地址
    • 16位位移的范围为-32768~32767,用补码表示
    • 16位位移由编译程序在编译时算出。
  • 指令call far ptr 标号实现的是段间转移
    • (CS)=标号所在的段地址
    • (IP)=标号所在的偏移地址
    • 该条指令相当于
      • push CS → push lP → jmp far ptr 标号
  • 转移地址在寄存器中的call指令
    • 显指令格式:call 16位寄存器
  • 转移地址在内存中的call指令
    • call word ptr 内存单元地址
      • 相当于:push IP → jmp word ptr 内存单元地址
    • call dword ptr 内存单元地址
      • 相当于:push CS → push lP → jmp dword ptr 内存单元地址

(二)ret指令介绍

ret指令 retf指令
功能 用栈中的数据修改IP的内容,从而实现近转移 用栈中的数据,修改CS和IP的内容,从而实现远转移
相当于 pop IP pop IP; pop CS

此外,ret指令后面还可以直接跟数字,其作用相当于:

1
2
ret n ⇿ pop IP
add sp, n

5.6 mul乘法指令

8位乘法 16位乘法
被乘数(默认) AL AX
乘数 8位寄存器或内存字节单元 16位寄存器或内存字单元
结果 AX DX(高位)和AX(低位)

5.7 标志寄存器PSW/FLAGS

5.7.1 认识标志寄存器的特殊之处

标志寄存器flag的结构:

flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

8086CPU中标志寄存器flag是16位的,其中没有使用flag的1、3、5、12、13、14、15位,这些位不具有任何含义。

5.7.2 直接访问标志寄存器的方法

pushf:将标志寄存器的值压栈;

popf:从栈中弹出数据,送入标志寄存器中。

5.7.3 ZF-零标志(Zero Flag)
  • ZF标记相关指令的计算结果是否为0
    • ZF=1,表示“结果是0”,1表示“逻辑真”
    • ZF=0,表示“结果不是0”,0表示“逻辑假“

在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如:add、sub、mul、div、inc、or、and等,它们大都是运算指令,进行逻辑或算术运算;

有的指令的执行对标志寄存器没有影响,比如:mov、push、pop等,它们大都是传送指令。

使用一条指令的时候,要注意这条指令的全部功能其中包括执行结果对标记寄存器的哪些标志位造成影响。

5.7.4 PF-奇偶标志(Parity Flag)
  • PF记录指令执行后,结果的所有二进制位中1的个数:
    • 1的个数为偶数,PF=1
    • 1的个数为奇数,PF=0。
5.7.5 SF-符号标志(Sign Flag)
  • SF记录指令执行后,将结果视为有符号数
    • 结果为负,SF=1
    • 结果为非负,SF=0
5.7.6 CF-进位标志(Carry Flag)

在进行无符号数运算的时候,CF记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值。

  • CF记录指令执行后
    • 有进位或借位,CF = 1
    • 无进位或借位,CF = 0
5.7.7 OF-溢出标志(Overflow Flag)

在进行有符号数运算的时候,如结果超过了机器所能表示的范围称为溢出

  • OF记录有符号数操作指令执行后
    • 有溢出,OF=1
    • 无溢出,OF=0

5.8 带进(借)位的加减法

5.8.1 adc带进位加法指令
  • adc是带进位加法指令,它利用了CF位上记录的进位值。
    • 格式:adc 操作对象1, 操作对象2
    • 功能:操作对象1= 操作对象1+操作对象2+CF
5.8.2 sbb带借位减法指令
  • 格式:sbb 操作对象1, 操作对象2
    • 功能:操作对象1=操作对象1-操作对象2-CF
    • 与sub区别:利用CF位上记录的借位值

5.9 cmp与条件转移指令

5.9.1 cmp指令
  • cmp是比较指令,功能相当于减法指令,但不保存结果。cmp指令执行后,将对标志寄存器产生影响
    • 格式:cmp 操作对象1,操作对象2
    • 功能:计算 操作对象1-操作对象2
5.9.2 条件转移指令jxxx

格式:jxxx 标号

根据单个标志位转移的指令

根据无符号数比较结果进行转移的指令

根据有符号数比较结果进行转移的指令

条件转移指令通常都和cmp相配合使用,cmp指令改变标志位,jxxx根据相应的标志寄存器判断是否跳转。

5.10 DF标志和串传送指令

5.10.1 DF-方向标志位(Direction Flag)
  • 功能:在串处理指令中,控制每次操作后si,di的增减。
    • DF=0:每次操作后si,di递增;
    • DF=1:每次操作后si,di递减。
  • 对DF位进行设置的指令
    • cld指令:将标志寄存器的DF位设为0(clear)
    • std指令:将标志寄存器的DF位设为1(setup)

5.10.2 rep指令
  • rep指令常和串传送指令搭配使用
    • 功能:根据cx的值,重复执行后面的指令
    • 例如:rep movsb
    • 等价于:
      wjh:movsb
      loop wjh

补充:movsb字节传送指令

格式:movsb
功能:执行movsb指令相当于进行下面几步操作:
(1)((es)×16+(di))=((ds)×16+(si))

(2) 如果df=0则:(si)=(si)+1,(di)=(di)+1;如果df=1则:(si)=(si)-1,(di)=(di)-1

6. 中断及外部设备操作

6.1 移位指令

移位指令 示例代码 释义 是否影响CF位
逻辑左移
SHL OPR,CNT
mov al,01001000b
shl al,1
二进制数字向左移动一位,
末尾补0,移出的一位进到CF中
循环左移
ROL OPR, CNT
mov al,01001000b
rol al,1
二进制数字向左移动一位,
然后补到末尾,移出的一位进到CF中
逻辑右移
SHR OPR,CNT
mov al,01001000b
shr al,1
二进制数字向右移动一位,
开头补0,移出的一位进到CF中
循环右移
ROR OPR,CNT
mov al,01001000b
ror al,1
二进制数字向右移动一位,
然后补到开头,移出的一位进到CF中
算术左移
SAL OPR,CNT
mov al,01001000b
sal al,1
二进制数字向左移动一位,
末尾补0,移出的一位进到CF中
算术右移
SAR OPR,CNT
mov al,01001000b
sar al,1
二进制数字向右移动一位,
但是开头一位保持不变,移
出的一位进到CF中
带进位循环左移
RCL OPR,CNT
mov al,01001000b
rcl al,1
二进制数字向左移动一位,
原CF中的数值回到末尾,移
出的一位进到CF中
带进位循环右移
RCR OPR,CNT
mov al,01001000b
rcr al,1
二进制数字向右移动一位,
原CF中的数值回到开头,移
出的一位进到CF中

注意①:当移动的位数大于1时,必须使用cl寄存器中转移动的位数。

注意②:逻辑左移相当于乘以2,逻辑右移相当于除以2。

6.2 操作显存数据

屏慕上的内容=显存中的数据

根据前面的图可知,8086CPU的显存地址空间位于:A0000到BFFFF区间。其中B8000h~BFFFFh共32K的空间,是8*25 彩色字符模式第0页的显示缓冲区。

B8000h~BFFFFh共32K的空间的简单解释

BFFFFh-B8000h = 7FFFH = 8000H → 2^3*2^12 = 2^3*2^2*2^10 = 32*1Kb

显示缓冲区结构

在显存中,一个字符确实占两个字节。‌ 在80*25彩色字符模式下,每个字符由两个字节组成,一个字节存储字符的ASCII码,另一个字节存储字符的属性(如背景色、前景色等)‌。

在显存中,每个字符占用两个字节的原因是因为:

  • 字符的ASCII码‌:一个字节(低位)用于存储字符的ASCII码,表示字符本身。
  • 字符的属性‌:另一个字节(高位)用于存储字符的属性,如背景色、前景色、闪烁、高亮等。

这种设计使得每个字符不仅可以显示出来,还可以通过属性字节来控制其显示效果。

例:编程序,在屏幕的中间,自底蓝字,显示Welcome to masm!

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
assume cs:codeseg, ds:datasg
datasg segment
db 'welcome to masm!'
datasg ends

codeseg segment
start:
; 初始化寄存器
mov ax, datasg
mov ds, ax
mov ax, 0B800H
mov es, ax
mov si, 0
mov di, 160*12+80-16
; 显示字符串
mov cx,16
w: mov al, [si]
mov es:[di], al
inc di
mov al, 71H
mov es:[di], al
inc si
inc di
loop w
mov ax,4c00h
int 21h
codeseg ends
end start

6.3 描述内存单元的标号

6.3.1 关于标号

代码段中的标号可以用来标记指令、段的起始地址。

代码段中的数据也可以用标号。

6.3.2 数据标号——去了冒号的数据标号

数据标号标记了存储数据的单元的地址和长度
数据标号不同于仅仅表示地址的地址标号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
assume cs:code
code segment
;数据标号a、b前面不加:,注意这段数据定义在数据段也是可以这样用的
a db 1 2,3,4 5,6,7,8
b dw 0
start : mov si,0
mov cx,8
s: mov al,a[si] ;相当于[si+a]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00h
int 21h
code ends
end start
6.3.3 扩展用法:二级指针:将标号当作数据来定义

6.4 直接定址表

6.4.1 数据的直接定址表

直接定址表:用查表的方法解决问题。

有如下问题:以十六进制的形式在屏幕中间显示给定的byte型数据。

新方按:建立一张表,表中依次存储字符“0” ~ “F”,我们可以通过数值0~15直接查找到对应的字符
table db '0123456789ABCDEF' ;字符表

6.4.2 代码的直接定址表

函数指针数组,示例如下

6.5 中断及其处理

6.5.1 中断的概念

中断:CPU不再接着(刚执行完的指令)向下执行,而是转去处理中断信息

内中断:由CPU内部发生的事件而引起的中断

外中断:由外部设备发生的事件引起的中断

6.5.2 8086的内中断
  • CPU内部产生的中断信息
    • (1) 除法错误,比如:执行div指令产生的除法溢出
    • (2) 单步执行
    • (3) 执行into 指令
    • (4) 执行int 指令
  • 8086的中断类型码
    • (1) 除法错误:0
    • (2) 单步执行:1
    • (3) 执行into指令:4
    • (4) 执行 int n指令,立即数n为中断类型码
  • 中断处理程序
    • CPU接到中断信息怎么办?
      • 执行中断处理程序
    • 中断处理程序在哪里 ?
      • 中断信息和其处理程序的入口地址之间有某种联系,CPU根据中断信息可以找到要执行的处理程序。
    • 中断向量表
      • 由中断类型码查表得到中断处理程序的入口地址,从而定位中断处理程序。

  • 中断过程
    • 中断过程由CPU的硬件自动完成
    • 用中断类型码找到中断向量,并用它设置CS和IP
  • 8086CPU的中断过程
    • (1) 从中断信息中取得中断类型码
    • (2) 标志寄存器的值入栈——中断过程中要改变标志寄存器的值,需要先行保护
    • (3) 设置标志寄存器的第8位TF和第9位IF的值为0
    • (4) CS寄存器的内容入栈
    • (5) IP寄存器的内容入栈
    • (6) 从中断向量表读取中断处理程序的入口地址,设置IP和CS。

6.6 中断处理程序及其结构

CPU随时都可能检测到中断信息,所以中断处理程序必须常驻内存(一直存储在内存某段空间之中)。
中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中(0000:0000-0000:03FF)。

6.6.1 编制中断处理程序—以除法错误中断为例

问题:如何编制中断处理程序?
方案:通过对 0号中断,即除法错误的中断处理,体会中断处理程序处理的技术问题
预期效果:编写一个0号中断处理程序,它的功能是在屏幕中间显示“overflow!”后,然后返回到操作系统。

  • 问题1:中断处理子程序应该放在哪里?

中断处理子程序应该存放在内存的确定位置,但这里我们是模拟中断处理,要重新找个地方,不破坏系统的中断处理函数。

在操作系统之上使用计算机,所有的硬件资源都在操作系统的管理之下,应该向操作系统申请获得存放中断处理子程序的内存。

使用汇编语言可以绕过操作系统,直接在找到一块别的程序不会用到的内存区,将中断处理子程序传送到其中即可。(注意:不是工程化的方法,但也体现实用技巧)

内存0000:0000~0000:03FFF大小为1KB的空间是系统存放中断向量表,DOS系统和其他应用程序都不会随便使用这段空间。8086支持256个中断,但实际上系统中要处理的中断事件远没有达到256个。利用中断向量表中的空闲单元来存放我们的程序。估计出,中断处理子程序的长度不可能超过256个字节,就选用从0000:0200至0000:02FF的256个字节的空间。

地址 内容
0000:0000 IP 0200
0000:0002 CS 0000
  • 问题2:怎么写安装程序?

(1) 编写可以显示“overflow!”的中断处理程序,命名为:do0
(2) 安装程序:将do0送入内存0000:0200处
(3) 将do0中断处理程序的入口地址0000:0200存储在中断向量表0号表项中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
; 程序框架
assume cs:code
code segment
start:
do0安装程序
设置中断向量表
mov ax,4c00h
int 21h
do0: 显示字符串 "overflow !"
mov ax,4c00h
int 21h
do0end: nop
code ends
end start
1
2
3
4
5
6
7
8
9
10
;; do0安装程序
mov ax,cs
mov ds,ax
mov si,offset do0 ; 设置ds:si指向源地址cs:do0
mov ax,0
mov es,ax
mov di,200h ; 设置es:di指向目的地址0000:0200h
mov cx, offset do0end - offset do0 ; 设置cx为传输长度为do0部分代码的长度
cld ; 设置传输方向为正
rep movsb
  • 问题3:中断处理函数do0怎么写?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;; do0中断处理子函数体
do0: jmp short do0start
db 'overflow!'
do0start:
mov ax, cs
mov ds, ax
mov si, 202h ; jmp short do0start这个语句占2字节,故是200+2=202h

mov ax,0b800h
mov es,ax
mov di,12*160+36*2
mov cx,9
s: mov al,[si]
mov es:[di],al
inc si
add di,2
loop s
  • 问题4:如何设置中断向量表?

设置中断向量表任务:将do0的入口地址0:200h,写到中断向量表的0号表项中即可。

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
6.6.2 单步中断

由Debug中的t命令说起…

程序的正常执行:取指令、改变CS:IP、执行指令、取指令……

Debug提供了单步中断的中断处理程序,功能为显示所有寄存器中的内容后等待输入命令。

  • 是什么,让CPU能执行一条指令就停下来?
    • Debug利用了CPU提供的单步中断的功能
    • 使用t命令时,Debug将TF标志设为1,使CPU工作在单步中断方式下…
    • 自定义单步中断处理程序,还可以实现特殊的功能。

单步中断过程与处理

  • 两个和中断相关的寄存器标志位
    • TF-陷阱标志(Trap flag):用于调试时的单步方式操作。
      • 当TF=1时,每条指令执行完后产生陷阱,由系统控制计算机
      • 当TF=0时,CPU正常工作,不产生陷阱。
    • IF-中断标志(Iinterrupt flag)
      • 当IF=1时,允许CPU响应可屏蔽中断请求;
      • 当IF=0时,关闭中断

CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断(中断类型码为1),引发中断过程,执行中断处理程序。

  • 中断过程
    • (1)取得中断类型码1;
    • (2) 标志寄存器入栈,TF、IF设置为0;
      • 中断处理程序也由一条条指令组成的。如果在执行中断处理程序之前,TF=1,则CPU在执行完中断处理程序的第一条指令后,又要产生单步中断,转去执行单步中断的中断处理程序的第一条指令……
        上面的过程将陷入一个永远不能结束的循环CPU永远执行单步中断处理程序的第一条指令所以,在进入中断处理程序之前,设置TF=0。
    • (3) CS、IP入栈;
    • (4) (IP)=(1×4), (CS)=(1×4+2)

应用:中断不响应的情况

一般情况下,CPU在执行完当前指令后,如果检测到中断信息,就响应中断,引发中断过程。在有些情况下,CPU在执行完当前指令后,即便是发生中断,也不会响应。

例如:在执行完向 ss寄存器传送数据的指令后,即便是发生中断,CPU 也不会响应。
原因:ss:sp联合指向栈顶,而对它们的设置应该连续完成,不能被中断打断

6.7 由int指令引发的中断

int格式:int n,n为立即数,表示中断类型码

功能:引发中断过程

  • CPU执行intn指令,相当于引发一个n号中断的中断过程,执行过程如下
    • (1) 取中断类型码n;
    • (2) 标志寄存器入栈,IF=0,TF=0;
    • (3) CS、IP入栈;
    • (4) (IP)=(n×4),(CS)=(n×4+2)。

小小结:
int指令的最终功能和call指令相似,都是调用一段程序。一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。

编写供应用程序调用的中断例程
技术手段:编程时,可以用int指令调用子程序;此子程序即中断处理程序,简称为中断例程。可以自定义中断例程实现特定功能。

  • 中断处理程序的常规的步骤
    • (1) 保存用到的寄存器。
    • (2) 处理中断。
    • (3) 恢复用到的寄存器。
    • (4) 用 iret指令返回。

6.8 BIOS和DOS中断处理

6.8.1 BIOS —— 基本输入输出系统

(一)BIOS,是在系统板的ROM中存放着一套程序

容量:8KB,地址:从FE000H开始

  • BIOS中的主要内容
    • (1) 硬件系统的检测和初始化程序
    • (2) 外部中断和内部中断的中断例程
    • (3) 用于对硬件设备进行I/0操作的中断例程
    • (4) 其他和硬件系统相关的中断例程
  • BIOS的意义
    • 使用BIOS功能调用,程序员不用了解硬件操作细节,直接使用指令设置参数,并中断调用BIOS例程,即可完成相关工作!
    • 使用BIOS功能调用:(1) 方便编程;(2) 能写出简洁、可读性好、易于移植的程序。

(二)BIOS中断调用示例

任务:在屏幕的5行12列显示3个红底高亮闪烁绿色的a。

方案:用BIOS的10h中断

ah=2时,调用第10h中断例程的2号子程序,设置光标位置;
ah=9时,调用第10h中断例程的9号子程序,在光标位置显示字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code
code segment
mov ah,2 ;置光标功能
mov bh,0 ;第0页
mov dh,5 ;dh中放行号
mov dl,12 ;dl中放列号
int 10h ;调用BIOS的10h中断

mov ah,9 ;显示字符功能
mov al,'a' ;字符a
mov bl,11001010b ;颜色属性
mov bh,0 ;第o页
mov cx,3 ;字符重复个数
int 10h ;调用BIOS的10h中断

mov ax,4c00h
int 21h
code ends
end

(三)有哪些BIOS中断,怎么用

6.8.2 DOS中断

常见的DOS中断:

BIOS和DOS在所提供的中断例程中包含了许多子程序,这些子程序实现了程序员在编程的时常用到的功能。
和硬件设备相关的DOS中断例程中,一般都调用BIOS的中断例程。

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

(1) CPU一加电,初始化(CS)=0FFFFH,(IP)=0,自动从FFFF:0单元开始执行程序。FFFF:0处有一条转跳指令,CPU执行该指令后,转去执行BIOS中的硬件系统检测和初始化程序。

(2) 初始化程序将建立BIOS所支持的中断向量,即将BIOS提供的中断例程的入口地址登记在中断向量表中。

(3) 硬件系统检测和初始化完成后,调用int 19h进行操作系统的引导。从此将计算机交由操作系统控制。

(4) DOS启动后,除完成其它工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。

6.9 端口的读写

6.9.1 引入——用端口访问外设:以发声为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
assume cs:codeseg
codeseg segment

start: mov al, 08h ;设置声音的频率
out 42h, al
out 42h, al
in al, 61h ;读设备控制器端口原值

mov ah, al ;保存原值
or al, 3 ;打开扬声器和定时器
out 61h, al ;接通扬声器,发声

mov cx,60000 ;延时
delay: nop
loop delay

mov al, ah ;恢复端口原值
out 61h, al
mov ax,4c00h
int 21h
codeseg ends
end start
  • 补充:CPU的邻居——CPU可以直接读写3个地方的数据
    • (1) CPU 内部的寄存器;
    • (2) 内存单元;
    • (3) (物理外设)端口
      • 各种接口卡,网卡、显龙等
      • 主板上的接口芯片
      • 其他芯片

6.9.2 端口的读写指令
  • 读写内存与寄存器的指令
    • mov, add, push...
  • 读写端口的指令
    • in: CPU从端口读取数据
    • out:CPU往端口写入数据

  • 端口的读写指令示例

    • 对0~255以内的端口进行读写,端口号用立即数给出

      • ```assembly
        in al, 20h ;从20h端口读入一个字节
        out 21h, al ;往21h端口写入一个字节
        1
        2
        3
        4
        5
        6
        7

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

        - ```assembly
        mov dx,3f8h ;将端口号3f8送入dx
        in al,dx ;从3f8h端口读入一个字节
        out dx,al ;向3f8h端口写入一个字节

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

6.10 操作CMOS RAM芯片

6.10.1 CMOS RAM 芯片

系统开机BIOS界面

我们在操作计算机的时候,电脑一开始的时候呢,我们按f8可以进入一个叫做bios设置当中,在设置里边包含了一些时间呀包括其他的一些整个系统的信息,如果没有这些信息的话呢,计算机是不能够启动起来的。

那么其实做出这些工作,它得益于在整个的主板上有这么一个所谓的cmos ram芯片,它包含着一个实时的时钟和一个有128个存储单元的ram存储器。

  • CMOS RAM 芯片
    • (1) 包含一个实时钟和一个有128个存储单元的RAM存储器
    • (2) 128 个字节的 RAM 中存储:内部实时钟、系统配置信息,相关的程序(用于开机时配置系统信息,引导系统启动)。
    • (3) CMOS RAM 芯片靠电池供电,关机后其内部的实时钟仍可正常工作 ,特别的是,此块RAM中的信息不丢失(一般RAM掉电丢失信息,这个其实是因为在这里边有一块纽扣电池为其供电)。
    • (4) 该芯片内部有两个端口,端口地址为70h和71h,CPU通过这两个端口读写CMOS RAM。
      • 70h地址端口,存放要访问的CMOS RAM单元的地址
      • 71h数据端口,存放从选定的单元中读取的数据,或要写入到其中的数据。

6.10.2 端口操作示例:提取CMOSRAM中存储的时间信息

问题描述:在屏幕中间显示当前的月份

6.11 外设连接与中断

CPU通过端口与外部设备“连接”,CPU 在执行指令过程中,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。

6.11.1 外中断:由外部设备发生的事件引起的中断
  • 可屏蔽中断
    • 可屏蔽中断是CPU 可以不响应的外中断。
    • CPU 是否响应可屏蔽中断,要看标志寄存器的IF 位的设置。
    • 当CPU检测到可屏蔽中断信息时:
      • 如果IF=1,则CPU在执行完当前指令后响闻应中断,引发中断过程;
      • 如果IF=0,则不响应可屏蔽中断。
    • 几乎所有由外设引发的外中断,都是可屏蔽中断,比如键盘输入、打印机请求。
  • 不可屏蔽中断
    • CPU 必须响应的外中断,当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。
    • 对于8086CPU不可屏蔽中断的中断类型码固定为2。
    • 不可屏蔽中断在系统中有必须处理的紧急情况发生时用来通知CPU 的中断信息。
6.11.2 外中断处理过程
  • 可屏蔽中断所引发的中断过程

    • (1) 取中断类型码n;
      • 可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU(对比内中断:中断类型码是在CPU内部产生的)。
    • (2) 标志寄存器入栈,IF=0,TF=0;
      • 将IF置0的原因:进入中断处理程序后,禁止其他的可屏蔽中断。如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF 置1 。
    • (3) CS、IP入栈;
    • (4) (IP)=(n×4),(CS)=(n×4+2)
  • 不可屏蔽中断的中断过程(中断值固定为2,不必取中断码)

    • (1) 标志寄存器入栈,IF=0,TF=0;
    • (2) CS、IP入栈;
    • (3) (IP)=(8),(CS)=(0AH)。

8086CPU提供的设置IF的指令:

sti——用于设置IF=1;

cli——用于设置IF=0

6.11.3 PC机键盘的处理过程

键盘输入的处理过程:① 键盘输入;② 引发9号中断;③ 执行int 9中断例程

(一)键盘输入

  • 键盘上的每一个键相当于一个开关,键盘中有一芯片对键盘上的每一个键的开关状态进行扫描。
  • 按下一个键时的操作
    • 开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。
    • 扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60H
  • 松开按下的键时的操作
    • 产生一个扫描码,扫描码说明了松开的键在键盘上的位置。
    • 松开按键时产生的扫描码也被送入60H端口中。
  • 扫描码——长度为一个字节的编码
    • 按下一个键时产生的扫描码——通码,通码的第7位为0
    • 松开一个键时产生的扫描码——断码,断码的第7位为1
    • 断码 = 通码+80H
    • 例:g键的通码为22H,断码为a2H

(二)引发9号中断

键盘的输入到达60H端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。

CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9中断例程。

  • 输入的字符键值如何保存?

    • 有BIOS键盘缓冲区!
      • BIOS键盘缓冲区:是系统启动后,BIOS用于存放int9中断例程所接收的键盘输入的内存区。
      • BIOS键盘缓冲区:可以存储15个键盘输入,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码(ASCII)。
  • 输入了控制键和切换键,如何处理 ?

    • 0040:17对应的内存单元存放:键盘状态字节

      | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
      | ——— | ———— | ———- | ————— | —— | —— | ———- | ———- |
      | Insert | CapsLock | NumLock | ScrollLock | alt | ctrl | 左shift | 右shift |

(三)执行int 9中断例程

  • BIOS 中提供的处理键盘输入的int 9中断例程的工作
    • (1) 读出60H 端口中的扫描码
    • (2) 根据扫描码分情况对待
      • 如果是字符键的扫描码,将该扫描码和它所对应的字符码(即 ASCI码)送入内存中的BIOS键盘缓冲区
      • 如果是控制键(比如 Ctrl)和切换键(比如 CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节 )写入内存中存储状态字节的单元。
    • (3) 对键盘系统进行相关的控制,如向相关芯片发出应答信息。

6.11.4 定制键盘输入处理

(一)PC机键盘的处理过程(int9中断例程)

(二)编程任务

在屏幕中间依次显示’a’~’z’,并可以让人看清,在显示的过程中,按下Esc键后,改变显示的颜色。

方案:尽可能忽略硬件处理细节,充分利用BIOS提供的int9中断例程对这些硬件细节进行处理;在改写后的中断例程中满足特定要求,并能调用BIOS的原int9中断例程。

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
;依次显示'a'~'z'
assume cs:code
stack segment
db 128 dup (0)
stack ends

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

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

mov ax,4c00h
int 21h
;定义延时函数
delay: push ax
push dx
mov dx,10h
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
code ends
end start

接下来的工作:按下 ESc键后,改变显示的颜色!
原理:键盘输入到达60h端口后,就会引发 9号中断,CPU 则转去执行int 9中断例程。

  • 按下 Esc键后改变显示的颜色
    • 编写int 9中断例程改变显示的颜色
      • (1) 从60h端口读出键盘的输入
        • in al 60h
      • (2) 调用BlOS的int9中断例程,处理硬件细节
        • ① 关于中断处理程序入口地址面对的问题
          • 要将中断向量表中的int 9中断例程的入口地址改为自编的中断处理程序的入口地址。
          • 在新中断处理程序中调用原来的int 9中断例程,还需要是原来的int9中断例程的地址。
          • 解决方法:保存原中断例程入口地址
          • 将原来int 9中断例程的偏移地址和段地址保存在ds:[0]ds:[2]单元中,在需要调用原来的int 9中断例程时候,到ds:[0]、ds:[2]找到
        • ② 如何调用原int 9指令的中断例程口
          • int 9己改,但仍然需要调用原int 9指令功能
          • 解决方法:模拟对原中断例程的调用
            (1) 标志寄存器入栈
            (2) IF=0,TF=0
            (3) CS、IP入栈
            (4) (IP)=((ds)×16+0),(CS)=((ds)×16+2)
      • (3) 判断是否为Esc的扫描码,如果是,改变显示的颜色后返回;如果不是,则直接返回。
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
88
89
90
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]
mov word ptr es:[9*4], offset int9
mov es:[9*4+2],cs

;显示'a'~'z
mov ax,0b800h
mov es,ax
mov ah,'a'
s: mov es:[160*12+40*2],ah
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]

mov ax,4c00h
int 21h

;定义延迟程序
delay: push ax
push dx
mov dx,10h
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

;定义中断例程
int9: push ax
push bx
push es
in al,60h
pushf
;pushf
pop bx
and bh,11111100b
push bx
popf
call dword ptr ds:[0] ;call的存在说明只是给esc增加了一个功能,并没有替换原有功能

cmp al,1 ;ESC扫描码1
jne int9ret
;改变颜色
mov ax,0bg00h
mov es,ax
inc byte ptr es:[160*12+40*2+1]

int9ret:pop es
pop bx
pop ax
iret

code ends
end start
6.11.5 改写中断例程的方法
6.11.6 用中断响应外设

(一)如何操作外部设备

以典型输入设计——键盘操作为例

硬件中断 int 9h BIOS中断 int 16h DOS中断 int 21h
由键盘上按下或松开一个键
时,如果中断是允许的,就
会产生int 9h中断,并转到
BIOS的键盘中断处理程序。
BIOS中断提供基本的键盘操作
功能号(AH)=
00H、10H 一从键盘读入字符
01H、11H 一读取键盘状态
02H、12H 一读取键盘标志
03H 一设置重复率
04H一设置键盘点击
05H 一字符及其扫描码进栈
在使用功能键和变换键的程序中很重要。
Dos中断提供丰富、便捷的功能调用
功能号(AH)=
01H 一 从键盘输入一个字符并回显
06H 一 读键盘字符
07H 一 从键盘输入一个字符不回显
08H 一 从键盘输入一个字符,不回显,检测CTRL-Break
0AH 一 输入字符到指定地址的缓冲
0BH - 读键盘状态
0CH -清除键盘缓冲区,并调用一种键盘功能

键盘缓冲区的实现
① 共16字
② 用环形队列,先进先出
③ 可存储15个按键扫描码

对键盘输入的处理的int 9h中断和int 16h中断:

(1)int 9h将键盘输入存入缓冲或改变状态字,键盘输入将引发9号中断,BIOS提供了int 9中断例程。

int 9中断例程从60h端口读出扫描码,并将其转化为相应的ASCII码或状态信息,存储在内存的指定空间(键盘缓冲区或状态字节)中。

键盘缓冲区中有16 个字单元,可以存储15个按键的扫描码和对应的入ASCII 码。

(2)BIOS提供了int 16h中断例程供程序员调用,以完成键盘的各种操作。

例:当(AH)=0时,读取键盘缓冲区功能:从键盘缓冲区中读取一个键盘输入,并且将其从缓冲区中删除。

(3)BIOS的int 9h中断例程和int 16h中断例程是一对相互配合的程序,int 9h中断例程向键盘缓冲区中写入,int 16h中断例程从缓冲区中读出。它们写入和读出的时机不同,int 9h中断例程在有键按下的时候向键盘缓冲区中写入数据而int 16h中断例程是在应用程序对其进行调用的时候,将数据从键盘缓冲区中读出。

补充解释:

int 9h硬件中断,这个中断历程呢是在有键摁下去的时候,向键盘缓冲区里边呢去写入数据,这个过程不受CPU控制,是单纯的外设键盘触发的外部硬件中断;

int 16h软件中断,它是在应用程序里边对它进行调用的时候,才将数据从键盘缓冲区中读出。

从cpu角度来讲,我不管你键盘什么时候去产生这样的呃动作,我需要的时候,我就通过int 16h去调用它,而对于int 9h号中断来讲,它和cpu没有关系,只要说有键摁下去了,就把它记下来。当这两个中断在应用程序里边我们合理的去设置的时候呢,他们相互配合呢帮助我们去读取这样一些数据。

(二)应用示例:更改屏幕颜色

要求:接收用户的键盘输入
输入“r”,将屏幕上的字符设置为红色;输入“g”,将屏幕上的字符设置为绿色;输入“b”,将屏幕上的字符设置为蓝色。

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
assume cs:code
code segment
start:
;调用中断,等待输入
mov ah,0
int 16h

;识别按键
mov ah,1
cmp al,'r'
je red
cmp al,'g'
je green
cmp al,'b'
je blue
jmp short sret

;设置屏幕颜色
red: shl ah,1
green: shl ah,1
blue: mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
and byte ptr es:[bx],11111000b
or es:[bxl,ah
add bx,2
loop s

sret: mov ax,4c00h
int 21h
code ends
end start

(三)应用:字符串的输入

问题:设计一个最基本的字符串输入程序,需要具备下面的功能:

(1) 在输入的同时需要显示这个字符串
(2) 一般在输入回车符后,字符串输入结束
(3) 能够删除已经输入的字符——用退格键。

程序的处理过程

(1) 调用int 16h读取键盘输入;
(2) 如果不是字符:①如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符,继续执行(1);②如果是Enter键,向字符栈中压入0,返回;
(3) 如果是字符键:字符入栈,显示字符栈中的所有字符,继续执行(1)。

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
; 该程序未完成
assume cs:code, ds:data
data segment ;“栈”空间
db 32 dup (?)
data ends

code segment
start:
mov ax, data
mov ds, ax
mov si, 0
mov dh, 12
mov dl, 20
call getstr

return: mov ax,4c00h
int 21h

;完整的接收字符串输入的子程序
getstr: push ax
getstrs:
;调用int 16h读取键盘输入
mov ah,0
int 16h

cmp al,20h
jb nochar ;小于20h为非字符
;字符入栈
;显示栈中的字符
jmp getstrs

;处理非字符
nochar:
cmpah,oeh ;退格键的扫描码
je backspace
cmp ah,1ch ;回车键的扫描码
je enter
jmp getstrs

;对退格键、回车键的处理
;退格
backspace:
;字符出栈
;显示栈中的字符
jmp getstrs
;回车
enter: mov al,0
;0字符入栈
;显示栈中的字符

pop ax
ret ;getstr结束

code ends
end start

6.12 读写磁盘

磁盘,包括“软盘”、“硬盘”(不过软盘已经退出历史舞台了)

(一)BIOS对磁盘的操作

  • 用BlOS int 13h对磁盘进行读操作
    • 入口参数:
      • (ah) = 2(2表示读扇区)
      • (al)=读取的扇区数
      • (ch)=磁道号 ,(cl)=扇区号
      • (dh)=磁头号(对于软盘即面号,一个面用一个磁头来读写)
      • (dl)=驱动器号:软驱从0开始,0:软驱A,1:软驱B,硬盘从80h开始,80h:硬盘C,81h:硬盘D
      • es:bx指向接收从扇区读入数据的内存区
    • 返回参数:
      • 操作成功:(ah)=0,(al)=读入的扇区数
      • 操作失败:(ah)=出错代码

例子:读取c盘0面0道1扇区的内容到内存单元0:200

1
2
3
4
5
6
7
8
9
10
mov ax,0
mov es,ax
mov bx,200h ;读入0:200h
mov al,1 ;1个扇区
mov ch,0 ;0磁道
mov cl,1 ;1扇区
mov dl,80h ;C盘
mov dh,0 ;0面
mov ah,2 ;读扇区
int 13h
  • 用BlOS int 13h对磁盘进行写操作

例子:将0:200中的内容写入C盘0面0道1扇区

1
2
3
4
5
6
7
8
9
10
mov ax,0
mov es,ax
mov bx,200h ;写0:200h
mov al,1 ;写1个扇区
mov ch,0 ;0磁道
mov cl,1 ;1扇区
mov dl,80h ;C盘
mov dh,0 ;0面
mov ah,3 ;3号写入功能
int 13h

(二)DOS中断对磁盘文件的支持—int 21H

  • 功能39H
    • 功能描述:用指定的驱动器和路径创建一个新目录
    • 入口参数:
      • AH = 39H
      • DS:DX=指定路径的字符串地址(以0为字符串的结束标志)
    • 出口参数:
      • CF=0——创建成功
      • CF=1——创建失败,AX=错误号(03H或05H),其含义见错误代码表
  • Copyrights © 2015-2024 wjh
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信