在12章学习过两种中断,分别是

  1. 除法溢出产生的0号中断
  2. 检测到标志寄存器的TF位为1,产生的单步中断

这一章,学习由于int指令引发的中断

13.1 int指令

int指令的格式为:

  1. int n ; n为中断类型码

该指令的功能是引发中断过程

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

  1. 取中断类型码n;
  2. 标志寄存器入栈,IF=0,TF=0;
  3. CS和IP入栈
  4. (IP)=(n4), (CS)=(n4+2)

从此转去执行n号中断的中断处理程序。

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

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov byte ptr es:[12*160+40*2],'!'
  6. int 0
  7. code ends
  8. end start

程序虽然没有做除法,但是在结尾处使用了int 0指令,这个指令的功能是和除法溢出产生的中断功能是一样的。

一般情况下,系统将一些具有一定功能的子程序,以中断处理程序的方式提供给应用程序调用。我们在编程的时候,可以用int指令调用这些子程序,我们将中断处理程序称为中断例程。

13.2 编写供应用程序调用的中断例程

前面,我们已经编写过中断0的中断例程了,现在我们讨论可以供应用程序调用的中断例程的编写方法。

§ 第13章 int指令 - 图1 问题一:编写、安装中断7ch的中断例程

  • 功能:求一个字型数据的平方
  • 参数:(ax)=要计算的数据
  • 返回值:dx、ax中存放结果的高16位和低16位
  • 应用举例:求2*3456^2
  1. assume cs:code
  2. code segment
  3. start: mov ax,3456 ; (ax)=3456
  4. int 7ch ; 调用7ch中断例程,计算(ax)^2
  5. add ax,ax
  6. adc dx,dx ; 2*(ax)^2,并dx:ax存放结果
  7. mov ax,4c00h
  8. int 21h
  9. code ends
  10. end start

分析一下,我们要做以下3部分工作

  1. 编写实现求平方功能的程序;
  2. 安装程序,将其安装在0:200处
  3. 设置中断向量表,将程序的入口地址保存在7ch表项中,使其成为中断7ch的中断例程

安装程序如下

  1. assume cs:code
  2. code segment
  3. start: mov ax,cs
  4. mov ds,ax
  5. mov si,offset sqr ; 设置ds:si指向源地址
  6. mov ax,0
  7. mov es,ax
  8. mov di,200h ; 设置es:di指向目的地址
  9. mov cx,offset sqrend-offset sqr ; 设置cx为传输长度
  10. cld ; 设置传输方向为正
  11. rep movsb
  12. mov ax,0
  13. mov es,ax
  14. mov word ptr es:[7ch*4],200h
  15. mov word ptr es:[7ch*4+2],0
  16. mov ax,4c00h
  17. int 21h
  18. sqr:mul ax
  19. iret
  20. sqrend:nop
  21. code ends
  22. end start

注意,在中断例程sqr的最后,要使用iret指令。用汇编语法描述,iret指令的功能为:

  1. pop IP
  2. pop CS
  3. popf

CPU执行完int 7ch指令进入中断例程之前,标志寄存器、当前的CS和IP被压入栈中,在执行完中断例程后,应该用iret指令恢复int 7ch执行前的标志寄存器和CS、IP的值,从而接着执行应用程序。

§ 第13章 int指令 - 图2

§ 第13章 int指令 - 图3 问题二:编写、安装中断7ch的中断例程。

  • 功能:将一个全是字母,以0结尾的字符串,转化为大写
  • 参数:ds:si 指向字符串的首地址
  • 应用举例:将data段中的字符串转化为大写
  1. assume cs:code
  2. data segment
  3. db 'conversation',0
  4. data ends
  5. code segment
  6. start: mov ax,data
  7. mov ds,ax
  8. mov si,0 ; 源地址ds:si = data:0
  9. int 7ch ; 引起7c中断例程
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start

安装程序如下。

  1. assume cs:code
  2. code segment
  3. start: mov ax,cs
  4. mov ds,ax
  5. mov si,offset capital
  6. mov ax,0
  7. mov es,ax
  8. mov di,200h
  9. mov cs,offset capitalend-offset capital
  10. cld
  11. rep movsb
  12. mov ax,0
  13. mov es,ax
  14. mov word ptr es:[7ch*4],200h
  15. mov word ptr es:[7ch*4+2],0
  16. mov ax,4c00h
  17. int 21h
  18. capital: push cx
  19. push si
  20. change: mov cl,[si]
  21. mov ch,0
  22. jcxz ok
  23. and byte ptr [si],11011111b
  24. inc si
  25. jmp short change
  26. ok: pop si
  27. pop cx
  28. iret
  29. capitalend: nop
  30. code ends
  31. end start

在中断例程capital中用到了寄存器si和cx,编写中断例程和编写子程序的时候具有同样的问题,就是要避免寄存器的冲突。应该注意例程中用到的寄存器的值的保存和恢复。

13.3 对int、iret和栈的深入理解

问题:用7ch中断例程完成loop指令的功能。

loop s的执行需要两个信息,循环次数和 s 的位移。所以,7ch中断例程要完成loop指令的功能,也需要这两个信息作为参数。我们用cx放循环的次数,用bx存放位移。

应用举例:在屏幕中间显示80个”!”。

  1. assume cs:code
  2. code segment
  3. start: mov ax,0b800h
  4. mov es,ax
  5. mov di,160*12
  6. mov bx,offset s-offset se ; 设置从标号se到标号s的转移位移
  7. mov cx,80
  8. s: mov byte ptr es:[di],'!'
  9. add di,2
  10. int 7ch ; 进入7ch例程,如果(cx)≠0,转移到标号s
  11. se: nop
  12. mov ax,4c00h
  13. int 21h
  14. code ends
  15. end start

在上面的程序中,使用int 7ch调用 7ch 中断例程进行转移,用bx传递转移的位移。

分析:为了模拟loop指令,7ch中断例程应具备以下的能力

  1. dec cx
  2. 如果(cx)≠0,转移到标号s处执行,否则向下执行

下面我们分析7ch中断例程如何实现到目的地址的转移

  1. 转到标号s显然应设§ 第13章 int指令 - 图4%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E6%AE%B5%E5%9C%B0%E5%9D%80#card=math&code=%28CS%29%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E6%AE%B5%E5%9C%B0%E5%9D%80&id=mXDpz),§ 第13章 int指令 - 图5%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E5%81%8F%E7%A7%BB%E5%9C%B0%E5%9D%80#card=math&code=%28IP%29%3D%E6%A0%87%E5%8F%B7s%E7%9A%84%E5%81%8F%E7%A7%BB%E5%9C%B0%E5%9D%80&id=vAJRl)
  2. 那么,中断例程如何得到标号s的段地址和偏移地址呢?
    int 7ch引发中断过程后,进入7ch中断例程,在中断过程中,当前的标志寄存器、CS和IP都要压栈,此时压入的CS和IP中的内容,分别是调用程序的段地址和int 7ch后一条指令的偏移地址。
    可见,在中断例程中,可以从栈里取得标号 s 的段地址和标号 se的偏移地址,而用标号 se的偏移地址加上bx中存放的转移位移就可以得到标号 s的偏移地址。
  3. 现在知道,可以从栈中直接和间接地得到标号 s 的段地址和偏移地址,那么如何用它们设置 CS:IP呢?
    可以利用iret指令,我们将栈中的 se 的偏移地址加上bx中的转移位移,则栈中的 se的偏移地址就变为了 s的偏移地址。我们再使用iret指令,用栈中的内容设置CS、IP,从而实现转移到标号 s处。

7ch中断例程如下

  1. lp: push bp
  2. mov bp,sp
  3. dec cx
  4. jcxz lpret
  5. add [bp+2],bx
  6. lpret: pop bp
  7. iret

因为要访问栈,使用了bp,在程序开始处将bp 入栈保存,结束时出栈恢复。当要修改栈中 se的偏移地址的时候,栈中的情况为:栈顶处时 bp原来的数值,下面是 se的偏移地址,再下面时 s 的段地址,再下面是标志寄存器的值。

而此时,bp中为栈顶的偏移地址,所以§ 第13章 int指令 - 图6*16%2B(bp)%2B2#card=math&code=%28ss%29%2A16%2B%28bp%29%2B2&id=Y1VOV)处为 se 的偏移地址,将它加上 bx 中的转移位移就变为 s 的偏移地址。最后用iret出栈返回,CS:IP即从标号 s 处开始执行指令。

如果§ 第13章 int指令 - 图7%3D0#card=math&code=%28cx%29%3D0&id=EBF1S),则不需要修改栈中 se 的偏移地址,直接返回即可。CPU从标号 se 处向下执行指令。

13.4 BIOS和DOS所提供的中断例程

在系统板的ROM种存放着一套程序,称为BIOS(Basic Input Output System,基本输入输出系统),BIOS种主要包含以下内容

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

操作系统DOS也提供了中断例程,从操作系统的角度来看,DOS的中断例程就是操作系统向程序员提供的编程资源

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

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

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

前面的课程中,我们都是自己编写中断例程,将他们放到安装程序中,然后运行安装程序,将它们安装到指定的内存中。此后,别的应用程序才可以调用。

而 BIOS 和 DOS 提供的中断例程是如何安装到内存中的呢?我们下面讲解它们的安装过程。

§ 第13章 int指令 - 图8

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

§ 第13章 int指令 - 图9

13.6 BIOS中断例程应用

下面我们举几个例子,来看一下 BIOS 中断例程的应用。

int 10h中断例程是 BIOS 提供的中断例程,其中包含了多个和屏幕输出相关的子程序。

一般来说,一个供程序员调用的中断例程中往往包含多个子程序,中断例程内部用传递进来的参数来决定执行哪一个子程序。BIOS 和 DOS提供的中断例程,都用 ah来传递内部子程序的编号。

§ 第13章 int指令 - 图10 下面看一下int 10h中断例程的设置光标位置功能。

  1. mov ah,2 ; 置光标
  2. mov bh,0 ; 0
  3. mov dh,5 ; dh中放行号
  4. mov dl,12 ; dl中放列号
  5. int 10h

(ah)=2 表示调用第 10h 号中断例程的 2 号子程序,功能为设置光标位置,可以提供光标的位置,分别为

  • 行号:80*25字符模式下(0~24)
  • 列好:80*25字符模式下(0~79)
  • 页号

上述的代码通过(bh)=0, (dh)=5, (dl)=12将光标设置到了第0页,第5行,第12列。

bh 中页号的含义:内存地址空间中,B8000H~BFFFFFH 共占 32KB 的空间,为80*25彩色字符模式的显示缓冲区,一屏的内容在显示缓冲区共占 4000 个字节。

显示缓冲区分为 8 页,每页 4KB,显示器可以显示任意一页的内容。一般情况下,显示第0页的内容。也就是说,通常情况下,B8000H~B8F9FH 中的 4000个字节的内容将出现在显示器上。

§ 第13章 int指令 - 图11 再看一下 int 10h中断例程的在光标位置显示字符功能

  1. mov ah,9 ; 在光标位置显示字符
  2. mov al,'a' ; 字符
  3. mov bl,7 ; 颜色属性
  4. mov bh,0 ; 0
  5. mov cx,3 ; 字符重复个数
  6. int 10h

(ah)=9表示调用第10号中断例程的9号子程序,功能为在光标位置显示字符,可以提供要显示的字符、颜色属性、页号、字符重复个数作为参数。

bl 中的颜色属性格式如下所示:

§ 第13章 int指令 - 图12

可以看出,和现存中的属性字节的格式相同。

§ 第13章 int指令 - 图13 编程:在屏幕的5行12列显示3个红底高亮闪烁绿色的 ‘a’

  1. assume cs:code
  2. code segment
  3. mov ah,2 ; 置光标
  4. mov bh,0 ; 0
  5. mov dh,5 ; dh中放行号
  6. mov dl,12 ; dl中放列好
  7. int 10h
  8. mov ah,9 ; 在光标位置显示字符
  9. mov al,'a' ; 字符
  10. mov bl,11001010b ; 颜色属性
  11. mov bh,0 ; 0
  12. mov cx,3 ; 字符重复个数
  13. int 10h
  14. mov ax,4c00h
  15. int 21h
  16. code ends
  17. end

注意,闪烁的效果必须在全屏DOS方式下才能看到。

§ 第13章 int指令 - 图14

13.7 DOS中断例程应用

int 21h中断例程是 DOS 提供的中断例程,其中包含了 DOS提供给程序员在编程时调用的子程序。

我们前面使用的是 int 21h中断例程的 4ch号功能,即程序返回功能,如下:

  1. mov ah,4ch ; 程序返回
  2. mov al,0 ; 返回值
  3. ; 或者为 mov ax,4c00h
  4. int 21h
  5. ; (ah)=4ch 表示调用第21h号中断例程的4ch号子程序,功能为程序返回,可以提供返回值作为参数

我们看一下int 21h中断例程在光标位置显示字符串的功能

  1. ds:dx 指向字符串 ; 要显示的字符串需要用 $ 作为结束符
  2. mov ah,9 ; 功能号9,表示在光标位置显示字符串
  3. int 21h
  4. ; (ah)=9表示调用第 21h 号中断例程的 9 号子程序,功能为在光标位置显示字符串,可以提供要显示字符串的地址作为参数

编程:在屏幕的5行12列显示字符串 “Welcom 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 ; 置光标
  7. mov bh,0 ; 0
  8. mov dh,5 ; dh中放行号
  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
  15. int 21h
  16. mov ax,4c00h
  17. int 21h
  18. code ends
  19. end start

上述程序在屏幕的 5 行 12 列显示字符串Welcom to masm!,直到遇见$$本身不显示,知识起到边界的作用)。

如果字符串比较长,遇到行尾,程序会自动转到下一行开头继续显示;如果到了最后一行,还能自动上卷一行。

DOS系统为程序员提供了许多可以调用的子程序,都包含在**int 21h**中断例程中

§ 第13章 int指令 - 图15