王爽《汇编语言》学习笔记

文章目录

前言:本笔记主要记录自己在学习过程中的易错、易混淆点,单看笔记可能无法串联知识点,需要结合书本才行。

一、基础知识

1、基础概念

机器指令:CPU能直接识别并执行的二进制数字。

  • 计算机将机器指令转变为一列高低电平,以使计算机的电子器件受到驱动,进行运算。

汇编指令:机器指令的助记符,同机器指令一一对应。

指令:指令通常由操作码和地址码(操作数)两部分组成

指令集:每种CPU都有自己的汇编指令集。

机器语言:机器指令的集合。

汇编语言由3类指令组成。

  • 汇编指令
  • 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
  • 其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码。

编译器:能将汇编指令转换成机器指令的翻译程序。

img

在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。

  • 磁盘不同于内存,磁盘上的数据或程序如果不读到内存中,就无法被CPU使用。

2、存储器

随机存储器(RAM)在程序的执行过程中可读可写,但必须带电存储,关机后存储的内容丢失。

只读存储器(ROM)在程序的执行过程中只读,关机后数据不丢失。

3、三种外部总线

CPU要想进行数据的读写,必须和外部器件(芯片)进行以下三类信息的交互:

  • 存储单元的地址(地址信息)
  • 器件的选择,读或写的命令(控制信息)
  • 读或写的数据(数据信息)

那CPU是通过什么将地址、数据和控制信息传到存储器芯片中的呢?

答:在计算机中专门有连接CPU和其他芯片的导线,通常称为总线 (物理上来讲,就是一根根导线的集合)

  • 地址总线:CPU通过地址总线来指定存储单元的地址

    一个CPU有N根地址线,则可以说这个CPU的地址总线宽度为N。这样的CPU最多可以寻找 2^n 个内存单元。

  • 数据总线:数据总线的宽度决定了CPU和外界的数据传送速度。

    8根数据总线一次可以传送一个8位二进制数据(即一个字节)。

  • 控制总线:有多少根控制总线,就意味着CPU对外部器件提供多少种控制。

4、CPU对外设的控制

CPU对外设都不能直接控制,如显示器、音箱、打印机等。

直接控制这些设备进行工作的是插在扩展插槽上的接口卡。CPU可以直接控制这些接口卡,从而实现CPU对外设的间接控制。

如:CPU无法直接控制显示器,但CPU可以直接控制显卡,从而实现对显示器的间接控制。

5、内存地址空间

CPU将系统中各类存储器看作一个逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。

CPU将各类存储器看作一个逻辑存储器:

img

我们在基于一个计算机硬件系统编程的时候,必须知道这个系统中的内存地址空间分配情况。因为当我们在某类存储器中读写数据的时候,必须知道它的第一个单元的地址和最后一个单元的地址,才能保证读写操作是在预期的存储器中进行。

二、寄存器

1. 寄存器

CPU由运算器、控制器、寄存器等器件构成,这些器件靠内部总线相连。

运算器进行信息处理;控制器控制各种器件进行工作;寄存器进行信息存储;内部总线连接各种器件,在它们之间进行数据的传送。

8086CPU为了兼容上一代的8位寄存器,AX,BX,CX,DX这四个寄存器可以拆开成两个 独立的 8位的寄存器来使用。分别为AH,AL, BH,BL, CH,CL, DH,DL。低八位(编号0-7)构成L寄存器,高八位构成H寄存器。

汇编指令或寄存器名称不区分大小写。

在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的。

2. 8086CPU 给出物理地址的方法

CPU通过地址总线送入存储器的,必须是一个内存单元的物理地址。

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

地址加法器采用 物理地址 = 段地址×16 + 偏移地址 的方法用段地址和偏移地址合成物理地址。

img

“段地址x16” 其实就是二进制数左移4位。

拓展:一个数据的十六进制形式左移1位,相当于乘以16;一个数据的十进制形式左移1位,相当于乘以10;一个X进制的数据左移1位,相当于乘以X。

3. 段寄存器

CS为代码段寄存器,IP为指令指针寄存器,它们的内容提供了CPU要执行指令的地址。

8086CPU的工作过程简要描述:

  1. 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
  2. IP=IP+所读取指令的长度,从而指向下一条指令;
  3. 执行指令。转到步骤1,重复这个过程。

img

mov指令(传送指令)不能用于设置CS、IP的值,8086CPU提供转移指令修改CS、IP的内容。

  • jmp 段地址:偏移地址:用指令中给出的段地址修改CS,偏移地址修改IP。如:jmp 2AE3:3
  • jmp 某一合法寄存器:用寄存器中的值修改IP。如:jmp ax,在含义上好似:mov IP,ax

4. 字

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

对于字,只要在mov指令中给出16位的寄存器(如,用AX而不是AL/AH)就可以进行16位数据的传送。

5. DS 和 [address]

DS寄存器:存放要访问数据的段地址

[address]:表示一个偏移地址为address的内存单元(8086CPU自动取ds中的数据为内存单元的段地址)。

通过段地址和偏移地址即可定位内存单元。

6. 栈(SS和SP)

8086CPU提供入栈和出栈指令,即可以将一段内存当作栈来使用。

8086CPU的入栈和出栈操作都是以字为单位进行的!

问:CPU如何知道栈顶的位置?

答:8086CPU中由两个寄存器,段寄存器SS和寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素

入栈

push ax表示将寄存器ax中的数据送入栈中,由两步完成。

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

入栈时,栈顶从高地址向低地址方向增长。

出栈

pop ax表示从栈顶取出数据送入ax,由以下两步完成。

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

8086CPU中没duan有记录栈顶上限和栈底的寄存器,因此需要程序员自己操心栈顶超界的问题。

CPU将内存中的某段内容当作 代码,是因 CS:IP 指向了那里;CPU将内存中的某段内容当作 栈,是因为 SS:SP 指向了那里。一定要弄清楚什么是我们的安排以及如何让CPU按照我们的安排行事。

三、第一个程序

先看一段简单的汇编语言源程序

assume cs:codesg

codesg segment

	mov ax,0123H
	mov bx,0456H
	add ax,bx
	add ax,ax

	mov ax,4c00
	int 21H

codesg ends

end

对程序进行说明:

  • XXX segment ··· ··· XXX ends
    • segment和ends成对出现,代表一个段的开始和结束。
    • 一个汇编程序是由多个段组成的,这些段被用来存放代码,数据或当作栈空间来使用。一个有意义的汇编程序至少要有一个段(存放代码)。
  • end
    • end:一个汇编程序结束的标记,遇到end后编译器停止编译。
  • assume
    • 含义为“假设”,假设某一个段寄存器和程序中的某一个用segment ···ends 定义的段关联。
    • 可以理解为将这个段寄存器指向程序段的段地址
  • 标号(codesg)
    • 一个标号指代一个地址
  • 程序返回 mov ax, 4c00 int 21H
    • 这两条指令代表程序返回

1.汇编程序从写出到执行的过程

img

2. 程序执行过程

DOS系统中 .EXE文件中的程序的加载过程

img

四、[BX] 和 loop指令

1. [bx] 和 loop指令

[bx] :同样表示一个内存单元,它的偏移地址在bx中,段地址默认在ds中。

loop(循环)指令:loop 标号,CPU执行loop指令的时候,要进行两步操作:

  1. (cx) = (cx) - 1;
  2. 判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行。

程序示例:编程计算2^12。

assume cs:code 

code segment 
	mov ax, 2
	
	mov cx, 11 
s:  add ax, ax 	;汇编中,这的标号s 实际标识了一个地址,这个地址处有一条指令:add ax,ax。
	loop s     
	
	mov ax,4c00h 
	int 21h 
code ends 
end

用 cx 和 loop 指令相配合实现循环功能的程序框架如下:

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

2. Debug和masm编译器对指令的不同处理

形如 mov ax,[idata] 的指令在Debug和masm中有着不同的解释:

Debug是将它解释为 “[idata]” 是一个内存单元,“idata” 是内存单元的偏移地址;

而编译器masm中则是将 “[idata]” 解释为“idata”。

  • 解决方法1:先将偏移地址送入BX,然后再使用mov ax,[bx]

  • 解决方法2:直接在 “[ ]” 的前面显式地给出段地址所在的段寄存器,如 mov al, ds:[0]

    这种写法通过编译器之后会变成Debug中的 mov al,[0]

拓展:

这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”,“cs:”,“ss:”,“es:”,在汇编语言中称为段前缀

3. loop 和 [bx] 的联合应用

直接看例子:

计算ffff:0 ~ ffff:b单元中的数据的和,结果存储在 dx 中

分析:

  1. 这些内存单元都是字节型数据范围0 ~ 255 ,12个这样的数据相加,结果不会大于65535,可以在dx中存放下。
  2. 对于8位数据不能直接加到16位的寄存器 dx 中。
  3. 如果仅向 dl 中累加 12 个 8 位数据,很有可能造成进位丢失。

解决方案:

用一个16位寄存器来做中介。将内存单元中的8位数据赋值到一个16位寄存器a中,再将ax中的数据加到dx,从而使两个运算对象的类型匹配并且不会越界。

assume cs:code 

code segment 
	mov ax, 0ffffh ;在汇编源程序中,数据不能以字母开头,所以要在前面加0
	mov ds, ax 
	mov bx, 0   ;初始化ds:bx指向ffff:0
	mov dx, 0   ;初始化累加寄存器dx,(dx)= 0
	
	mov cx, 12  ;初始化循环计数寄存器cx,(cx)= 12
s:  mov al, [bx]
	mov ah, 0
	add dx, ax  ;间接向dx中加上((ds)* 16 +(bx))单元的数值
	inc bx      ;ds:bx指向下一个单元
	loop s 
	
	mov ax, 4c00h 
	int 21h 
code ends 
end

4. 安全的编程空间

之前没有提到的一个问题,如果在写程序之前不关注要操作的内存,直接开始使用,如果改写了内存中重要的系统数据,可能会引起系统崩溃。可见,在不能确定一段内存空间中是否存放着重要数据或代码的时候,不能随意向其中写入内容。

一般操作系统和合法程序都不会使用0:200~0:2ff这256个字节的空间,所以可以使用这段安全的空间。

学习汇编语言,要通过它来获得底层的编程体验,理解计算机底层的基本工作机理。所以我们尽量直接对硬件编程,而不理会操作系统。

这在纯DOS方式(实模式)下是可以做到的,但在windows或Unix这种运行与CPU保护模式的操作系统上是不可能的,因为这种操作系统已经将CPU全面严格的管理了。

五、包含多个段的程序

一个问题:根据什么设置CPU的 CS:IP 指向程序的第一条要执行的指令?

这一点,是由可执行文件中的描述信息指明的。在一个程序文件中,用伪指令end描述程序的结束和程序的入口。

在编译,连接后,由“end start”指明的程序入口被转化为一个入口地址,存储在可执行文件的描述信息中。

当程序被加载入内存之后,加载者从程序的可执行文件的描述信息中读到程序的入口地址,设置CS:IP。这样CPU就从我们希望的地址处开始执行。

在代码段中使用数据

示例:利用栈,将程序中定义的数据逆序存放

assume cs:codesg 

codesg segment 
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ; 0:0~0:15单元
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 0:16~0:47单元的空间作为栈使用
			
start:	mov ax, cs 
	    mov ss, ax 
	    mov sp, 30h ;将设置栈顶ss:sp指向栈底cs:30
		
	    mov bx, 0
	    mov cx, 8
	 s: push cs:[bx]
	    add bx, 2
	    loop s    ;以上将代码段0~15单元中的8个字型数据依次入栈
		
	    mov bx, 0
		mov cx, 8
	s0:	pop cs:[bx]		
		add bx,2
		loop s0   ;以上依次出栈8个字型数据到代码段0~15单元中
			
		mov ax,4c00h 
		int 21h 
codesg ends 
end start	;指明程序的入口在start处

在描述dw的作用时,不仅可以说用它来定义数据,也可以说用它来开辟内存空间留给之后的程序使用。

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

assume cs:code,ds:data,ss:stack 

data segment 
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
data ends 

stack segment 
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;0-31单元
stack ends 

code segment 
start:	mov ax, stack	;将名称为“stack”的段的段地址送入ax
		mov ss, ax
		mov sp, 20h  	;设置栈顶ss:sp指向stack:20
		
		mov ax, data 	;将名称为“data”的段的段地址送入ax
		mov ds, ax   	;ds指向data段			
		mov bx, 0    	;ds:bx指向data段中的第一个单元
		
		mov cx, 8
	s:  push [bx]
		add bx, 2
		loop s       ;以上将data段中的0~15单元中的8个字型数据依次入栈
			
		mov bx, 0
		
		mov cx, 8
	s0:	pop [bx]
		add bx, 2
		loop s0      ;以上依次出栈8个字型数据到data段的0~15单元中
			
		mov ax, 4c00h 
		int 21h 
code ends
end start

总之,CPU到底如何处理我们定义的段中的内容,是当作指令执行,当作数据访问,还是当作栈空间,完全是考程序中具体的汇编指令,和汇编指令对 CS:IP、SS:IP、DS 等寄存器 的设置来决定的。

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

1. and 和 or 指令

and指令:逻辑与指令,按位进行与运算。

例如指令:

mov al, 01100011B
and al, 00111011B

执行后:al=00100011B

or指令:逻辑或指令,按位进行或运算。

例如指令:

mov al, 01100011B
or al, 00111011B
执行后:al=01111011B

2.关于ASCII码

世界上有很多编码方案,有一种方案叫做ASCII编码,是在计算机系统中通常被采用的。简单地说,所谓编码方案,就是一套规则,它约定了用什么样的信息来表示现实对象。比如说,在ASCII编码方案中,用 61H 表示 “a”, 62H 表示 “b”。一种规则需要人们遵守才有意义。

在文本编辑过程中,我们按一下键盘的a键,就会在屏幕上看到“a”。这是怎样一个过程呢?

我们按下键盘的a键,这个按键的信息被送入计算机,计算机用ASCII码的规则对其进行编码,将其转化为 61H 存储在内存的指定空间中; 文本编辑软件从内存中取出 61H ,将其送到显卡上的显存中; 工作在文本模式下的显卡,用ASCII码的规则解释显存中的内容,61H被当作字符“a”,显卡驱动显示器,将字符“a”的图像画在屏幕上。

3. 大小写字符转换的问题

在汇编语言中,用 '···' 的方式指明数据是以字符形式给出的,编译器会自动将它们转化为ASCII码。

小写字母的ASCII码值比大写字母的ASCII码值大20H。

重新观察:就 ASCII 码的二进制形式来看,除第五位(位数从0开始计算)外,大写字母和小写字母的其他各位都一样。大写字母 ASCII 码的第5位为0,小写字母的第5位为1。

程序示例:将datasg中的第一个字符串转化为大写,第二个字符串转化为小写

assume cs:codesg, ds:datasg 

datasg segment 
	db 'BaSiC'
	db 'iNfOrMaTion'
datasg end

codesg segment 
start:	mov ax, datasg 
		mov ds, ax	;设置ds 指向 datasg段
		
		mov bx, 0	;设置(bx)=0,ds:bx指向'BaSic'的第一个字母
		
		mov cx, 5     	 ;设置循环次数5,因为'Basic'有5个字母
	 s: mov al, [bx]     ;将ASCII码从ds:bx所指向的单元中取出
		and al, 11011111B ;将al中的ASCII码的第5位置为0,变为大写字母
		mov [bx], al	 ;将转变后的ASCII码写回原单元
		inc bx		     ;(bx)加1,ds:bx指向下一个字母
		loop s 
		
		mov bx, 5		;设置(bx)=5,ds:bx指向,'iNfOrMaTion'的第一个字母
		
		mov cx, 11		;设置循环次数11,因为'iNfOrMaTion'有11个字母
     s0: mov al, [bx]
		or al, 00100000B ;将a1中的ASCII码的第5位置为1,变为小写字母
		mov [bx], al 
		inc bx
		loop s0
		
		mov ax, 4c00h 
		int 21h 
codesg ends

4. [bx+idata]

除了前面使用 [bx] 来指明一个内存单元外,还可以使用一种更灵活的方式来指明内存单元: [bx+idata] 表示一个内存单元,它的偏移地址为 (bx)+idata(bx中的数值加idata)的内存单元。

数字化的描述为:(ax)=((ds)*16+(bx)+idata)

也可写为[idata+bx],还可写为:idata[bx],[bx].idata

5. SI和DI寄存器

si 和 di 功能和BX相似,但si 和 di 不可以分为两个8位寄存器。

[bx+si]和[bx+di]的含义相似:

[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)

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

该指令也可以写成如下格式:mov ax, [bx][si]

[bx+si+idata]和[bx+di+idata]的含义相似:

[bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+(si)+idata

··· ···

6. 不同的寻址方式的灵活应用

总结一下上面几种定位内存地址的方法:

  • [idata]用一个常量表示偏移地址,可用于直接定位一个内存单元
  • [bx]用一个变量表示偏移地址,可用于间接定位一个内存单元
  • [bx+idata]用一个常量和一个变量表示偏移地址,可在一个起始地址的基础上用变量间接定位一个内存单元
  • [bx+si]用两个变量表示偏移地址
  • [bx+si+idata]用两个变量和一个常量表示偏移地址

程序示例:将 datasg 段中每个单词的头一个字母改为大写字母

assume cs:codesg,ds:datasg,ss:stacksg 

datasg segment
	db 'ibm            ' ;16个字节
	db 'dec            ' 
	db 'dos            '
	db 'vax            ' 
datasg ends 

stacksg segment 		;定义一个段,用来做栈段,容量为16个字节
	dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends 

codesg segment 
start:	mov ax, stacksg 
		mov ss, ax
		mov sp, 16 
		mov ax, datasg 
		mov ds, ax 
		mov bx, 0 
			
		;cx为默认循环计数器,二重循环只有一个计数器,所以外层循环先保存cx值,再恢复,采用栈保存
		mov cx, 4
	s0:	push cx		;将外层循环的cx值入栈
		mov si, 0
		mov cx, 3	;cx设置为内层循环的次数
		
	s:	mov al, [bx+si]
		and al, 11011111b ;每个字符转为大写字母
		mov [bx+si], al 
		inc si
		loop s 
			
		add bx, 16 
		pop cx		;从栈顶弹出原cx的值,恢复cx
		loop s0 	;外层循环的loop指令将cx中的计数值减1
			
		mov ax,4c00H 
		int 21H 
codesg ends
end start

一般来说,在需要暂存数据的时候,我们都应该使用栈。

七、数据处理的两个基本问题

1. bx、si、di和bp

在8086CPU中,只有这4个寄存器可以用在“[…]”中来进行内存单元的寻址。

在[ ]中,这4个寄存器可以单个出现,或只能以4种组合出现:bx和si、bx和di、bp和si、bp和di。(看清楚关系)

只要在[…]中使用寄存器bp,而指令中没有显性地给出段地址, 段地址就默认在ss中。

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

绝大多数机器指令都是进行数据处理的指令,处理大致可分为3类:读取、写入、运算。

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

示例:指令举例

img

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

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

(1) 立即数(idata)

mov ax, 1     
add bx, 2000h          
or bx, 00010000b
mov al, 'a'

(2) 寄存器

mov ax, bx     
mov ds, ax 
push bx 
mov ds:[0], bx 
push ds 
mov ss, ax
mov sp, ax

(3) 段地址(SA)和偏移地址(EA)

mov ax, [0]
mov ax, [di]
mov ax, [bx+8]
mov ax, [bx+si]
mov ax, [bx+si+8]   

mov ax, [bp]
mov ax, [bp+8]
mov ax, [bp+si]
mov ax, [bp+si+8]   

mov ax, ds:[bp]
mov ax, es:[bx]
mov ax, ss:[bx+si]
mov ax, cs:[bx+si+8] 

4. 寻址方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TzHjb1Bj-1637678343673)(H:\typora_Note\picture\image-20210625122127619.png)]

5. 指令要处理的数据有多长

8086CPU的指令,可以处理两种尺寸的数据,byte 和 word。所以在机器指令中要指明指令进行的是字操作还是字节操作。

  1. 通过寄存器名指明要处理的数据的尺寸。
    例: mov ax, ds:[0] ; 寄存器al指明了数据为1字长
  2. 在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte。
    例:mov byte ptr ds:[0], 1 ; byte ptr 指明了指令访问的内存单元是一字节
  3. 有些指令默认了访问的是字单元还是字节单元
    例:push [1000H] ; push 指令只进行字操作。

6. div指令(除法指令)

使用div做除法的时候应注意以下问题:

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

格式:

div reg
div 内存单元

7. 伪指令dd,dup

db 和 dw 定义字节型数据和字型数据。

dd 是用来定义 dword(double word,双字) 型数据的伪指令。

dup 是一个操作符,在汇编语言中同 db、dw、dd 等一样,也是由编译器识别处理的符号。

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

示例:

db 3 dup (0)       ;定义了3个字节,它们的值都是0,相当于db 0,0,0
db 3 dup (0, 1, 2) ;定义了9个字节,它们是0、1、2、0、1、2、0、1、2,相当于db 0,1,2,0,1,2,0,1,2
db 3 dup ('abc', 'ABC') ;定义了18个字节,它们是'abcABCabcABCabcABCC',相当于db'abcABCabcABCabcABC'

八、转移指令的原理

可以修改IP,或同时修改CS和IP的指令统称为转移指令。概括地讲,转移指令就是可以控制CPU执行内存中某处代码的指令。

8086CPU的转移行为有以下几类。

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

由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移。

  • 短转移IP的修改范围为-128 ~ 127。
  • 近转移IP的修改范围为-32768 ~ 32767。

8086CPU的转移指令分为以下几类。

  • 无条件转移指令(如:jmp)
  • 条件转移指令
  • 循环指令(如:loop)
  • 过程
  • 中断

1. 操作符offset

操作符offset 在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。

示例:

assume cs:codesg
codesg segment
	start:mov ax,offset start	;相当于 mov ax,0
	    s:mov ax,offset s		;相当于 mov ax,3
codesg ends
ends

2. jmp指令

jmp为无条件转移,可以只修改IP,也可以同时修改CS和IP;

jmp指令要给出两种信息:

  • 转移的目的地址
  • 转移的距离(段间转移、段内短转移,段内近转移)

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

jmp short 标号(转到标号处执行指令)(段内短转移)

示例:

assume cs:codesg
codesg segment
  start:mov ax,0
        jmp short s 
        add ax, 1	;被越过
      s:inc ax 		;程序执行后, ax中的值为 1 
codesg ends
end start

jmp short s指令的读取和执行过程:

  1. (CS)=0BBDH,(IP)=0006,CS:IP指向EB 03(jmp short s的机器码);
  2. 读取指令码EB 03进入指令缓冲器;
  3. (IP) = (IP) + 所读取指令的长度 = (IP) + 2 = 0008,CS:IP指向add ax,1;
  4. CPU指行指令缓冲器中的指令EB 03;
  5. 指令EB 03执行后,(IP)=000BH,CS:IP指向inc ax

在“jmp short 标号” 指令所对应的机器码中,并不包含转移的目的地址,而包含的是转移的位移。这个位移,是编译器根据汇编指令中的 “标号” 计算出来的。

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

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

还有一种和 “jmp short 标号” 功能相近的指令格式,jmp near ptr 标号,它实现的是段内近转移。

指令“jmp near ptr 标号”的功能为:(IP) = (IP) + 16位位移

  1. 16位位移 = 标号处的地址 - jmp指令后的第一个字节的地址;
  2. near ptr 指明此处的位移为16位位移,进行的是段内近转移;
  3. 16位位移的范围为-32768~32767,用补码表示;
  4. 16位位移由编译程序在编译时算出。

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

jmp far ptr 标号(段间转移,又称远转移)

指令 “jmp far ptr 标号” 功能如下:

  1. (CS) = 标号所在段的段地址;
  2. (IP) = 标号所在段中的偏移地址。
  3. far ptr 指明了指令用标号的段地址和偏移地址修改CS和IP。
assume cs:codesg
codesg segment
   start: mov ax, 0
		 mov bx, 0
          jmp far ptr  s
          db 256 dup (0) 
        s:add ax,1
          inc ax
codesg ends
end start

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

格式:jmp 16位 reg

功能:IP =(16位 reg)

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

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

(1)jmp word ptr 内存单元地址(段内转移)

功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。

示例:

mov ax, 0123H
mov ds:[0], ax
jmp word ptr ds:[0]
;执行后,(IP)=0123H

mov ax,0123H
mov [bx],ax
jmp word ptr [bx]
;执行后,(IP)=0123H

(2)jmp dword ptr 内存单元地址(段间转移)

功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。

  • (CS)=(内存单元地址+2)
  • (IP)=(内存单元地址)

示例:

mov ax, 0123H
mov ds:[0], ax
mov word ptr ds:[2], 0
jmp dword ptr ds:[0]
;执行后,(CS)=0,(IP)=0123H,CS:IP 指向 0000:0123

3. jcxz指令

jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移。

在对应的机器码中包含转移的位移,而不是目的地址。

对IP的修改范围都为-128~127。

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

操作:

当(cx) = 0时,(IP) = (IP) + 8位位移;

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

当(cx) ≠ 0时,什么也不做(程序向下执行)

从jcxz的功能中可以看出,“jcxz 标号” 的功能相当于:if((cx)==0) jmp short 标号

4. loop指令

loop指令为循环指令,所有的循环指令都是短转移。

在对应的机器码中包含转移的位移,而不是目的地址。

对IP的修改范围都为:-128~127。

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

操作:

  1. (cx) = (cx) - 1;
  2. 如果 (cx) ≠ 0,(IP) = (IP) + 8位位移。
  • 8位位移 = 标号处的地址 - loop指令后的第一个字节的地址;
  • 8位位移的范围为-128~127,用补码表示;
  • 8位位移由编译程序在编译时算出。

如果(cx)= 0,什么也不做(程序向下执行)。

从loop 的功能中可以看出,“loop 标号” 的功能相当于:(cx)--; if((cx) != 0) jmp short 标号

5. 编译器对转移位移超界的检测

根据位移进行转移的指令,它们的转移范围受到转移位移的限制,如果在源程序中出现了转移范围超界的问题,在编译的时候,编译器将会报错。

九、CALL 和 RET 指令

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

1. ret 和 retf

  • ret指令用栈中的数据,修改IP的内容,从而实现近转移;
  • retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。

CPU执行ret指令时,相当于进行:pop IP,具体进行下面两步操作:

  1. (IP) = ( (ss) * 16 + (sp) )
  2. (sp) = (sp) + 2

CPU执行retf指令时,相当于进行:pop IP, pop CS:具体进行下面四步操作:

  1. (IP) = ( (ss) * 16 + (sp) )
  2. (sp) = (sp) + 2
  3. (CS) = ( (ss) * 16 + (sp) )
  4. (sp) = (sp) + 2

示例:

assume cs:code 
stack seqment
	db 16 dup (0)
stack ends 

code segment
		mov ax, 4c00h
		int 21h 
 start:	mov ax, stack 
 		mov ss, ax
 		mov sp, 16
		mov ax, 0
		push ax 
		mov bx, 0
		ret 
code ends
end start
;ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令。

2. call 指令

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

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

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

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

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

操作:

  1. (SP)=(SP)-2
    ((SS) * 16 + (SP)) = (IP)
  2. (IP)=(IP)+16 位位移

CPU执行此种格式的call指令时,相当于进行 push IP jmp near ptr 标号

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

call far ptr 标号(段间转移)

操作:

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

CPU执行此种格式的call指令时,相当于进行:push CS push IP jmp far ptr 标号

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

call 16位reg

功能:

  1. (SP)=(SP)-2
  2. ((SS) * 16+(SP))=(IP)
  3. (IP)=(16为reg)

CPU执行此种格式的call指令时,相当于进行: push IP jmp 16位reg

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

有两种格式:

(1)call word ptr 内存单元地址

CPU执行此种格式的call指令时,相当于进行:push IP jmp word ptr 内存单元地址

(2)call dword ptr 内存单元地址

CPU执行此种格式的call指令时,相当于进行:push CS push IP jmp dword ptr 内存单元地址

3. call 和 ret 的配合使用

配合使用call 和 ret 实现子程序的框架:

assume cs:code
code segment
main: :
      :
	  call sub1			;调用子程序sub1
      :
	  mov ax,4c00h
      int 21h

sub1: :					;子程序sub1开始
	  :
	  call sub2			;调用子程序sub2
	  :
	  ret				;子程序返回

sub2: :					;子程序sub2开始
	  :
	  ret				;子程序返回
code ends
end main

4. mul 指令(乘法指令)

mul是乘法指令,使用 mul 做乘法时,应注意:

(1)相乘的两个数,要么都是8位,要么都是16位。

  • 8 位: 一个默认放在 AL 中,另一个放在 8位寄存器内存字节单元中;
  • 16 位: 一个默认放在 AX中,另一个放在 16 位寄存器内存字单元中。

(2)结果:

  • 8位:结果默认放在 AX 中;
  • 16位:高位默认在DX中,低位在 AX 中。

格式:mul regmul 内存单元

示例:

1.计算100*10
;100和10小于255,可以做8位乘法
mov al,100
mov bl,10
mul bl
;结果: (ax)=1000(03E8H) 

2.计算100*10000
;100小于255,可10000大于255,所以必须做16位乘法
mov ax,100
mov bx,10000
mul bx
;结果: (ax)=4240H,(dx)=000FH   (F4240H=1000000)

十、标志寄存器

CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可能不同)具有以下3种作用。

(1)用来存储相关指令的某些执行结果;

(2)用来为CPU执行相关指令提供行为依据;

(3)用来控制CPU的相关工作方式。

这种特殊的寄存器在8086CPU中,被称为标志寄存器(flag),其中存储的信息通常被称为程序状态字(PSW)。

flag寄存器不同于其他寄存器,它是按位起作用的(其余的寄存器是用来存放数据的),每一位都有专门的含义,记录特定的信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y1PkENcc-1637678343682)(H:\typora_Note\picture\image-20210629145021777.png)]

注意:

在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、div、inc、or、and等,它们大都是运算指令(进行逻辑或算术运算);有的指令的执行对标志寄存器没有影响,比如,mov、push、pop等,它们大都是传送指令。

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

1. ZF标志(零标志位)

flag 的第6位,它记录相关指令执行后,其结果是否为0。

  • 如果结果为0,那么 zf = 1(表示结果是0);
  • 如果结果不为0,那么 zf = 0。
mov ax, 2
sub ax, 1 
;执行后,结果不为0,则zf = 0

2. PF标志(奇偶标志位)

flag 的第2位,它记录相关指令执行后,其结果的所有 bit 位中1的个数是否为偶数。

  • 如果1的个数为偶数,pf = 1;
  • 如果为奇数,那么pf = 0。
mov al, 1
or al, 2  
;执行后,结果为00000011B,其中有2(偶数)个1,则pf = 1;

3. SF标志(符号标志位)

flag 的第7位,它记录相关指令执行后,其结果是否为负。

  • 如果结果为负,sf = 1;
  • 如果非负,sf = 0。

计算机中通常用补码表示数据,一个数可以看成有符号数或无符号数。

如:

00000001B,可以看成无符号1,或有符号+1
10000001B,可以看成无符号129,或有符号-127

CPU在执行add等指令的时候,就包含了两种含义:

  • 可以将add指令进行的运算当作无符号数的运算;
  • 也可以将add指令进行的运算当作有符号数的运算。

关键在于我们的程序需要用到哪一种结果。

总结:SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值。

4. CF标志(进位标志位)和 OF寄存器(溢出标志位)

CF标志(进位标志位)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D0efwO2R-1637678343684)(H:\typora_Note\picture\image-20210629152454655.png)]

例如:

两个8位数据:98H + 98H,将产生进位。

两个8位数据:97H - 98H,将产生借位。

8086CPU中flag的CF位就是用来记录这个 进位/借位值的。

OF寄存器(溢出标志位)

什么是溢出呢?

指令运算的结果用8位寄存器或一个内存单元存放,那么对于8位的有符号数据,机器能表示的范围就是-128~127,16位同理。在进行有符号运算的时候,如果结果超过了机器能表示的范围称为“溢出”。

flag 的第11位,一般情况下,OF记录了 有符号数运算 的结果是否发生了溢出。

  • 如果发生溢出,OF = 1;
  • 如果没有,OF = 0。

注意!!

CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位。

CPU在执行add等指令的时候,就包含了两种含义:无符号数运算和有符号数运算。

  • 对于无符号数运算,CPU用CF位来记录是否产生了进位;
  • 对于有符号数运算,CPU用OF位来记录是否产生了溢出,当然,还要用SF位来记录结果的符号。

示例:

mov al, 98
add al, 99  
;add执行后:CF=0,OF=1。
    
mov al,0F0H  
add al,88H  
;add执行后:CF=1,OF=1。

mov al,0F0H
add al,088H
;add执行后:CF=1,OF=0。

可以看出,CF和OF所表示的进位和溢出,是分别对无符号数和有符号数运算而言的,他们之间没有任何关系。

5. adc指令 和 sbb指令

adc指令

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

指令格式:adc 操作对象1, 操作对象2

功能:操作对象1 = 操作对象1 + 操作对象2 + CF

例如:

mov ax, 2
mov bx, 1
sub bx, ax  ;无符号运算借位CF=1,有符号运算OF = 0
adc ax, 1   
;执行后,(ax)= 4。adc执行时,相当于计算:(ax)+1+CF = 2+1+1 = 4。

加法可以分为两步来进行:(1)低位相加;(2)高位相加再加上低位相加产生的进位值。

注:inc和loop指令不影响CF位。

sbb指令

sbb是带借位减法指令,它利用了CF位上记录的借位值。

指令格式:sbb 操作对象1, 操作对象2

功能:操作对象1 = 操作对象1 - 操作对象2 - CF

例如:计算 003E1000H - 00202000H,结果放在ax,bx中

mov bx, 1000H
mov ax, 003EH
sub bx, 2000H
sbb ax, 0020H

6. cmp指令

cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。

cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。

cmp指令格式:cmp 操作对象1,操作对象2
功能:计算对操作对象1 - 操作对象2但不保存结果,仅仅根据结果对标志位进行设置。

例如:
指令cmp ax, ax,做(ax)-(ax)的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位。
指令执行后:zf=1,pf=1,sf=0,cf=0,of=0。

其实,可以通过执行cmp ax,bx ,相关标志位的值就可以判断结果:

若(ax)=(bx) 则(ax)-(bx)=0,所以:zf=1
若(ax)!=(bx) 则(ax)-(bx)!=0,所以:zf=0
若(ax)<(bx) 则(ax)-(bx)产生借位,所以:cf=1
若(ax)>=(bx) 则(ax)-(bx)不产生借位,所以:cf=0
若(ax)>(bx) 则(ax)-(bx)既不产生借位,结果又不为0,所以:cf=0且zf=0
若(ax)<=(bx) 则(ax)-(bx)既可能借位,结果可能为0,所以:cf=1或zf=1

实际上往往会出现溢出,所以,我们应该在考察sf(得知实际结果的正负)的同时考查of(得知有没有溢出),就可以得知逻辑上真正结果的正负,同时就可以知道比较的结果。

下面,以cmp ah,bh为例,总结一下CPU执行cmp指令后,sf 和 of 的值是如何来说明比较的结果的。

  • 若sf=1,而of=0;说明没有溢出,那么sf的计算结果正确;所以,(ah)<(bh)
  • 若sf=1,而of=1;说明出现了溢出,那么sf的结果相反;所以,(ah)>(bh)
  • 若sf=0,而of=1;说明有溢出,那么sf的结果相反;所以,(ah)<(bh)
  • 若sf=0,而of=0;说明没有溢出,那么sf的结果正确;所以,(ah)>=(bh)

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

“转移” 指的是它能够修改IP,而 “条件” 指的是它可以根据某种条件,决定是否修改IP。

如:jcxz 就是一个条件转移指令,它可以检测cx中的数值,如果(cx)=0,就修改IP,否则什么也不做。

所有条件转移指令的转移位移都是[-128,127]。

大多数条件转移指令都检测标志寄存器的相关标志位,根据检测的结果来决定是否修改IP。

这些条件转移指令通常都和cmp相配合使用,检测不同的标志位来达到不同的条件跳转效果:

下面是常用的根据无符号数的比较结果进行转移的条件转移指令。

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

j:jump,e:equal,ne:not equal,b:below,nb:not below,a:above,na:not above。

例:统计data中数值小于8的字节个数,用ax保存

···
data segment
	db 8,11,8,1,8,5,63,38
data ends
···
	mov ax,data
	mov ds,ax			
	mov ax,0			;初始化累加器
	mov bx,0			;ds:bx指向第一个字节
	mov cx,8
s:  cmp byte ptr [bx],8	 ;和8进行比较
	jne next			;如果不小于8 转到next,继续循环
	inc ax				;如果小于8 就将计数值加1
next:inc bx
	loop s
···
; 程序执行后:(ax)=2

8. DF标志(方向标志位)和串传送指令

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

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

格式:movsb
功能:将ds:si指向的内存单元中的字节送入es:di中,然后根据标志寄存器df位的值,将si和di递增或递减。

格式:movsw
功能:将ds:si指向的内存字单元中的字送入es:di中,然后根据标志寄存器df位的值,将si和di递增2或递减2。

movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用:
格式:rep movsb
功能:rep的作用是根据cx的值,重复执行后面的串传送指令。

8086CPU提供下面两条指令对df位进行设置。

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

示例:用串传送指令,将data段中第一个字符串复制到他后面的空间中

data segment
	db 'Welcome to masm!'
	db 16 dup (0)
data ends

	mov ax,data
	mov ds,ax
	mov si,0	;ds:si 指向 data:0
	mov es,ax
	mov di,16	;es:di 指向 data:0010
	mov cx,16	;(cx)=16,rep循环16次
	cld			;设置df=0,正向传送
	rep movsb
···

9. pushf 和 popf

pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中

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

十一、内中断

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

中断信息可以来自CPU的内部和外部(内中断,外中断)

1. 内中断的产生

内中断:当CPU的内部有以下需要处理的事情发生时,将产生中断信息,引发中断过程。

(1)除法错误,比如,执行div指令产生的除法溢出;
(2)单步执行;
(3)执行 into指令;
(4)执行 int指令。

CPU首先要知道,所接收到的中断信息的来源。8086CPU用称为中断类型码 的数据来标识中断信息的来源。

中断类型码为一个字节型数据,可以表示256种中断信息的来源(中断源)。

上述的4种中断源,在8086CPU中的中断类型码如下:

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

2. 中断处理程序 和 中断向量表

中断处理程序

我们编写的,用来处理中断信息的程序被称为中断处理程序。

根据CPU的设计,中断类型码的作用就是用来定位中断处理程序的。那如何根据8位的中断类型码得到中断处理程序的段地址和偏移地址呢?

中断向量表

CPU用8位的中断类型码通过 中断向量表找到相应的中断处理程序入口地址。(中断向量表就是中断向量的列表,中断向量就是中断处理程序的入口地址)

image-20210629173844010

  • 中断向量表在内存中保存,其中存放着256个中断源所对应的中断处理程序的入口。
  • CPU只要知道了中断类型码,就可以将中断类型码作为中断向量表的表项号,定位相应的表项,从而得到中断处理程序的入口地址。
  • 对于8086PC机,中断向量表指定放在内存地址0处。从0000:0000到0000:03FF的1024个单元存放中断向量表。(不能放在别处)
    在中断向量表中,一个表项存放一个中断向量,也就是一个中断处理程序的入口地址,对于8086CPU,这个入口地址包括段地址和偏移地址,所以一个表项占两个字(高地址字存放段地址,低地址字存放偏移地址)

3. 中断过程

CPU会自动根据中断类型找到对应的中断向量并设置CS和IP的值,CPU硬件完成这个工作的过程称为中断过程。

具体步骤:

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

4. iret指令

CPU随时都可能执行中断处理程序,所以中断处理程序必须一直存储在内存某段空间之中。而中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。

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

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

iret通常和硬件自动完成的中断过程配合使用。

iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序。

5. 除法错误中断的处理

当CPU执行div 等除法指令的时候,如果发生了除法溢出错误,将产生中断类型码为0的中断信息,CPU将检测到这个信息,然后引发中断过程,转去执行0号中断对应的中断处理程序。

mov ax, 1000h 
mov bh, 1
div bh 
;发生除法溢出错误

实例:编程,当发生除法溢出时,在屏幕中间显示“overflow!”,返回DOS。

分析:

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

程序框架:

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

do0:	显示字符串"overflow!"
		mov ax,4c00h
		int 21th
		
code ends
end start

完整程序:

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指向目的地址0000:0200
		mov cx, offset do0end - offset do0		;设置cx为传输长度
		cld				        ;设置传输方向为正
		rep movsb 
		
		;设置中断向量表
		mov ax, 0               
		mov es, ax
		mov word ptr es:[0*4], 200h
		mov word ptr es:[0*4+2], 0

      	mov ax,4c00h
      	int 21h

		;do0程序的主要任务是显示字符串
do0:	jmp short do0 start 
      	db "overflow!"

do0start:
      	 mov ax, cs
      	 mov ds, ax
      	 mov si, 202h			;设置ds:si指向字符串

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

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

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

6. 单步中断

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

关于Debug的内容,见王爽《汇编语言》P249-P250.

7. int指令

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

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

  1. 取中断类型码n
  2. 标志寄存器入栈,IF=0,TF=0
  3. CS,IP入栈
  4. (ip)=(n*4),(cs)=(n*4+2)
  5. 转去执行n号中断的中断处理程序

可以在程序中使用int指令调用任何一个中断的中断处理程序。

8. 编写中断例程

示例1:求2 * 3456²

assume cs:code
code segment
start: 
     mov ax, 3456 	;(ax)=3456
     int 7ch  		; 调用中断7ch的中断例程,计算ax中的数据的平方
     add ax, ax  
     adc dx, dx  	;存放结果,将结果乘以2
     mov ax,4c00h
     int 21h
code ends
end start 

示例2:
功能:将一个全是字母,以0结尾的字符串,转化为大写。
参数:ds:si指向字符串的首地址。
应用举例:将data段中的字符串转化为大写。

assume cs:code

data segment
	db 'conversation',0
data ends

code segment
start:   mov ax, data
		mov ds, ax
		mov si, 0
		int 7ch
		
		mov ax,4c00h
		int 21h
code ends
end start   

安装程序如下:

assume cs:code
code segment

start:  mov ax,cs
		mov ds,ax
		mov si,offset capital
		mov ax,0
		mov es,ax
		mov di,200h
		mov cx,offset capitalend - offset capital
		cld
		rep movsb

		mov ax,0
		mov es,ax
		mov word ptr es:[7ch*4],200h
		mov word ptr es:[7ch*4+2],0
		mov ax,4c00h
		int 21h

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
		iret
capitalend:nop

code ends
end start

9. BIOS和DOS所提供的中断例程

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

(1)硬件系统的检测和初始化程序;
(2)外部中断和内部中断的中断例程;
(3)用于对硬件设备进行I/O操作的中断例程;
(4)其他和硬件系统相关的中断例程。

BIOS 和 DOS 在所提供的中断例程中包含了许多程序员在编程的时候需要用到的小程序。程序员在编程的时候,可以用 int 指令直接调用 BIOS 和 DOS 提供的中断例程,来完成某些工作。

和硬件设备相关的DOS中断例程中,一般都调用了BIOS的中断例程。

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

BIOS和DOS提供的中断例程是如何安装到内存中的呢?

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

DOS中断例程应用

示例:在屏幕的5行12列显示字符串“Welcome to masm!”

assume cs:code 
 
data segment 
	db	'Welcome to masm',  '$' ;“$”本身不显示,只起到边界的作用
data ends 

code segment
start:	 mov ah, 2  ;置光标
		mov bh, 0  ;第0页
		mov dh, 5  ;dh中放行号
		mov dl, 12 ;dl中放列号
		int 10h 
		
		mov ax, data 
		mov ds, ax 
		mov dx, 0  ;ds:dx指向字符串的首地址data:0 
		mov ah, 9  
		int 21h 
		
		mov ax, 4c00h 
		int 21h 
code ends
end start

十二、端口

1. 端口基本概念

前面讲过,各种存储器都和CPU的地址线,数据线,控制线相连,CPU操控它们的时候,把它们都当作内存来对待,把它们总地看做一个由若干个存储单元构成的逻辑存储器,我们称这个逻辑存储器为内存地址空间。

在PC机系统中,和CPU通过总线相连的芯片除各种存储器外,还有以下3种芯片。

  1. 各种接口卡(如网卡、显卡)上的接口芯片,它们控制接口卡进行工作;
  2. 主板上的接口芯片,CPU通过它们对部分外设进行访问;
  3. 其他芯片,用来存储相关系统信息,或进行相应的输入输出处理。

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

  1. 都和CPU的总线相连(通过它们所在的芯片进行);
  2. CPU对他们进行读、写的时候都通过控制线向他们所在的芯片发出端口读写指令。

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

CPU可以直接读取下面三个地方的数据:

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

2. 端口的读写

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

端口的读写指令只有两条:in和out,分别用于从端口读取数据和往端口写入数据。

示例:

;对0~255以内的端口进行读写时
in al, 20h  	;从20h端口读入一个字节
out 20h, al  	;往20h端口写入一个字节

;对256~65535的端口进行读写时,端口号放在dx中
mov dx, 3f8h  	;将端口号3f8h送入dx
in al, dx  		;从3f8h端口读入一个字节
out dx, al 		;向3f8h端口写入一个字节

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

3. CMOS RAM芯片

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

  1. 包含一个实时钟和一个有128个存储单元的RAM存储器

  2. 该芯片靠电池供电。关机后内部的实时钟正常工作,RAM中的信息不丢失

  3. 128个字节的RAM中,内部实时钟占用 0~0dh 单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOS RAM中的系统信息。

  4. 该芯片内部有两个端口,端口地址为 70h 和 71h 。CPU通过这两个端口来读写CMOS RAM

  5. 70h为地址端口,存放要访问的 CMOS RAM 单元的地址;71h为数据端口,存放从选定的 CMOS RAM 单元中读取的数据,或要写入到其中的数据。

    可见,CPU对CMOS RAM的读写分两步进行,比如,读 CMOS RAM 的2号单元:
    ① 将2送入端口 70h;
    ② 从端口 71h 读出2号单元的内容。

CMOS RAM中存储的时间信息

在CMOS RAM中,存放着当前的时间:年、月、日、时、分、秒。这6个信息的长度都为1个字节,存放单元为:

024789

这些数据以BCD码的方式存放。

BCD码是以4位二进制数表示十进制数码的编码方法,如下表所示:

0123456789
0000000100100011010001010110011110001001

4. shl和shr指令

shl和shr是逻辑移位指令。

shl是逻辑左移指令,它的功能为:

  1. 将一个寄存器或内存单元中的数据向左移位;
  2. 将最后移出的一位写入CF中;
  3. 最低位用0补充。

shr是逻辑右移指令,它和 shl 所进行的操作刚好相反~

mov al, 01001000b 
shl al, 1 			;将a1中的数据左移一位
;执行后(al)=10010000b,CF=0。

mov al, 10000001b 
shr al, 1  ;将al中的数据右移一位
;执行后(al)=01000000b,CF=1。

十三、外中断

CPU在计算机系统中,除了能够执行指令,进行运算以外,还应该拥有I/O(输入输出)能力。

1. 接口芯片和端口

PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CPU将这些寄存器当作端口来访问。

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

即: CPU通过端口和外部设备进行联系。

2. 外中断信息

当CPU外部有需要处理的事情发生的时候,比如说,外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。

在PC系统中,外中断源有两类:

1、可屏蔽中断

可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置。

当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断。可屏蔽中断过程类似于内部中断:

  1. 获取中断类型码n(从外部通过总线输入)
  2. 标志寄存器入栈,IF=0,TF=0
  3. CS,IP入栈
  4. (IP)=(n*4),(CS)=(n*4+2)

中断过程中将IF置0的原因:在进入中断处理程序后,禁止其他的可屏蔽中断。如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1。

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

  • sti,设置IF=1;
  • cli,设置IF=0。

2、不可屏蔽中断

不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。

对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:

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

几乎所有由外设引发的外中断,都是可屏蔽中断。如键盘输入,不可屏蔽中断通常是在系统中有必须处理的紧急情况发生时通知CPU的中断信息。

3. PC机键盘的处理过程

1.键盘输入

键盘上每个按键都相当于一个开关,键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。

按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为 60h。

松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入 60h 端口中。

一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。

扫描码长度为一个字节,通码的第7位为0,断码的第7位为1。
即:断码 = 通码 + 80h
比如,g键的通码为 22h,断码为 a2h。

image-20210629205239022

image-20210629205303643

2.引发9号中断

键盘的输入到达60h端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9中断例程。

3.执行 int 9 中断例程

BIOS提供了int 9中断例程,用来进行基本的键盘输入处理,主要的工作如下:

  1. 读出60h端口中的扫描码;
  2. 如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区;
    如果是控制键(比如Ctrl)和切换键(比如CapsLock)的扫描码,则将其转变为状态字节(用二进制位记录控制键和切换键状态的字节)写入内存中存储状态字节的单元;
  3. 对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。

BIOS 键盘缓冲区(是系统启动后,BIOS用于存放int 9中断例程所接收的键盘输入的内存区)可以存储15个键盘输入,在该缓冲区中一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。

0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下。

状态置1
0右shift状态置1表示按下右shift键
1左shift状态置1表示按下左shift键
2Ctrl状态置1表示按下Ctrl键
3Alt状态置1表示按下Alt键
4ScrollLock状态置1表示Scroll指示灯亮
5NumLock状态置1表示小键盘输入的是数字
6CapsLock状态置1表示输入大写字母
7Insert状态置1表示处于删除态

4. 编写int 9中断例程

示例,见王爽《汇编语言》P276-P284

CPU对外设输入的通常处理方法:

  1. 外设的输入送入端口;
  2. 向CPU发出外中断(可屏蔽中断)信息;
  3. CPU检测到可屏蔽中断信息,如果IF=1,CPU在执行完当前指令后响应中断,执行相应的中断例程;
  4. 可在中断例程中实现对外设输入的处理。

端口和中断机制,是CPU进行I/O的基础。

十四、指令系统总结

对8086CPU的指令系统进行一下总结。读者若要详细了解8086指令系统中的各个指令的用,可以查看有关的指令手册。

8086CPU提供以下几大类指令。

  1. 数据传送指令
    如:mov、push、pop、pushf、popf、xchg 等都是数据传送指令,这些指令实现寄存器和内存、寄存器和寄存器之间的单个数据传送。
  2. 算术运算指令
    如:add、sub、adc、sbb、inc、dec、cmp、imul、idiv、aaa等都是算术运算指令,这些指令实现寄存器和内存中的数据的算数运算。它们的执行结果影响标志寄存器的sf、zf、of、cf、pf、af位。
  3. 逻辑指令
    如:and、or、not、xor、test、shl、shr、sal、sar、rol、ror、rcl、rcr等都是逻辑指令。除了not指外,它们的执行结果都影响标志寄存器的相关标志位。
  4. 转移指令
    可以修改IP,或同时修改CS和IP的指令统称为转移指令。转移指令分为以下几类。
    (1)无条件转移指令,比如,jmp;
    (2)条件转移指令,比如,jcxz、je、jb、ja、jnb、jna等;
    (3)循环指令,比如,loop;
    (4)过程,比如,call、ret、retf;
    (5)中断,比如,int、iret。
  5. 处理机控制指令
    这些指令对标志寄存器或其他处理机状态进行设置,如:cld、std、cli、sti、nop、clc、cmc、stc、hlt、wait、esc、lock等都是处理机控制指令。
  6. 串处理指令
    这些指令对内存中的批量数据进行处理,如:movsb、movsw、cmps、scas、lods、stos等。若要使用这些指令方便地进行批量数据的处理,则需要和rep、repe、repne 等前缀指令配合使用。

十五、 直接定值表

讨论如何有效合理地组织数据,以及相关的编程技术。

1. 描述单元长度的标号

在之前,我们一直在代码段中使用标号来标记指令、数据、段的起始地址。这些标号仅仅表示了内存单元的地址。

如:

assume cs:code
code segment
         a : db 1,2,3,4,5,6,7,8  ;在a后面加有":"
         b : dw 0
start :mov si,offset a
         mov bx,offset b
         mov cx,8
    s : mov al,cs:[si]
         mov ah,0
         add cs:[bx],ax
         inc si
         loop s
         mov ax,4c00h
         int 21h
code ends
end start

注意:在后面加有 “ :” 的地址标号,只能在代码段中使用,不能在其他段中使用。

现在我们引出新的概念:数据标号,这种标号 不但表示内存单元的地址,还表示了内存单元的长度(即此标号处的单元,是一个字节单元还是字单元,还是双字单元)。

上面的程序现在可以写成这样:

assume cs:code
code segment
          a db 1,2,3,4,5,6,7,8 	;标号a、b后面没有":"
          b dw 0               
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

在code段中a和b后并没有 ”:” 号,这种写法同时描述内存地址和单元长度的标号。

标号a ,描述了地址 code:0 和从这个地址开始后的内存单元都是字节单元;
而标号b,描述了地址 code:8 和从这个地址开始以后的内存单元都是字单元。

2.在其他段中使用数据标号

注意:
如果想在代码段中直接使用数据标号访问数据,需要用伪指令 assume 将标号所在的段和一个段寄存器联系起来。否则,编译器在编译的时候,无法确定标号的段地址在哪一个寄存器中。

当然,并不是说用伪指令 assume 将标号所在的段和某个段相联系,段寄存器中就真的存放该段的地址。在程序中还要使用指令对段寄存器进行设置。

如:在程序中用ds寄存器和data段相连,则编译器对相关指令的编译如下

指令:  mov al,a[si]
编译为:mov al,[si+0]

指令:  add b,ax
编译为:add [8],ax
  • 8
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ClimberCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值