王爽 汇编语言学习笔记(详细)汇编语言脚本之家

一、基础知识

1、指令

机器指令:CPU能直接识别并执行的二进制编码
汇编指令:汇编指令是机器指令的助记符,同机器指令一一对应。
指令:指令通常由操作码和地址码(操作数)两部分组成
指令集:每种CPU都有自己的汇编指令集。
汇编语言由3类指令组成。
汇编指令
伪指令:没有对应的机器码,由编译器执行,计算机并不执行
其他符号:如+、-、*、/等,由编译器识别,没有对应的机器码。
编译器:够将汇编指令转换成机器指令的翻译程序每一种CPU都有自己的汇编指令集。

image.png

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

2、存储器

随机存储器(RAM)在程序的执行过程中可读可写,必须带电存储
只读存储器(ROM)在程序的执行过程中只读,关机数据不丢失
image.png

image.png
image.png
(以上3张图片来自王道考研 - 计算机组成原理课件)

3、总线

1、总线
总线是连接各个部件的信息传输线,是各个部件共享的传输介质。
主板上有核心器件和一些主要器件,这些器件通过总线(地址总线、数据总线、控制总线)相连。这些器件有CPU、存储器、外围芯片组、扩展插槽等。扩展插槽上一般插有RAM内存条和各类接口卡。
image.png
总线根据位置分类:

  • 片内总线(芯片内部总线)
  • 系统总线(计算机各部件之间的信息传输线)根据传送信息的不同,系统总线从逻辑上又分为3类,地址总线、控制总线和数据总线。

CPU要想进行数据的读写,必须和外部器件(标准的说法是芯片)进行以下3类信息的交互。

  1. 地址总线:CPU通过地址总线来指定存储单元
    image.png
    1根导线可以传送的稳定状态只有两种,高电平或是低电平。用二进制表示就是1或0

图示有10根地址线即一次可以传输10位,访问存储单元地址为1011,寻址范围为0 ~ (210 - 1)

  1. 数据总线:CPU与内存或其他器件之间的数据传送是通过数据总线来进行的
    image.png
    8根数据线一次可传送一个8位二进制数据(即一个字节),传送2个字节需要两次;16根数据线一次可传送2个字节(内存对齐核心原理)
  2. 控制总线:CPU对外部器件的控制是通过控制总线来进行的。

有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。
所以,控制总线的宽度决定了CPU对外部器件的控制能力。

2、CPU对存储器的读写

image.png
1、 CPU通过地址线将地址信息3发出。
2、 CPU通过控制线发出内存读命令,选中存储器芯片,并通知它,将要从中读取数据。
3、 存储器将3号单元中的数据8通过数据线送入CPU。写操作与读操作的步骤相似。
联想:在组成原理中用微操作表示:(PC) → MAR; 1 → R; M(MAR) → MDR; …

3、CPU对外设的控制

CPU对外设都不能直接控制,如显示器、音箱、打印机等。
直接控制这些设备进行工作的是插在扩展插槽上的接口卡。
扩展插槽通过总线和CPU相连,所以接口卡也通过总线同CPU相连。CPU可以直接控制这些接口卡,从而实现CPU对外设的间接控制。
如:CPU无法直接控制显示器,但CPU可以直接控制显卡,从而实现对显示器的间接控制

4、内存地址空间

CPU将系统中各类存储器看作一个逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。
对于CPU,所有存储器中的存储单元都处于一个统一的逻辑存储器中,它的容量受CPU寻址能力限制。(或许就是计组中学的统一编址吧)
image.png
每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据(对ROM写无效)。
image.png

二、寄存器

1、寄存器

CPU由运算器、控制器、寄存器等器件构成,这些器件靠片内总线相连。
运算器进行信息处理;控制器控制各种器件进行工作;寄存器进行信息存储;
8086CPU有14个寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW都是16位
image.png
16位结构CPU具有下面几方面的结构特性。

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

8086CPU可以一次性处理以下两种尺寸的数据。

  • 字节:记为byte,一个字节由8个bit组成,可以存在8位寄存器中。
  • 字:记为word,一个字由两个字节组成,可以存在一个16位寄存器中(16位CPU)
    image.png
    8086采用小端模式:高地址存放高位字节,低地址存放低位字节。

    2、通用寄存器

    通用寄存器:通常用来存放一般性的数据,有AX、BX、CX、DX,它们可分为两个可独立使用的8位寄存器,
16位 8高位 8低位
AX AH AL
BX BH BL
CX CH CL
DX DH DL

在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的
一个8位寄存器所能存储的数据范围是0 ~ 28-1。

3、8086CPU给出物理地址的方法

8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。
8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位。
从8086CPU的内部结构来看,如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB。
8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
image.png
当8086CPU要读写内存时:

  1. CPU中的相关部件提供两个16位的地址,一个称为段地址,另一个称为偏移地址;
  2. 地址加法器将两个16位地址合成为一个20位的物理地址;

地址加法器采用物理地址 = 段地址×16 + 偏移地址的方法用段地址和偏移地址合成物理地址。
例如,8086CPU要访问地址为123C8H的内存单元,1230H左移一位(空出4位)加上00C8H合成123C8H

4、段寄存器

我们可以将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元,可以用分段的方式来管理内存。
用一个段存放数据,将它定义为“数据段”;
用一个段存放代码,将它定义为“代码段”;
用一个段当作栈,将它定义为“栈段”。
注意:

  • 一个段的起始地址一定是16的倍数;
  • 偏移地址为16位,变化范围为0-FFFFH,所以一个段的长度最大为64KB。
  • CPU可以用不同的段地址和偏移地址形成同一个物理地址。

段寄存器:8086CPU有4个段寄存器:CS、DS、SS、ES,提供内存单元的段地址。

1、CS和IP

CS为代码段寄存器,IP为指令指针寄存器,
CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,
CPU将CS:IP指向的内容当作指令执行。(即PC)
image.png
8086CPU的工作过程简要描述

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

在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置为CS=FFFFH,IP=0000H,即在8086PC机刚启动时,FFFF0H单元中的指令是8086PC机开机后执行的第一条指令。
8086CPU提供转移指令修改CS、IP的内容。

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

8086CPU不支持将数据直接送入段寄存器的操作,这属于8086CPU硬件设计

2、DS 和 [address]

DS寄存器:通常用来存放要访问数据的段地址
[address]表示一个偏移地址为address的内存单元,段地址默认放在ds中
通过数据段段地址和偏移地址即可定位内存单元。
mov bx, 1000H ;8086CPU不支持将数据直接送入段寄存器的操作
mov ds, bx ;ds存放数据段地址
mov [0], al ;将al数据(1字节)存到1000H段的0偏移地址处,即10000H
mov ax, [2] ;将数据段偏移地址2处的一个字(8086为2字节)存放到ax寄存器
add cx, [4] ;将偏移地址4处的一个字数据加上cx寄存器数据放到cx寄存器
sub dx, [6] ;dx寄存器数据减去数据段偏移地址6处的字数据存到dx

3、SS 和 SP

在基于8086CPU编程的时候,可以将一段内存当作栈来使用。
栈段寄存器SS,存放段地址,SP寄存器存放偏移地址,任意时刻,SS:SP指向栈顶元素
8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
push ax表示将寄存器ax中的数据送入栈中,由两步完成。
1、SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
2、将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶。

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

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

实验

  1. 将10000H~1000FH这段空间当作栈,初始状态栈是空的;
  2. 设置AX=001AH,BX=001BH;
  3. 将AX、BX中的数据入栈;
  4. 然后将AX、BX清零;
  5. 从栈中恢复AX、BX原来的内容。 ```c mov ax, 1000H mov ss, ax mov sp, 0010H ;初始化栈顶 mov ax, 001AH mov bx, 001BH

push ax push bx ;ax、bx入栈

sub ax, ax ;将ax清零,也可以用mov ax,0, ;sub ax,ax的机器码为2个字节, ;mov ax,0的机器码为3个字节。

sub bx, bx

pop bx ;从栈中恢复ax、bx原来的数据 pop ax ;

  1. <a name="m9LyE"></a>
  2. ## 三、第一个程序
  3. <a name="Xsmep"></a>
  4. ### 1、汇编程序从写出到执行的过程
  5. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22782459/1658756096913-65318e78-d4cb-41d8-aebc-99e21f82249d.png#clientId=udcac21e7-d9c7-4&errorMessage=unknown%20error&from=paste&id=uae42e4d9&originHeight=58&originWidth=767&originalType=url&ratio=1&rotation=0&showTitle=false&size=56212&status=error&style=shadow&taskId=ufc725aa9-0262-41ec-b235-bc94dc4954d&title=)<br />加载后,CPU的CS:IP指向程序的第一条指令(即程序的入口)
  6. ```c
  7. ;1.asm
  8. assume cs:codesg ;将用作代码段的段codesg和段寄存器cs联系起来。
  9. codesg segment ;定义一个段,段的名称为“codesg”,这个段从此开始
  10. ;codesg是一个标号,作为一个段的名称,最终被编译连接成一个段的段地址
  11. mov ax, 0123H
  12. mov bx, 0456H
  13. add ax, bx
  14. add ax, ax
  15. mov ax, 4c00H
  16. int 21H ;这两条指令实现程序的返回
  17. codesg ends ;名称为“codesg”的段到此结束
  18. end ;编译器在编译汇编程序的过程中,碰到了伪指令end,结束对源程序的编译

image.png
image.png

2、程序执行过程跟踪

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

四、[bx] 和 loop指令

1、[bx] 和 loop指令

[bx] 的含义:[bx]同样表示一个内存单元,它的偏移地址在bx中,段地址默认在ds中
loop指令的格式是:loop 标号,CPU执行loop指令的时候,要进行两步操作,

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

例如:计算212

  1. assume cs:code
  2. code segment
  3. mov ax, 2
  4. mov cx, 11 ;循环次数
  5. s: add ax, ax
  6. loop s ;在汇编语言中,标号代表一个地址,标号s实际上标识了一个地址,
  7. ;这个地址处有一条指令:add axax
  8. ;执行loop s时,首先要将(cx)减1,然后若(cx)不为0,则向前
  9. ;转至s处执行add axax。所以,可以利用cx来控制add axax的执行次数。
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end

loop 和 [bx] 的联合应用

计算ffff:0 ~ ffff:b单元中的数据的和,结果存储在dx中
问题分析:
这些内存单元都是字节型数据范围0 ~ 255 ,12个字节数据和不会超过65535,dx可以存下
对于8位数据不能直接加到 dx
解决方案:
用一个16位寄存器来做中介。将内存单元中的8位数据赋值到一个16位寄存器a中,再将ax中的数据加到dx

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

2、段前缀

mov ax, ds:[bx]
mov ax, cs:[bx]
mov ax, ss:[bx]
mov ax, es:[bx]
mov ax, ss:[0]
mov ax, cs:[0]
这些出现在访问内存单元的指令中,用于显式地指明内存单元的段地址
的“ds:”,“cs:”,“ss:”,“es:”,在汇编语言中称为段前缀。
段前缀的使用
将内存ffff:0 ~ ffff:b单元中的数据复制到0:200 ~ 0:20b单元中。

  1. assume cs:code
  2. code segment
  3. mov ax, 0ffffh
  4. mov ds, ax ;(ds)= 0ffffh
  5. mov ax, 0020h
  6. mov es, ax ;(es)= 0020h 0:200 等效于 0020:0
  7. mov bx, 0 ;(bx)= 0,此时ds:bx指向ffff:0es:bx指向0020:0
  8. mov cx12 ;(cx)=12,循环12
  9. s: mov dl,[bx] ;(d1)=((ds)* 16+(bx)),将ffff:bx中的字节数据送入dl
  10. mov es:[bx],dl ;((es)*16+(bx))=(d1),将dl中的数据送入0020:bx
  11. inc bx ;(bx)=(bx)+1
  12. loop s
  13. mov ax4c00h
  14. int 21h
  15. code ends
  16. end

五、包含多个段的程序

程序中对段名的引用,将被编译器处理为一个表示段地址的数值。
mov ax, data
mov ds, ax
mov bx, ds:[6]
在代码段中使用数据

  1. ;计算 8 个数据的和存到 ax 寄存器
  2. assume cs:code
  3. code segment
  4. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;define word 定义8个字形数据
  5. start: mov bx, 0 ;标号start
  6. mov ax, 0
  7. mov cx, 8
  8. s: add ax, cs:[bx]
  9. add bx, 2
  10. loop s
  11. mov ax, 4c00h
  12. int 21h
  13. code ends
  14. end start ;end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方
  15. ;用end指令指明了程序的入口在标号start处,也就是说,“mov bx0”是程序的第一条指令。

在代码段中使用栈

  1. ;利用栈,将程序中定义的数据逆序存放。
  2. assume cs:codesg
  3. codesg segment
  4. dw 0123h0456h0789h0abch0defh0fedh0cbah0987h ; 0-15单元
  5. dw 0000000000000000 ; 16-47单元作为栈使用
  6. start: mov ax, cs
  7. mov ss, ax
  8. mov sp, 30h ;将设置栈顶ss:sp指向栈底cs:30 30h = 48d
  9. mov bx, 0
  10. mov cx, 8
  11. s: push cs:[bx]
  12. add bx, 2
  13. loop s ;以上将代码段0~15单元中的8个字型数据依次入栈
  14. mov bx, 0
  15. mov cx, 8
  16. s0: pop cs:[bx]
  17. add bx2
  18. loop s0 ;以上依次出栈8个字型数据到代码段0~15单元中
  19. mov ax4c00h
  20. int 21h
  21. codesg ends
  22. end start ;指明程序的入口在start

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

  1. assume cs:code,ds:data,ss:stack
  2. data segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ;0-15单元
  4. data ends
  5. stack segment
  6. dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;0-31单元
  7. stack ends
  8. code segment
  9. start: mov ax, stack;将名称为“stack”的段的段地址送入ax
  10. mov ss, ax
  11. mov sp, 20h ;设置栈顶ss:sp指向stack:20 20h = 32d
  12. mov ax, data ;将名称为“data”的段的段地址送入ax
  13. mov ds, ax ;ds指向data
  14. mov bx, 0 ;ds:bx指向data段中的第一个单元
  15. mov cx, 8
  16. s: push [bx]
  17. add bx, 2
  18. loop s ;以上将data段中的0~15单元中的8个字型数据依次入栈
  19. mov bx, 0
  20. mov cx, 8
  21. s0: pop [bx]
  22. add bx, 2
  23. loop s0 ;以上依次出栈8个字型数据到data段的0~15单元中
  24. mov ax, 4c00h
  25. int 21h
  26. code ends
  27. end start
  28. ;“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,
  29. ;可执行文件中的程序被加载入内存后,CPUCS:IP被设置指向这个入口,从而开始执行程序中的第一条指令

关于可执行文件结构与程序入口的详细描述参考:PE文件结构详解其它相关脚本之家

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

1、and 和 or

and指令:逻辑与指令,按位进行与运算。
mov al, 01100011B
and al, 00111011B
执行后:al=00100011B即都为1才为1
or指令:逻辑或指令,按位进行或运算。
mov al, 01100011B
or al, 00111011B
执行后:al=01111011B 即只要有一个为1就为1
关于ASCII码
世界上有很多编码方案,有一种方案叫做ASCII编码,是在计算机系统中通常被采用的。简单地说,所谓编码方案,就是一套规则,它约定了用什么样的信息来表示现实对象。比如说,在ASCII编码方案中,用61H表示“a”,62H表示“b”。一种规则需要人们遵守才有意义。
在文本编辑过程中,我们按一下键盘的a键,就会在屏幕上看到“a”。我们按下键盘的a键,这个按键的信息被送入计算机,计算机用ASCII码的规则对其进行编码,将其转化为61H存储在内存的指定空间中;文本编辑软件从内存中取出61H,将其送到显卡上的显存中;工作在文本模式下的显卡,用ASCII码的规则解释显存中的内容,
61H被当作字符“a”,显卡驱动显示器,将字符“a”的图像画在屏幕上。我们可以看到,显卡在处理文本信息的时候,是按照ASCII码的规则进行的。这也就是说,如果我们要想在显示器上看到“a”,就要给显卡提供“a”的ASCIⅡ码,61H。如何提供?当然是写入显存中。
以字符形式给出的数据

  1. assume cs:code,ds:data
  2. data segment
  3. db 'unIx' ;相当于“db 75H6EH49H58H
  4. db 'foRK'
  5. data ends
  6. code segment
  7. start: mov al, 'a' ;相当于“mov al, 61H”,“a”的ASCI码为61H
  8. mov b1, 'b'
  9. mov ax, 4c00h
  10. int 21h
  11. code ends
  12. end start

大小写转换的问题
image.png
小写字母的ASCII码值比大写字母的ASCII码值大20H
大写字母ASCII码的第5位为0,小写字母的第5位为1(其他一致)

  1. assume cs:codesg,ds:datasg
  2. datasg segment
  3. db 'BaSiC'
  4. db 'iNfOrMaTion'
  5. datasg end
  6. codesg segment
  7. start: mov ax, datasg
  8. mov ds, ax ;设置ds 指向 datasg
  9. mov bx, 0 ;设置(bx)=0ds:bx指向'BaSic'的第一个字母
  10. mov cx, 5 ;设置循环次数5,因为'Basic'5个字母
  11. s: mov al, [bx] ;将ASCII码从ds:bx所指向的单元中取出
  12. and al, 11011111B;将al中的ASCII码的第5位置为0,变为大写字母
  13. mov [bx], al ;将转变后的ASCII码写回原单元
  14. inc bx ;(bx)加1ds:bx指向下一个字母
  15. loop s
  16. mov bx, 5 ;设置(bx)=5ds:bx指向,iNfOrMaTion'的第一个字母
  17. mov cx, 11 ;设置循环次数11,因为‘iNfOrMaTion'有11个字母
  18. s0: mov al, [bx]
  19. or al, 00100000B;将a1中的ASCII码的第5位置为1,变为小写字母
  20. mov [bx], al
  21. inc bx
  22. loop s0
  23. mov ax, 4c00h
  24. int 21h
  25. codesg ends

2、[bx+idata]

[bx+idata]表示一个内存单元, 例如:mov ax, [bx+200]
该指令也可以写成如下格式:
mov ax, [200+bx]
mov ax, 200[bx]
mov ax, [bx].200
用[bx+idata]的方式进行数组的处理

  1. assume cs:codesg,ds:datasg
  2. datasg segment
  3. db 'BaSiC';转为大写
  4. db 'MinIx';转为小写
  5. datasg ends
  6. codesg segment
  7. start:
  8. mov ax, datasg
  9. mov ds, ax
  10. mov bx, 0 ;初始ds:bx
  11. mov cx, 5
  12. s: mov al, 0[bx]
  13. and al, 11011111b ;转为大写字母
  14. mov 0[bx], al ;写回
  15. mov al, 5[bx] ;[5 + bx]
  16. or al, 00100000b ;转为小写字母
  17. mov 5[bx], al
  18. inc bx
  19. loop s
  20. mov ax, 4c00h
  21. int 21h
  22. codesg ends
  23. end start

C语言描述

  1. int main()
  2. {
  3. char a[] = "BaSic";
  4. char b[] = "MinIX";
  5. int i = 0;
  6. do
  7. {
  8. a[i] = a[i] & 0xDF;
  9. b[i] = b[i] | 0x20;
  10. i++;
  11. } while(i < 5);
  12. return 0;
  13. }

3、SI 、DI 与 寻址方式的灵活应用

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

  1. assume cs: codesg, ds: datasg
  2. datasg segment
  3. db 'welcome to masm!';用sidi实现将字符串‘welcome to masm"复制到它后面的数据区中。
  4. db '................'
  5. datasg ends
  6. codesg segment
  7. start: mov ax, datasg
  8. mov ds, ax
  9. mov si, 0
  10. mov cx, 8
  11. s: mov ax, 0[si] ;[0 + si]
  12. mov 16[si], ax ;[16 + si] 使用[bx +idata]方式代替di,使程序更简洁
  13. add si, 2
  14. loop s
  15. mov ax, 4c00h
  16. int 21h
  17. codesg ends
  18. end start

2、[bx + si] 和 [bx + di]
[bx+si]和[bx+di]的含义相似
[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)
指令mov ax, [bx + si]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中
该指令也可以写成如下格式:mov ax, [bx][si]
3、[bx+si+idata]和[bx+di+idata]
[bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+(si)+idata
指令mov ax,[bx+si+idata]的含义:将一个内存单元字数据的内容送入ax,段地址在ds中
4、不同的寻址方式的灵活应用
[idata]用一个常量来表示地址,可用于直接定位一个内存单元;
[bx]用一个变量来表示内存地址,可用于间接定位一个内存单元;
[bx+idata]用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元;
[bx+si]用两个变量表示地址;
[bx+si+idata]用两个变量和一个常量表示地址。

  1. ;将datasg段中每个单词改为大写字母
  2. assume cs:codesg,ds:datasg,ss:stacksg
  3. datasg segment
  4. db 'ibm ' ;16
  5. db 'dec '
  6. db 'dos '
  7. db 'vax ' ;看成二维数组
  8. datasg ends
  9. stacksg segment ;定义一个段,用来做栈段,容量为16个字节
  10. dw 0, 0, 0, 0, 0, 0, 0, 0
  11. stacksg ends
  12. codesg segment
  13. start: mov ax, stacksg
  14. mov ss, ax
  15. mov sp, 16
  16. mov ax, datasg
  17. mov ds, ax
  18. mov bx, 0 ;初始ds:bx
  19. ;cx为默认循环计数器,二重循环只有一个计数器,所以外层循环先保存cx值,再恢复,我们采用栈保存
  20. mov cx, 4
  21. s0: push cx ;将外层循环的cx值入栈
  22. mov si, 0
  23. mov cx, 3 ;cx设置为内层循环的次数
  24. s: mov al, [bx+si]
  25. and al, 11011111b ;每个字符转为大写字母
  26. mov [bx+si], al
  27. inc si
  28. loop s
  29. add bx, 16 ;下一行
  30. pop cx ;恢复cx
  31. loop s0 ;外层循环的loop指令将cx中的计数值减1
  32. mov ax4c00H
  33. int 21H
  34. codesg ends
  35. 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内部、内存、端口

image.png
3、汇编语言中数据位置的表达
汇编语言中用3个概念来表达数据的位置
立即数(idata)
mov ax, 1 ;对于直接包含在机器指令中的数据(执行前在CPU的指令缓冲器中)
add bx, 2000h ;在汇编语言中称为:立即数(idata)
or bx, 00010000b
mov al, ‘a’
寄存器
mov ax, bx ;指令要处理的数据在寄存器中,在汇编指令中给出相应的寄存器名。
mov ds, ax
push bx
mov ds:[0], bx
push ds
mov ss, ax
mov sp, ax

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

;指令要处理的数据在内存中,在汇编指令中可用[X]的格式给出EA,SA在某个段寄存器中。
mov ax, [0]
mov ax, [di]
mov ax, [bx+8]
mov ax, [bx+si]
mov ax, [bx+si+8] ;以上段地址默认在ds中
mov ax, [bp]
mov ax, [bp+8]
mov ax, [bp+si]
mov ax, [bp+si+8] ;以上段地址默认在ss中
mov ax, ds:[bp]
mov ax, es:[bx]
mov ax, ss:[bx+si]
mov ax, cs:[bx+si+8] ;显式给出存放段地址的寄存器

寻址方式
image.png
4、指令要处理的数据有多长
8086CPU的指令,可以处理两种尺寸的数据,byte和word
通过寄存器名指明要处理的数据的尺寸。
例如: mov al, ds:[0] 寄存器al指明了数据为1字节
在没有寄存器名存在的情况下,用操作符X ptr指明内存单元的长度,X在汇编指令中可以为word或byte。
例如:mov byte ptr ds:[0], 1 byte ptr 指明了指令访问的内存单元是一个字节单元
有些指令默认了访问的是字单元还是字节单元
例如,push [1000H],push 指令只进行字操作。
5、寻址方式的综合应用

image.png

mov ax, seg
mov ds, ax
mov bx, 60h ;确定记录地址,ds:bx mov word ptr [bx+0ch], 38 ;排名字段改为38 [bx].0ch
add word ptr [bx+0eh], 70 ;收入字段增加70 [bx].0eh
mov si, 0 ;用si来定位产品字符串中的字符
mov byte ptr [bx+10h+si], ‘V’ ;[bx].10h[si]
inc si
mov byte ptr [bx+10h+si], ‘A’
inc si
mov byte ptr [bx+10h+si], ‘X’

C语言描述

  1. /*定义一个公司记录的结构体*/
  2. struct company
  3. {
  4. char cn[3];/*公司名称*/
  5. char hn[9];/*总裁姓名*/
  6. int pm;/*排名*/
  7. int sr;/*收入*/
  8. char cp[3];/*著名产品*/
  9. };
  10. //sizeof (struct company) == 24
  11. int main()
  12. {
  13. /*定义一个公司记录的变量,内存中将存有一条公司的记录*/
  14. struct company dec = {"DEC", "Ken Olsen", 137, 40, "PDP"};
  15. int i;
  16. dec.pm = 38;
  17. dec.sr = dec.sr + 70;
  18. i = 0;
  19. dec.cp[i] = 'V'; //mov byte ptr [bx].10h[si], 'V'
  20. i++;
  21. dec.cp[i] = 'A';
  22. i++;
  23. dec.cp[i] = 'X';
  24. return 0;
  25. }

6、div指令、dd、dup、mul指令
div是除法指令
除数:有8位和16位两种,在一个寄存器或内存单元中。
被除数:默认放在AX或DX和AX中,
如果除数为8位,被除数则为16位,默认在AX中存放;
如果除数为16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位。
结果:
如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;
如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。

;利用除法指令计算100001/100。
;100001D = 186A1H
mov dx, 1
mov ax, 86A1H ;(dx)*10000H+(ax)=100001
mov bx, 100
div bx ;利用除法指令计算1001/100
mov ax, 1001
mov bl, 100
div b1

伪指令dd
db和dw定义字节型数据和字型数据。
dd是用来定义dword(double word,双字)型数据的伪指令
操作符dup
dup在汇编语言中同db、dw、dd等一样,也是由编译器识别处理的符号。
它和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 ‘abc’, ‘ABC’ ,’abc’ , ‘ABC, ‘abc’, ‘ABC’。

mul 指令
mul是乘法指令,使用 mul 做乘法的时候:相乘的两个数:要么都是8位,要么都是16位。

  • 8 位: AL中和 8位寄存器或内存字节单元中;
  • 16 位: AX中和 16 位寄存器或内存字单元中。

结果

  • 8位:AX中;
  • 16位:DX(高位)和 AX(低位)中。

格式:mul 寄存器 或 mul 内存单元

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

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

八、转移指令的原理

可以修改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在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址。

    1. ;将s处的一条指令复制到s0
    2. assume cs:codesg
    3. codesg segment
    4. s: mov ax, bx ;(mov ax,bx 的机器码占两个字节)
    5. mov si, offset s ;获得标号s的偏移地址
    6. mov di, offset s0 ;获得标号s0的偏移地址
    7. mov ax, cs:[si]
    8. mov cs:[di], ax
    9. s0: nop ;(nop的机器码占一个字节)
    10. nop
    11. codesg ends
    12. ends

    2、jmp指令

    jmp为无条件转移,转到标号处执行指令可以只修改IP,也可以同时修改CS和IP;
    jmp指令要给出两种信息:

  • 转移的目的地址

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

jmp short 标号 jmp near ptr 标号 jcxz 标号 loop 标号 等几种汇编指令,它们对 IP的修改
是根据转移目的地址和转移起始地址之间的位移来进行的。在它们对应的机器码中不包含转移的目的地址,而包含的是到目的地址的位移距离。
1、依据位移进行转移的jmp指令
jmp short 标号(段内短转移)
指令“jmp short 标号”的功能为(IP)=(IP)+8位位移,转到标号处执行指令
(1)8位位移 = “标号”处的地址 - jmp指令后的第一个字节的地址;
(2)short指明此处的位移为8位位移;
(3)8位位移的范围为-128~127,用补码表示
(4)8位位移由编译程序在编译时算出。

  1. assume cs:codesg
  2. codesg segment
  3. start:mov ax,0
  4. jmp short s ;s不是被翻译成目的地址
  5. add ax, 1
  6. s:inc ax ;程序执行后, ax中的值为 1
  7. codesg ends
  8. end start

CPU不需要这个目的地址就可以实现对IP的修改。这里是依据位移进行转移
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 near ptr 标号 (段内近转移)
指令“jmp near ptr 标号”的功能为:(IP) = (IP) + 16位位移。
2、转移的目的地址在指令中的jmp指令
jmp far ptr 标号(段间转移或远转移)
指令 “jmp far ptr 标号” 功能如下:

  • (CS) = 标号所在段的段地址;
  • (IP) = 标号所在段中的偏移地址。
  • far ptr指明了指令用标号的段地址和偏移地址修改CS和IP。
    1. assume cs:codesg
    2. codesg segment
    3. start: mov ax, 0
    4. mov bx, 0
    5. jmp far ptr s ;s被翻译成转移的目的地址0B01 BD0B
    6. db 256 dup (0) ;转移的段地址:0BBDH,偏移地址:010BH
    7. s: add ax,1
    8. inc ax
    9. codesg ends
    10. end start
    image.png
    3、转移地址在寄存器或内存中的jmp指令
    jmp 16位寄存器 功能:IP =(16位寄存器)
    转移地址在内存中的jmp指令有两种格式:
    jmp word ptr 内存单元地址(段内转移)
    功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
    mov ax, 0123H
    mov ds:[0], ax
    jmp word ptr ds:[0]
    ;执行后,(IP)=0123H
    jmp dword ptr 内存单元地址(段间转移)
    功能:从内存单元地址处开始存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
    1、(CS)=(内存单元地址+2)
    2、(IP)=(内存单元地址)
    1. mov ax, 0123H
    2. mov ds:[0], ax;偏移地址
    3. mov word ptr ds:[2], 0;段地址
    4. jmp dword ptr ds:[0]
    5. ;执行后,
    6. ;(CS)=0
    7. ;(IP)=0123H
    8. ;CS:IP 指向 0000:0123
    4、jcxz指令和loop指令

jcxz指令
jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,
在对应的机器码中包含转移的位移,而不是目的地址。对IP的修改范围都为-128~127。
指令格式:jcxz 标号(如果(cx)=0,则转移到标号处执行。)
当(cx) = 0时,(IP) = (IP) + 8位位移

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

当(cx)!=0时,什么也不做(程序向下执行)
loop指令
loop指令为循环指令,所有的循环指令都是短转移,在对应的机器码中包含转移的位移,而不是目的地址。
对IP的修改范围都为-128~127。
指令格式:loop 标号 ((cx) = (cx) - 1,如果(cx) ≠ 0,转移到标号处执行)。
(cx) = (cx) - 1;如果 (cx) != 0,(IP) = (IP) + 8位位移。

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

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

九、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

  1. assume cs:code
  2. stack seqment
  3. db 16 dup (0)
  4. stack ends
  5. code segment
  6. mov ax, 4c00h
  7. int 21h
  8. start: mov ax, stack
  9. mov ss, ax
  10. mov sp, 16
  11. mov ax, 0
  12. push ax ;ax入栈
  13. mov bx, 0
  14. ret ;ret指令执行后,(IP)=0CS:IP指向代码段的第一条指令。可以push cs push ax retf
  15. code ends
  16. end start

2、call 指令

call指令经常跟ret指令配合使用,因此CPU执行call指令,进行两步操作:
(1)将当前的 IP 或 CS和IP 压入栈中;
(2)转移(jmp)。
call指令不能实现短转移,除此之外,call指令实现转移的方法和 jmp 指令的原理相同。
call 标号(近转移)
CPU执行此种格式的call指令时,相当于进行 push IP jmp near ptr 标号
call far ptr 标号(段间转移)
CPU执行此种格式的call指令时,相当于进行:push CS,push IP jmp far ptr 标号
call 16位寄存器
CPU执行此种格式的call指令时,相当于进行: push IP jmp 16位寄存器
call word ptr 内存单元地址
CPU执行此种格式的call指令时,相当于进行:push IP jmp word ptr 内存单元地址

mov sp, 10h
mov ax, 0123h
mov ds:[0], ax
call word ptr ds:[0]
;执行后,(IP)=0123H,(sp)=0EH

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

  1. mov sp, 10h
  2. mov ax, 0123h
  3. mov ds:[0], ax
  4. mov word ptr ds:[2], 0
  5. call dword ptr ds:[0]
  6. ;执行后,(CS)=0,(IP)=0123H,(sp)=0CH

3、call 和 ret 的配合使用

分析下面程序

  1. assume cs:code
  2. code segment
  3. start: mov ax,1
  4. mov cx,3
  5. call s ;(1CPU指令缓冲器存放call指令,IP指向下一条指令(mov bx, ax),执行call指令,IP入栈,jmp
  6. mov bx,ax ;(4IP重新指向这里 bx = 8
  7. mov ax,4c00h
  8. int 21h
  9. s: add ax,ax
  10. loop s;(2)循环3ax = 8
  11. ret;(3return : pop IP
  12. code ends
  13. end start

call 与 ret 指令共同支持了汇编语言编程中的模块化设计
编写子程序

十、标志寄存器

1、标志寄存器
CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理机,个数和结构都可能不同)具有以下3种作用。
(1)用来存储相关指令的某些执行结果;
(2)用来为CPU执行相关指令提供行为依据;
(3)用来控制CPU的相关工作方式。
这种特殊的寄存器在8086CPU中,被称为标志寄存器(flag)。
8086CPU的标志寄存器有16位,其中存储的信息通常被称为程序状态字(PSW-Program Status Word)
flag寄存器是按位起作用的,它的每一位都有专门的含义,记录特定的信息。

image.png
在8086CPU的指令集中,有的指令的执行是影响标志寄存器的,比如,add、sub、mul、div、inc、or、and等,它们大都是运算指令(进行逻辑或算术运算);有的指令的执行对标志寄存器没有影响,比如,mov、push、pop等,它们大都是传送指令
1、零标志位 (ZF)
零标志位(Zero Flag)。它记录相关指令执行后,其结果是否为0。
如果结果为0,那么zf = 1(表示结果是0);如果结果不为0,那么zf = 0。

  1. mov ax, 1
  2. sub ax, 1 ;执行后,结果为0,则zf = 1
  3. mov ax, 2
  4. sub ax, 1 ;执行后,结果不为0,则zf = 0

2、奇偶标志位 (PF)
奇偶标志位(Parity Flag)。它记录相关指令执行后,其结果的所有bit位中1的个数是否为偶数。
如果1的个数为偶数,pf = 1,如果为奇数,那么pf = 0。
mov al, 1
add al, 10 ;执行后,结果为00001011B,其中有3(奇数)个1,则pf = 0;
mov al, 1
or al, 2 ;执行后,结果为00000011B,其中有2(偶数)个1,则pf = 1;
3、符号标志位(SF)
符号标志位(Symbol Flag)。它记录相关指令执行后,其结果是否为负。
如果结果为负,sf = 1;如果非负,sf = 0。
计算机中通常用补码来表示有符号数据。计算机中的一个数据可以看作是有符号数,也可以看成是无符号数。
00000001B,可以看作为无符号数1,或有符号数+1;
10000001B,可以看作为无符号数129,也可以看作有符号数-127。
对于同一个二进制数据,计算机可以将它当作无符号数据来运算,也可以当作有符号数据来运算
CPU在执行add等指令的时候,就包含了两种含义:可以将add指令进行的运算当作无符号数的运算,也可以将add指令进行的运算当作有符号数的运算
SF标志,就是CPU对有符号数运算结果的一种记录,它记录数据的正负。在我们将数据当作有符号数来运算的时候,可以通过它来得知结果的正负。如果我们将数据当作无符号数来运算,SF的值则没有意义,虽然相关的指令影响了它的值

mov al, 10000001B
add al, 1 ;执行后,结果为10000010B,sf = 1,表示:如果指令进行的是有符号数运算,那么结果为负;
1
2
mov al, 10000001B
add al, 01111111B ;执行后,结果为0,sf = 0,表示:如果指令进行的是有符号数运算,那么结果为非负

3、进位标志位(CF)
进位标志位(Carry Flag)。一般情况下,在进行无符号数运算的时候,它记录了运算结果的最高有效位向更高位的进位值,或从更高位的借位值
image.png
97H - 98H 产生借位CF = 1 ==》 (al) = 197H - 98H = FFH
4、溢出标志位(OF)
溢出标志位(Overflow Flag)。一般情况下,OF记录了有符号数运算的结果是否发生了溢出。
如果发生溢出,OF = 1;如果没有,OF = 0。
CF和OF的区别:CF是对无符号数运算有意义的标志位,而OF是对有符号数运算有意义的标志位
CPU在执行add等指令的时候,就包含了两种含义:无符号数运算和有符号数运算。

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

mov al, 98
add al, 99 ;执行后将产生溢出。因为进行的”有符号数”运算是:(al)=(al)+ 99 = 98 + 99=197 = C5H 为-59的补码
;而结果197超出了机器所能表示的8位有符号数的范围:-128-127。
;add 指令执行后:无符号运算没有进位CF=0,有符号运算溢出OF=1
;当取出的数据C5H按无符号解析C5H = 197, 当按有符号解析通过SP得知数据为负,即C5H为-59补码存储,

mov al,0F0H ;F0H,为有符号数-16的补码 -Not(F0 - 1)
add al,088H ;88H,为有符号数-120的补码 -Not(88- 1)
;执行后,将产生溢出。因为add al, 088H进行的有符号数运算结果是:(al)= -136
;而结果-136超出了机器所能表示的8位有符号数的范围:-128-127。
;add 指令执行后:无符号运算有进位CF=1,有符号运算溢出OF=1

2、adc指令和sbb指令
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。

image.png

  1. ;计算1EF000H+201000H,结果放在ax(高16位)和bx(低16位)中。
  2. ;将计算分两步进行,先将低16位相加,然后将高16位和进位值相加。
  3. mov ax, 001EH
  4. mov bx, 0F000H
  5. add bx, 1000H
  6. adc ax, 0020H

sbb指令
sbb是带借位减法指令,它利用了CF位上记录的借位值。
指令格式:sbb 操作对象1, 操作对象2
功能:操作对象1 = 操作对象1 - 操作对象2 - CF

  1. ;计算 003E1000H - 00202000H,结果放在axbx中,程序如下:
  2. mov bx, 1000H
  3. mov ax, 003EH
  4. sub bx, 2000H
  5. sbb ax, 0020H

3、cmp指令
cmp是比较指令,cmp的功能相当于减法指令,只是不保存结果。cmp指令执行后,将对标志寄存器产生影响。
其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
cmp指令格式:cmp 操作对象1,操作对象2
例如:
指令cmp ax, ax,做(ax)-(ax)的运算,结果为0,但并不在ax中保存,仅影响flag的相关各位。
指令执行后:zf=1,pf=1,sf=0,cf=0,of=0。
CPU在执行cmp指令的时候,也包含两种含义:进行无符号数运算和进行有符号数运算。

cmp ax, bx 无符号比较时
(ax) = (bx) zf = 1
(ax) ≠ (bx) zf = 0
(ax) < (bx) cf = 1
(ax) ≥ (bx) cf = 0
(ax) > (bx) cf = 0 且 zf = 0
(ax) ≤ (bx) cf = 1 且 zf = 1

上面的表格可以正推也可以逆推
如果用cmp来进行有符号数比较时
SF只能记录实际结果的正负,发生溢出的时候,实际结果的正负不能说明逻辑上真正结果的正负。
但是逻辑上的结果的正负,才是cmp指令所求的真正结果,所以我们在考察SF的同时考察OF,就可以得知逻辑上真正结果的正负,同时就知道比较的结果。

  1. mov ah, 08AH ; -Not(8A-1) = -118 即当成有符号数时为-118
  2. mov bh, 070H ; 有符号数时最高位为0为正数, 70H = 112
  3. cmp ah, bh ;(ah)-(bh)实际得到的结果是1AH
  4. ; 在逻辑上,运算所应该得到的结果是:(-118)- 112 = -230
  5. ; sf记录实际结果的正负,所以sf=0

cmp ah, bh
(1)如果sf=1,而of=0 。 of=0说明没有溢出,逻辑上真正结果的正负=实际结果的正负; sf=1,实际结果为负,所以逻辑上真正的结果为负,所以(ah)<(bh)
(2)如果sf=1,而of=1: of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负; sf=1,实际结果为负。
实际结果为负,而又有溢出,这说明是由于溢出导致了实际结果为负,,如果因为溢出导致了实际结果为负,那么逻辑上真正的结果必然为正。 这样,sf=1,of=1,说明了(ah)>(bh)。
(3)如果sf=0,而of=1。of=1,说明有溢出,逻辑上真正结果的正负≠实际结果的正负;sf=0,实际结果非负。而of=1说明有溢出,则结果非0,所以,实际结果为正。
实际结果为正,而又有溢出,这说明是由于溢出导致了实际结果非负,如果因为溢出导致了实际结果为正,那么逻辑上真正的结果必然为负。这样,sf=0,of=1,说明了(ah)<(bh)。
(4)如果sf=0,而of=0
of=0,说明没有溢出,逻辑上真正结果的正负=实际结果的正负;sf=0,实际结果非负,所以逻辑上真正的结果非负,所以(ah)≥(bh)。

4、检测比较结果的条件转移指令
可以根据某种条件,决定是否修改IP的指令
jcxz它可以检测cx中的数值,如果(cx)=0,就修改IP,否则什么也不做。
所有条件转移指令的转移位移都是[-128,127]。
多数条件转移指令都检测标志寄存器的相关标志位,根据检测的结果来决定是否修改IP
这些条件转移指令通常都和cmp相配合使用,它们所检测的标志位,都是cmp指令进行无符号数比较的时记录比较结果的标志位
根据无符号数的比较结果进行转移的条件转移指令(它们检测zf、cf的值)

指令 含义 检测的相关标志位
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,b:below,a:above,n:not

  1. ;编程,统计data段中数值为8的字节的个数,用ax保存统计结果。
  2. mov ax, data
  3. mov ds, ax
  4. mov bx, 0 ;ds:bx指向第一个字节
  5. mov ax, 0 ;初始化累加器mov cx8
  6. s:
  7. cmp byte ptr[bx], 8 ;和8进行比较
  8. jne next ;如果不相等转到next,继续循环
  9. inc ax ;如果相等就将计数值加1
  10. next:
  11. inc bx
  12. loop s ;程序执行后:(ax)=3

5、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。
格式:rep movsb
movsb和movsw进行的是串传送操作中的一个步骤,一般来说,movsb和movsw都和rep配合使用,
功能:rep的作用是根据cx的值,重复执行后面的串传送指令
8086CPU提供下面两条指令对df位进行设置。

  • cld指令:将标志寄存器的df位置0
  • std指令:将标志寄存器的df位置1 ```c ;将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次 c1d ;设置df=0,正向传送 rep movsb

  1. **6pushfpopf**<br />pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中<br />pushfpopf,为直接访问标志寄存器提供了一种方法。
  2. <a name="jE7sy"></a>
  3. ## 十一、内中断
  4. <a name="cZcod"></a>
  5. ### 1、内中断的产生
  6. 任何一个通用的CPU,都具备一种能力,可以在执行完当前正在执行的指令之后,检测到从CPU外部发送过来的或内部产生的一种特殊信息,并且可以立即对所接收到的信息进行处理。这种特殊的信息,我们可以称其为:中断信息。中断的意思是指,CPU不再接着(刚执行完的指令)向下执行,而是转去处理这个特殊信息。<br />中断信息可以来自CPU的内部和外部(内中断,外中断)<br />内中断:当CPU的内部有需要处理的事情发生的时候,将产生中断信息,引发中断过程。这种中断信息来自CPU的内部<br />8086CPU的内中断(下面四种情况将产生中断信息)
  7. - 除法错误,比如,执行div指令产生的除法溢出;
  8. - 单步执行;
  9. - 执行into指令;
  10. - 执行int指令。
  11. 中断信息中包含中断类型码,中断类型码为一个字节型数据,可以表示256种中断信息的来源(中断源)<br />上述的4种中断源,在8086CPU中的中断类型码如下。
  12. - 除法错误:0
  13. - 单步执行:1
  14. - 执行into指令:4
  15. - 执行int指令,该指令的格式为int n,指令中的n为字节型立即数,是提供给CPU的中断类型码。
  16. <a name="zVyhR"></a>
  17. ### **2、中断处理程序、中断向量表、中断过程**
  18. 中断处理程序<br />用来处理中断信息的程序被称为中断处理程序。<br />根据CPU的设计,中断类型码的作用就是用来定位中断处理程序。比如CPU根据中断类型码4,就可以找到4号中断的处理程序<br />中断向量表<br />中断向量,就是中断处理程序的入口地址。中断向量表,就是中断处理程序入口地址的列表<br />CPU8位的中断类型码通过中断向量表找到相应的中断处理程序的入口地址
  19. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22782459/1658756101694-1f033ead-defd-4c82-ad48-02bfd9fc40dd.png#clientId=udcac21e7-d9c7-4&errorMessage=unknown%20error&from=paste&id=u0033b6a2&originHeight=471&originWidth=741&originalType=url&ratio=1&rotation=0&showTitle=false&size=140335&status=error&style=shadow&taskId=u6e90a927-a78c-4614-b7c5-cd70550bdc5&title=)<br />中断过程<br />中断过程的主要任务就是用中断类型码在中断向量表中找到中断处理程序的入口地址,设置CS和IP<br />简要描述如下
  20. 1. 取得中断类型码N
  21. 2. pushf
  22. 3. TF=0IF=0 (为什么这样参考单步中断)
  23. 4. push CS , push IP
  24. 5. IP)=(N * 4),(CS)=(N * 4 + 2
  25. 硬件在完成中断过程后,CS:IP将指向中断处理程序的入口,CPU开始执行中断处理程序。<br />**3iret指令**<br />CPU随时都可能执行中断处理程序,中断处理程序必须一直存储在内存某段空间之中<br />而中断处理程序的入口地址,即中断向量,必须存储在对应的中断向量表表项中。<br />中断处理程序的常规编写步骤:
  26. 1. 保存用到的寄存器;
  27. 2. 处理中断;
  28. 3. 恢复用到的寄存器;
  29. 4. iret指令返回。
  30. iret 指令描述为:pop IP pop CS popf<br />iret指令执行后,CPU回到执行中断处理程序前的执行点继续执行程序<br />**4、除法错误中断的处理**
  31. > mov ax, 1000h<br />mov bh, 1<br />div bh ;除法溢出错误
  32. 1、当CPU执行div bh时,发生了除法溢出错误,产生0号中断信息,从而引发中断过程,<br />2CPU执行0号中断处理程序<br />3、系统中的0号中断处理程序的功能:显示提示信息“Divide overflow”后,返回到操作系统中。<br />**编程实验**<br />编程:编写0号中断处理程序do0,当发生除法溢出时,在屏幕中间显示“overflow!”,返回DOS。<br />10000:02000000:02FF256个字节的空间所对应的中断向量表项都是空的,可以将中断处理程序do0传送到内存0000:0200处。<br />2、中断处理程序do0放到0000:0200,再将其地址登记在中断向量表对应表项
  33. - 0号表项的地址0:00:0字单元存放偏移地址,0:2字单元存放段地址
  34. - do0的段地址0存放在0000:0002字单元中,将偏移地址200H存放在0000:0000字单元
  35. ```c
  36. assume cs:code
  37. code segment
  38. start:
  39. mov ax, cs
  40. mov ds, ax
  41. mov si, offset do0 ;设置ds:si指向源地址
  42. mov ax, 0
  43. mov es, ax
  44. mov di, 200h ;设置es:di指向目的地址0000:0200
  45. mov cx, offset do0end - offset do0 ;设置cx为传输长度 编译时给出do0部分代码长度
  46. cld ;设置传输方向为正
  47. rep movsb ;将do0的代码送入0:200处
  48. mov ax, 0 ;设置中断向量表
  49. mov es, ax
  50. mov word ptr es:[0*4], 200h
  51. mov word ptr es:[0*4+2], 0
  52. mov ax,4c00h
  53. int 21h
  54. ;do0程序的主要任务是显示字符串
  55. do0: jmp short do0 start
  56. db "overflow!"
  57. do0start:
  58. mov ax, cs
  59. mov ds, ax
  60. mov si, 202h ;设置ds:si指向字符串
  61. mov ax, 0b800h
  62. mov es, ax
  63. mov di, 12*160+36*2 ;设置es:di指向显存空间的中间位置
  64. mov cx, 9 ;设置cx为字符串长度
  65. s: mov al, [si]
  66. mov es:[di], al
  67. inc si
  68. add di, 1
  69. mov al, 02h ;设置颜色
  70. mov es:[di], al
  71. add di, 1
  72. loop s
  73. mov ax, 4c00h
  74. int 21h
  75. do0end: nop
  76. code ends
  77. end start

5、单步中断
CPU在执行完一条指令之后,如果检测到标志寄存器的TF位为1,则产生单步中断,引发中断过程。单步中断的中断类型码为1
Debug是如何利用CPU所提供的单步中断的功能进行调试?如使用t命令查看寄存器状态
Debug提供了单步中断的中断处理程序,功能为显示所有寄存器中的内容后等待输入命令
在使用t命令执行指令时,Debug将TF设置为1,在CPU执行完这条指令后就引发单步中断,执行单步中断的中断处理程序,所有寄存器中的内容被显示在屏幕上,并且等待输入命令。
在进入中断处理程序之前,设置TF=0。从而避免CPU在执行中断处理程序的时候发生单步中断
6、int指令
int指令的格式为:int n ,n为中断类型码,它的功能是引发中断过程。
CPU执行int n指令,相当于引发一个n号中断的中断过程
在程序中使用int指令调用任何一个中断的中断处理程序(中断例程)
编写供应用程序调用的中断例程
实验1

  1. ;求2 * 3456^2
  2. assume cs:code
  3. code segment
  4. start:
  5. mov ax, 3456 ;(ax)=3456
  6. int 7ch ; 调用中断7ch的中断例程,计算ax中的数据的平方
  7. add ax, ax
  8. adc dx, dx ;存放结果,将结果乘以2
  9. mov ax,4c00h
  10. int 21h
  11. code ends
  12. end start
  1. ;编程:安装中断7ch的中断例程
  2. ;功能:求一word型数据的平方。
  3. ;参数:(ax) = 要计算的数据。
  4. ;返回值:dxax中存放结果的高16位和低16位。
  5. assume cs:code
  6. code segment
  7. start:
  8. mov ax,cs
  9. mov ds,ax
  10. mov si,offset sqr ;设置ds:si指向源地址
  11. mov ax,0
  12. mov es,ax
  13. mov di,200h ;设置es:di指向目的地址
  14. mov cx,offset sqrend - offset sqr ;设置cx为传输长度
  15. cld ;设置传输方向为正
  16. rep movsb
  17. mov ax,0
  18. mov es,ax
  19. mov word ptr es:[7ch*4],200h
  20. mov word ptr es:[7ch*4+2],0
  21. mov ax,4c00h
  22. int 21h
  23. sqr:
  24. mul ax
  25. iret ;CPU执行int 7ch指令进入中断例程之前,标志寄存器、当前的CSIP被压入栈
  26. ;在执行完中断例程后,应该用iret 指令恢复int 7ch执行前的标志寄存器和CSIP
  27. sqrend: nop
  28. code ends
  29. end start

实验2

  1. ;功能:将一个全是字母,以0结尾的字符串,转化为大写。
  2. ;参数:ds:si指向字符串的首地址。
  3. ;应用举例:将data段中的字符串转化为大写。
  4. assume cs:code
  5. data segment
  6. db 'conversation',0
  7. data ends
  8. code segment
  9. start: mov ax, data
  10. mov ds, ax
  11. mov si, 0
  12. int 7ch
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start
  1. ;功能:将一个全是字母,以0结尾的字符串,转化为大写。
  2. ;参数:ds:si指向字符串的首地址。
  3. ;应用举例:将data段中的字符串转化为大写。
  4. assume cs:code
  5. data segment
  6. db 'conversation',0
  7. data ends
  8. code segment
  9. start: mov ax, data
  10. mov ds, ax
  11. mov si, 0
  12. int 7ch
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start
  1. assume cs:code
  2. code segment
  3. start:
  4. mov ax,cs
  5. mov ds,ax
  6. mov si,offset capital
  7. mov ax,0
  8. mov es,ax
  9. mov di,200h
  10. mov cx,offset capitalend - offset capital
  11. cld
  12. rep movsb
  13. mov ax,0
  14. mov es,ax
  15. mov word ptr es:[7ch*4],200h
  16. mov word ptr es:[7ch*4+2],0
  17. mov ax,4c00h
  18. int 21h
  19. capital:
  20. push cx
  21. push si
  22. change:
  23. mov cl,[si]
  24. mov ch,0
  25. jcxz ok
  26. and byte ptr [si],11011111b
  27. inc si
  28. jmp short change
  29. ok:
  30. pop si
  31. pop cx
  32. iret
  33. capitalend:nop
  34. code ends
  35. end start

7、BIOS和DOS所提供的中断例程
在系统板的ROM中存放着一套程序,称为BIOS(基本输入输出系统)
BIOS中主要包含以下几部分内容

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

程序员在编程的时候,可以用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启动后,除完成其他工作外,还将它所提供的中断例程装入内存,并建立相应的中断向量。
BIOS中断例程应用
一般来说,一个供程序员调用的中断例程中往往包括多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。
BIOS和DOS提供的中断例程,都用ah来传递内部子程序的编号。
编程:在屏幕的5行12列显示3个红底高亮闪烁绿色的“al。

  1. assume cs:code
  2. code segment
  3. ;int 10h中断例程的"设置光标位置"功能
  4. mov ah, 2;设置光标调用第10h号中断例程的2号子程序,功能为设置光标位置(可以提供光标所在的行号、列号和页号作为参数)
  5. ;设置光标到第0页,第5行,第12
  6. mov bh, 0;第0
  7. mov dh, 5dh中放行号
  8. mov dl, 12dl中放列号
  9. int 10h
  10. ;int10h中断例程的"在光标位置显示字符"功能。
  11. mov ah9 ;调用第10h号中断例程的9号子程序,功能为在光标位置显示字符
  12. ;提供要显示的字符、颜色属性、页号、字符重复个数作为参数
  13. mov al'a' ;字符
  14. mov b111001010b ;颜色属性
  15. mov bh0 ;第0
  16. mov cx3 ;字符重复个数
  17. int 10h
  18. code ends
  19. end

bh中页号的含义:内存地址空间中,B8000H~BFFFFH共32kB的空间,为8025彩色字符模式的显示缓冲区。
一屏的内容在显示缓冲区中共占4000个字节。显示缓冲区分为8页,每页4KB(约4000B),显示器可以显示任意一页的内容。一般情况下,显示第0页的内容。也就是说,通常情况下,B8000H~B8F9FH中的4000个字节的内容将出现在显示器上。
*DOS中断例程应用

int 21h中断例程是DOS提供的中断例程,4ch号功能,即程序返回功能

mov ah, 4ch ;调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数
mov al, 0 ;返回值
int 21h

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

  1. assume cs:code
  2. data segment
  3. db 'Welcome to masm', '$' ;“$”本身并不显示,只起到边界的作用
  4. data ends
  5. code segment
  6. start: mov ah, 2 ;10号中断设置光标位置功能
  7. mov bh, 0 ;第0
  8. mov dh, 5dh中放行号
  9. mov dl, 12 ;dl中放列号
  10. int 10h
  11. mov ax, data
  12. mov ds, ax
  13. mov dx, 0 ;ds:dx指向字符串的首地址data:0 (参数)
  14. mov ah, 9 ;调用第21h号中断例程的9号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数
  15. int 21h
  16. mov ax, 4c00h ;21号中断程序返回功能
  17. int 21h
  18. code ends
  19. end start

十二、端口

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

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

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

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

从CPU的角度,将这些寄存器都当作端口,对它们进行统一编址,从而建立了一个统一的端口地址空间。
每一个端口在地址空间中都有一个地址。在访问端口的时候,CPU通过端口地址来定位端口。因为端口所在的芯片和CPU通过总线相连,
CPU可以直接读写以下3个地方的数据。

CPU内部的寄存器;
内存单元;
端口。

1、端口的读写
端口地址和内存地址一样,通过地址总线来传送。在PC系统中,CPU最多可以定位64KB个不同的端口。则端口地址的范围为0-65535。
端口的读写指令只有两条:in和out,分别用于从端口读取数据和往端口写入数据。
在in和out指令中,只能使用ax或al来存放从端口中读入的数据或要发送到端口中的数据。

  1. ;对0~255以内的端口进行读写时:
  2. in al, 20h ;从20h端口读入一个字节
  3. out 20h, al ;往20h端口写入一个字节
  4. ;对256~65535的端口进行读写时,端口号放在dx中:
  5. mov dx, 3f8h ;将端口号3f8h送入dx
  6. in al, dx ;从3f8h端口读入一个字节
  7. out dx, al ;向3f8h端口写入一个字节

2、CMOS RAM芯片
PC机中,有一个CMOSRAM芯片,一般简称为CMOS。此芯片的特征如下
1、包含一个实时钟和一个有128个存储单元的RAM存储器
2、该芯片靠电池供电。关机后内部的实时钟正常工作,RAM中的信息不丢失
3、128个字节的RAM中,内部实时钟占用0~0dh单元来保存时间信息,其余大部分单元用于保存系统配置信息,供系统启动时BIOS程序读取。BIOS也提供了相关的程序,使我们可以在开机的时候配置CMOSRAM中的系统信息。
该芯片内部有两个端口,端口地址为70h和71h。CPU通过这两个端口来读写CMOS RAM
4、70h为地址端口,存放要访问的CMOSRAM单元的地址;71h为数据端口,存放从选定的CMOSRAM单元中读取的数据,或要写入到其中的数据。
可见,CPU对CMOS RAM的读写分两步进行,比如,读CMOS RAM的2号单元:
①将2送入端口70h;
②从端口71h读出2号单元的内容。

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

9 8 7 6 5 4 3 2 1 0

BCD码是以4位二进制数表示十进制数码的编码方法 4 == 0100B
一个字节可表示两个BCD码。则CMOS RAM存储时间信息的单元中,存储了用两个BCD码表示的两位十进制数,高4位的BCD码表示十位,低4位的BCD码表示个位。比如,00010100b表示14。

  1. ;编程,在屏幕中间显示当前的月份。
  2. assume cs:code
  3. code segment
  4. start: mov al8 ;从CMOS RAM8号单元读出当前月份的BCD码。
  5. out 70hal
  6. in al, 71h ;从数据端口71h中取得指定单元中的数据:
  7. mov ah, al ;al中为从CMOSRAM8号单元中读出的数据
  8. mov cl, 4
  9. shr ah, cl ;ah中为月份的十位数码值,左移四位空出四位
  10. and al, 00001111b ;al中为月份的个位数码值
  11. add ah, 30h ;BCD码值+30h=十进制数对应的ASCII
  12. add al, 30h
  13. mov bx, 0b800h
  14. mov es, bx
  15. mov byte ptr es:[160*12+40*2], ah ;显示月份的十位数码
  16. mov byte ptr es:[160*12+40*2+2], al ;接着显示月份的个位数码
  17. mov ax4c00h
  18. int 21h
  19. code ends
  20. end start

3、shl和shr指令
shl和shr是逻辑移位指令
shl是逻辑左移指令,它的功能为:

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

shr是逻辑右移指令,同理

  1. mov al, 01001000b
  2. shl al, 1 ;将a1中的数据左移一位执行后(al)=10010000bCF=0
  3. mov al, 01010001b
  4. mov cl, 3 ;如果移动位数大于1时,必须将移动位数放在cl
  5. shl al, c1
  6. mov al, 10000001b
  7. shr al, 1 ;将al中的数据右移一位执行后(al)=01000000bCF=1

将X逻辑左移一位,相当于执行X=X*2。
将X逻辑右移一位,相当于执行X=X/2

十三、外中断

1、外中断

PU在计算机系统中,除了能够执行指令,进行运算以外,还应该能够对外部设备进行控制,接收它们的输入,向它们进行输出(I/O能力)
PC系统的接口卡和主板上,装有各种接口芯片。这些外设接口芯片的内部有若干寄存器,CPU将这些寄存器当作端口来访问
外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中;
CPU向外设的输出也不是直接送入外设,而是先送入端口中,再由相关的芯片送到外设。
CPU还可以向外设输出控制命令,而这些控制命令也是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。
即:CPU通过端口和外部设备进行联系
当CPU外部有需要处理的事情发生的时候,比如说,外设的输入到达,相关芯片将向CPU发出相应的中断信息。CPU在执行完当前指令后,可以检测到发送过来的中断信息,引发中断过程,处理外设的输入。
PC系统中,外中断源有两类
1、可屏蔽中断
可屏蔽中断是CPU可以不响应的外中断。CPU是否响应可屏蔽中断,要看标志寄存器的IF位的设置。
当CPU检测到可屏蔽中断信息时,如果IF=1,则CPU在执行完当前指令后响应中断,引发中断过程;如果IF=0,则不响应可屏蔽中断。
可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的;而内中断的中断类型码是在CPU内部产生的。
中断过程中将IF置0的原因就是,在进入中断处理程序后,禁止其他的可屏蔽中断。
如果在中断处理程序中需要处理可屏蔽中断,可以用指令将IF置1。
8086CPU提供的设置IF的指令:sti,设置IF=1;cli,设置IF=0。
2、不可屏蔽中断
不可屏蔽中断是CPU必须响应的外中断。当CPU检测到不可屏蔽中断信息时,则在执行完当前指令后,立即响应,引发中断过程。
对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以中断过程中,不需要取中断类型码。则不可屏蔽中断的中断过程为:①标志寄存器入栈,IF=0,TF=0;②CS、IP入栈;③(IP)=(8),(CS)=(0AH)。
几乎所有由外设引发的外中断,都是可屏蔽中断。当外设有需要处理的事件(比如说键盘输入)发生时,相关芯片向CPU发出可屏蔽中断信息。不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。
2、PC机键盘的处理过程
键盘中有一个芯片对键盘上的每一个键的开关状态进行扫描。按下一个键时,开关接通,该芯片就产生一个扫描码,扫描码说明了按下的键在键盘上的位置。扫描码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。松开按下的键时,也产生一个扫描码,扫描码说明了松开的键在键盘上的位置。松开按键时产生的扫描码也被送入60h端口中。
一般将按下一个键时产生的扫描码称为通码,松开一个键产生的扫描码称为断码。
扫描码长度为一个字节,通码的第7位为0,断码的第7位为1
即:断码 = 通码 + 80h。比如,g键的通码为22h,断码为a2h
键盘的输入到达60h端口时,相关的芯片就会向CPU发出中断类型码为9的可屏蔽中断信息。CPU检测到该中断信息后,如果IF=1,则响应中断,引发中断过程,转去执行int 9中断例程。
image.png
BIOS提供了int9中断例程,用来进行基本的键盘输入处理,主要的工作如下:
(1)读出60h端口中的扫描码;
(2)如果是字符键的扫描码,将该扫描码和它所对应的字符码(即ASCII码)送入内存中的BIOS键盘缓冲区; 如果是控制键(比如Ctrl)和切换键(比如CapsLock)的扫描码,则将其转变为状态字节写入内存中存储状态字节的单元;
(3)对键盘系统进行相关的控制,比如说,向相关芯片发出应答信息。
BIOS键盘缓冲区可以存储15个键盘输入,一个键盘输入用一个字单元存放,高位字节存放扫描码,低位字节存放字符码。
0040:17单元存储键盘状态字节,该字节记录了控制键和切换键的状态。键盘状态字节各位记录的信息如下。

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

编写int 9中断例程

  1. ;编程:在屏幕中间依次显示“a”~“z”,并可以让人看清。在显示的过程中,按下'Esc'键后,改变显示的颜色。
  2. ;完整功能代码:
  3. assume cs:code
  4. stack segment
  5. db 128 dup (0)
  6. stack ends
  7. data segment
  8. dw 0,0
  9. data ends
  10. code segment
  11. start:
  12. mov ax,stack
  13. mov ss,ax
  14. mov sp,128
  15. mov ax,data
  16. mov ds,ax
  17. mov ax,0
  18. mov es,ax
  19. push es:[9*4]
  20. pop ds:[0]
  21. push es:[9*4+2]
  22. pop ds:[2] ;将原来的int 9中断例程的入口地址保存在ds:0ds:2单元中
  23. mov word ptr es:[9*4], offset int9
  24. mov es:[9*4+2], cs ;在中断向量表中设置新的int 9中断例程的入口地址
  25. ;显示字符串
  26. mov ax, 0b800h
  27. mov es, ax
  28. mov ah, 'a'
  29. s:
  30. mov es:[160*12+40*2], ah
  31. call delay
  32. inc ah
  33. cmp ah, 'z'
  34. jna s
  35. mov ax,0
  36. mov es,ax
  37. push ds:[0]
  38. pop es:[9*4]
  39. push ds;[2]
  40. pop es;[9*4+2] ;将中断向量表中int 9中断例程的入口恢复为原来的地址
  41. mov ax,4c00h
  42. int 21h
  43. ;将循环延时的程序段写为一个子程序
  44. delay:
  45. push ax
  46. push dx
  47. mov dx, 2000h ;用两个16位寄存器来存放32位的循环次数
  48. mov ax, 0
  49. s1:
  50. sub ax, 1
  51. sbb dx, 0
  52. cmp ax, 0
  53. jne s1
  54. cmp dx, 0
  55. jne s1
  56. pop dx
  57. pop ax
  58. ret
  59. ;------以下为新的int 9中断例程--------------------
  60. int9:
  61. push ax
  62. push bx
  63. push es
  64. in al, 60h;从端口60h读出键盘的输入
  65. pushf ;标志寄存器入栈
  66. pushf
  67. pop bx
  68. and bh,11111100b
  69. push bx
  70. popf ;TF=0,IF=0
  71. call dword ptr ds:[0] ;对int指令进行模拟,调用原来的int 9中断例程
  72. cmp al,1
  73. jne int9ret
  74. mov ax,0b800h
  75. mov es,ax
  76. inc byte ptr es:[160*12+40*2+1] ;属性增加1,改变颜色
  77. int9ret:
  78. pop es
  79. pop bx
  80. pop ax
  81. iret
  82. code ends
  83. end start

CPU对外设输入的通常处理方法
(1)外设的输入送入端口;
(2)向CPU发出外中断(可屏蔽中断)信息;
(3)CPU检测到可屏蔽中断信息,如果IF=1,CPU在执行完当前指令后响应中断,执行相应的中断例程;
(4)可在中断例程中实现对外设输入的处理。
端口和中断机制,是CPU进行I/O的基础。

十四、直接定址表

  1. assume cs:code
  2. code segment
  3. a : db 1,2,3,4,5,6,7,8 ;在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。
  4. b : dw 0
  5. start :mov si,offset a
  6. mov bx,offset b
  7. mov cx,8
  8. s : mov al,cs:[si]
  9. mov ah,0
  10. add cs:[bx],ax
  11. inc si
  12. loop s
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start

程序中,code、a、b、start、s都是标号。这些标号仅仅表示了内存单元的地址
描述了单位长度的标号

  1. assume cs:code
  2. code segment
  3. a db 1,2,3,4,5,6,7,8 ;标号ab后面没有":",因此它们是可以同时描述内存地址和单元长度的标号。
  4. ;标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元
  5. b dw 0 ;标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元。
  6. start : mov si,0
  7. mov cx,8
  8. s : mov al,a[si]
  9. mov ah,0
  10. add b,ax
  11. inc si
  12. loop s
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start

使用数据标号来描述存储数据的单元的地址和长度。

  1. assume cs:code
  2. code segment
  3. a db 1,2,3,4,5,6,7,8 ;标号ab后面没有":",因此它们是可以同时描述内存地址和单元长度的标号。
  4. ;标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元
  5. b dw 0 ;标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元。
  6. start : mov si,0
  7. mov cx,8
  8. s : mov al,a[si]
  9. mov ah,0
  10. add b,ax
  11. inc si
  12. loop s
  13. mov ax,4c00h
  14. int 21h
  15. code ends
  16. end start
  1. data segment
  2. a db 1,2,3,4,5,6,7,8
  3. b dw 0
  4. c dw a, b ;等价于c dw offset a, offset b
  5. ;数据标号c处存储的两个字型数据为标号ab 的偏移地址
  6. data ends
  7. data segment
  8. a db 1,2,3,4,5,6,7,8
  9. b dw 0
  10. c dd a,b ;等价于c dw offset a, seg a, offset b, seg b
  11. ;数据标号c处存储的两个双字型数据为标号a的偏移地址和段地址、标号b 的偏移地址和段地址
  12. data ends

seg操作符,功能为取得某一标号的段地址
建立一张表,表中依次存储字符“0”~“F”,我们可以通过数值0 ~ 15直接查找到对应的字符

  1. assume cs:code
  2. code segment
  3. start:
  4. mov al,0eh
  5. call showbyte
  6. mov ax,4c00h
  7. int 21h
  8. ;子程序:
  9. ;用al传送要显示的数据
  10. showbyte:
  11. jmp short show
  12. table db '0123456789ABCDEF' ;字符表
  13. show: push bx
  14. push es
  15. mov ah,al
  16. shr ah,1
  17. shr ah,1
  18. shr ah,1
  19. shr ah,1 ;右移4位,ah中得到高4位的值
  20. and al,00001111b ;al中为低4位的值
  21. mov bl,ah
  22. mov bh,0
  23. mov ah,table[bx] ;用高4位的值作为相对于table的偏移,取得对应的字符
  24. mov bx,0b800h
  25. mov es,bx
  26. mov es:[160*12+40*2],ah
  27. mov bl,al
  28. mov bh,0
  29. mov al,table[bx] ;用低4位的值作为相对于table的偏移,取得对应的字符
  30. mov es:[160*12+40*2+2],al
  31. pop es
  32. pop bx
  33. ret
  34. code ends
  35. end start

十五、 指令系统总结

我们对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 等前缀指令配合使用。