千哥读书笔记:汇编语言(王爽第四版)第15章15.4 编写int 9 中断例程

这一章的难度相对较低,但还是有很多细节没有讲清楚,或者这本身就是王爽老师写书的一种风格,即故意不讲清楚一些细节,而让读者自己去摸索,从而加深学习知识的印象。

比如本章的15.4 编写int 9 中断例程,所举的例子,“在屏幕中间依次显示”a"~"z",在显示的过程中,按下ESC键后,改变显示的颜色”,就有一些细节需要自己去摸索和理解。

先复习一下本章的知识要点:

一、接口芯片和端口

1、外设接口芯片的内部有若干寄存器,CPU将这些寄存器当作端口来访问(需要复习14章内容)。

2、外设的输入不直接送入内存和CPU,而是送入相关的接口芯片的端口中。

3、CPU向外设的输入也不是直接送入外设,而是先送入端口中,再由相关芯片送到外设。

4、CPU向外设输出的控制命令,是先送到相关芯片的端口中,然后再由相关的芯片根据命令对外设实施控制。

二、外中断信息

当CPU的内部有需要处理的事情发生,将产生中断信息,引发中断过程,这种中断信息来自CPU内部;而当外设的输入到达,相关外设的芯片向CPU发出中断信息,CPU可以检测到外设发送过来的中断信息,引发中断过程,处理外设的输入,这就是外部中断。

外部中断又分为两种:

1、可屏蔽中断

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

1)如果IF = 1,则CPU在执行完当前的指令后会响应可屏蔽中断。

2)如果IF = 0,则CPU不响应可屏蔽中断。

3)可屏蔽中断信息来自于CPU外部,中断类型码是通过数据总线送入CPU的。

而关于内部中断引发的过程,在第12章有这样的描述:

1)取中断类型码

2)标志寄存器入栈,IF=0,TF=0

3)CS、IP入栈

4)(IP)=(n*4),(CS)=(n*4+2)

而外部中断引发的过程,除在第1步的实现上有所不同外,基本上和内中断过程相同。而将IF设置为0的原因,就在于进入中断处理程序后,必须禁止其他的可屏蔽中断。

有了以上的知识点,我们可以画一个可屏蔽中断的流程图来帮助理解:

2、不可屏蔽中断

这种中断是CPU必须响应的外部中断,对于8086CPU,不可屏蔽中断的中断类型码固定为2,所以在中断过程中,不需要取中断类型码,其中断过程为:

1)标志寄存器入栈,IF=0,TF=0

2)CS、IP入栈

3)(IP)=(8),(CS)=(0AH)

不可屏蔽中断是在系统中有必须处理的紧急情况发生时用来通知CPU的中断信息。

三、PC机键盘对键盘的处理过程

1、键盘中有一个芯片对键盘上的每个键的开关状态进行扫描。

2、按下一个键时,产生一个叫做“通码”的扫描码,通码被送入主板上的相关接口芯片的寄存器中,该寄存器的端口地址为60h。

3、松开一个键时,产生一个叫做“断码”的扫描码,断码也被送入60h端口中。

4、扫描码长度为一个字节,通码的第7位为0,断码的第7位为1,即:断码= 通码+80h

表15.1是键盘上部分键的扫描码,只列出了通码,断码=通码+80h

四、9号中断的引发过程

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

2、如果IF=1,则响应中断,引发中断过程,转去执行int 9中断例程。

3、BIOS提供了int 9中断例程,主要工作如下:

1)读出60h端口中的扫描码。

这一步,是通过in指令来完成,并且只能用al存放从端口中读入的数据,因为前面提到的,扫描码长度为一个字节(访问8位端口时用al)。关于int指令读取端口数据的操作,在原书中第266页有说明(al或ax来存放从端口读入的数据)。

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

3)如果是控制键(比如Ctrl)和切换键(比如CapLock)的扫描码,则将其转化为状态字节,写入内存中存储状态字节的单元。而在0040:17单元,则存储键盘状态字节,记录了控制键和切换键的状态,相关信息如下:

4)对键盘系统进行相关的控制,比如,向相关芯片发出应答信息。

以上流程如下图:

有了以上背景知识,就可以来深入理解本章15.4 编写int 9中断例程

这个例程的要求是:编程,在屏幕中间依次显示“a”“z”,并可以让人看清。在显示的过程中,按下Esc 键后,改变显示的颜色。

先贴出这个程序第一个版本的源代码,然后逐步分析一些重点需要理解的地方:

====================

assume cs:code

stack segment

  db 128 dup(0)

stack ends

data segment

  dw 128 dup(0)

data ends

code segment

    start:mov ax,stack

          mov ss,ax

          mov sp,128

          mov ax,data

          mov ds,ax

          mov ax,0

          mov es,ax

          push es:[9*4]

          pop ds:[0]

          push es:[9*4+2]

          pop ds:[2];将原来的int 9中断例程的入口地址保存在ds:0、ds:2单元中

          mov word ptr es:[9*4],offset int9

          mov es:[9*4+2],cs ;在中断向量表中设置新的int 9中断例程的入口地址

          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];将中断向量表中int 9中断例程的入口恢复为原来的地址

          mov ax,4c00h

          int 21

    delay:push ax

          push dx

          mov dx,1000h;循环10000000h次,读者可以根据自己机器的速度调整循环次数

          mov ax,0

       s1:sub ax,1

          sbb dx,0

          cmp ax,0

          jne s1

          cmp dx,0

          jne s1

          pop dx

          pop ax

          ret

    ;--------------以下为新的int 9 中断例程----------------

    int9: push ax

          push bx

          push es

          in al,60h

          pushf

          pushf

          pop bx

          and bh,11111100b

          push bx

          popf

          call dword ptr ds:[0];对int 指令进行模拟,调用原来的int 9中断例程

          cmp al,1

          jne int9ret

          mov ax,0b800h

          mov es,ax

          inc byte ptr es:[160*12+40*2+1] ;将属性值加1,改变颜色

 int9ret: pop es

          pop bx

          pop ax

          iret

code ends

end start

====================

这个示例程序的主要设计思路是:

1、在屏幕中间依次显示“a”~“z”。

2、重新编写int 9中断例程,按照自己的意图(即按下Esc 键后,改变字符显示的颜色)来处理键盘的输入。

3、在重新编写的int 9中断例程中,调用BIOS原有的int 9中断例程。

关于第一步的基本原理,原书有详细讲述,本笔记不作阐述。而这一步最难理解的部分,就是以delay为标号的那几段延时代码:

====================

delay:push ax

          push dx

          mov dx,1000h;循环10000000h次,读者可以根据自己机器的速度调整循环次数

          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

====================

问题来了:

1、在代码注释里,“循环10000000h次”是怎么算出来的?

2、在原书276页有一段话:因为现在CPU的速度都很快,所以循环的次数一定要大,用两个16位寄存器来存放32位的循环次数,其中对应的代码是:

sub ax,1

sbb dx,0

那么,通过sub和sbb指令,怎么就实现了32位的循环次数?而回答了第2个问题,第一个问题也就迎刃而解。但书中并没有详细解释。显然,这又是王爽老师挖的一个坑,得让我们自己去填。

我们先来看delay中的这段代码:

====================

          mov dx,1000h

          mov ax,0

       s1:sub ax,1

          sbb dx,0

          cmp ax,0

          jne s1

          cmp dx,0

          jne s1

====================

1、首先,dx被赋值为1000h,ax被赋值为0。在执行sub ax,1这行代码时,ax 由0减去1,得到FFFF,这是-1的16位(因为ax是16位的)补码,也即二进制1111111111111111;此时,由于0减1产生了借位,所以CF值为1。

2、所以,在第一层循环(sub ax,1至第一个jne s1)中,对ax的减法操作实际上就是FFFF+1 = 10000h次。

3、subb dx,0,根据原书第222页的说明,(dx)=(dx)-0-CF。使得在进行第一次第二层循环(sub ax,1至第二个jne s1)时, dx = 1000h - 0 - 1 = FFF。

4、在从第二次进行第一层循环(sub ax,1至第一个jne s1)开始到该循环结束,由于sub ax,1的操作是FFFF-1,没有产生借位,故CF值在执行这条指令之后为0,直到后续的第一层循环,ax再次为0之前CF值都为0。故在进行第二层循环时,dx = FFF- 0 -0 = FFF。计算结束后,由于dx不等于0,故又将回到第一层循环(sub ax,1至第一个jne s1),由此最终产生了10000h次的第一层循环。

5、在每次第一层循环(sub ax,1至第一个jne s1)结束后,ax在每次自减1的情况下又将归零,CF又在下一步的计算中(sub ax,1)产生借位,CF值又为1。然后将在第二层循环中将dx再次减1,比如:dx = FFF- 0 -1 = FFE

6、由于第一层循环与第二层循环的嵌套结合,对ax的操作循环了10000h次(FFFF+1),对dx的操作循环了1000h次,故总循环次数达到了10000X1000 = 10000000h次。

关于这个原理,可以用debug来观察一下ax与dx的变化信息。总结一下:

这种程序设计思想,就是结合sub和sbb指令,用两个16位寄存器AX、DX来存放32位的循环次数。用DX:AX表示循环次数,即高位是DX(第二层循环),低位是AX(第一层循环)。

而这么重要的细节,王爽老师居然一笔带过,没有20年以上的脑血栓真是无法理解这种设计思想。

接下来,看第二步和第三步:

2、重新编写int 9中断例程,按照自己的意图(即按下Esc 键后,改变字符显示的颜色)来处理键盘的输入。

3、在重新编写的int 9中断例程中,调用BIOS原有的int 9中断例程。

其程序设计的思想是:

1、用重新设计的int 9中断例程来代替BIOS原有的 int9中断例程,也就是需要增加改变字符显示的颜色的功能(这就有一点hacking的味道了)。

2、在重新设计的int 9中断例程中,调用BIOS原有的int 9中断例程,从而处理其他的硬件细节。也就是说,原有的int 9的中断例程的运作逻辑不能改变,否则将影响系统和其他程序的正常运行。

对于第一点,首先就要保存原来的int 9中断例程的入口地址,然后将新的int 9中断例程的地址注册到原int 9中断例程的入口中,最后在程序结束时,还要将原来的int 9中断例程的入口地址进行恢复。于是,这就有了以下几段代码:

====================

;保存原来的int 9中断例程的入口地址

 push es:[9*4]

 pop ds:[0]

 push es:[9*4+2]

 pop ds:[2];将原来的int 9中断例程的入口地址保存在ds:0、ds:2单元中

;将新的int 9中断例程的地址注册到原int 9中断例程的入口中

 mov word ptr es:[9*4],offset int9

 mov es:[9*4+2],cs ;在中断向量表中设置新的int 9中断例程的入口地址

…………;中间代码省略,将原来的int 9中断例程的入口地址进行恢复

push ds:[0]

pop es:[9*4]

push ds:[2]

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

====================

这几段代码很容易理解,大体上是通过压栈和出栈的方式,改变新旧int 9中断例程的入口注册。只是在将新的int 9中断例程的地址注册到原int 9中断例程的入口中的时候,用到了offset 来获得新的int 9的偏移地址,并且通过cs获得段地址,然后通过mov 指令将新地址的内容复制到es:[9*4]、es:[9*4+2]两个单元。

而重新设计的int 9中断例程,书中提到,是不能用 int 指令来调用的。

千哥的理解是,这可能和int指令的内部运作机制有关,有可能是绑定了原来的中断例程入口地址,现在已将地址注册进行了改变,用int命令调用中断例程的话会产生错误。当然,这只是猜想,毕竟有些隐藏的机制我们是不清楚的。

按照这个思路,就要用别的指令来对int指令进行模拟。根据对CPU执行int指令的分析,其原理分为五个步骤:

1、取中断类型码n

2、标志寄存器入栈

3、IF=0,TF=0

4、CS、IP入栈

5、(IP)=(n*4),(CS)=(n*4+2)

以上步骤,取中断类型码的目的,是为了定位中断例程的入口地址,而现在入口地址已经知道,就是改变后的ds:[0]、ds:[2]两个单元,所以用别的指令模拟int 指令时,不需要做第一步。也就是:

1、标志寄存器入栈

2、IF=0,TF=0

3、CS、IP入栈

4、(IP)=((ds)*16+0),(CS)=((ds)*16+2)

而改进后的步骤的第3、4步,其功能和call dword ptr ds:[0]的功能一样,也是:

1、CS、IP入栈

2、(IP)=((ds)*16+0),(CS)=((ds)*16+2)

这里又涉及对call指令的复习(原书10.6节内容)。

对于call dword ptr 内存单元地址

相当于进行:

push CS

push IP

jmp dword ptr   内存单元地址

由于有“dword”这个关键词 ,会将4个内存单元地址(每个单元1个字节),也就是2个字来保存转移的目的段地址和转移的目的偏移地址。

而“jmp dword ptr   内存单元地址”指令,就是从内存单元地址开始存放2个字,高地址处的字(16位)是转移的目的段地址,低地址处(16位)是转移的目的偏移地址,然后实施目的地址的跳转。

所以,模拟int指令的过程,就变为了:

1、标志寄存器入栈

2、IF=0,TF=0

3、call dword ptr ds:[0]

对于第二步,也就是IF=0,TF=0,用的是下面的指令来实现:

====================

pushf

pop bx

and bh,11111100b

push bx

popf

====================

意思是:先是将标志寄存器入栈,然后用pop弹出,赋值给bx,然后通过and,将bh的第7位和第8位设置为0,然后再将bx入栈,再用popf对标志寄存器重新设置,从而实现IF=0,TF=0。

需注意的是,这里的标志寄存器入栈(pushf),和前面的第一步,标志寄存器入栈不是一回事。这里的pushf,是为了改变bx这个中间变量的值。

所以,调用原有的int 9中断例程的完整代码就是:

====================

pushf

pushf

pop bx

and bh,11111100b

push bx

popf

call dword ptr ds:[0];对int 指令进行模拟,调用原来的int 9中断例程

====================

最后,进行对新的int 9中断例程的完整分析:

====================

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];对int 指令进行模拟,调用原来的int 9中断例程

          cmp al,1

          jne int9ret

          mov ax,0b800h

          mov es,ax

          inc byte ptr es:[160*12+40*2+1] ;将属性值加1,改变颜色

 int9ret: pop es

          pop bx

          pop ax

          iret

====================

一开始的push ax、 push bx 、push es,是为了保存原有的现场信息,这个不用多说。

接下来的in al,60h,就是从端口60h处读出键盘的输入信息。

然后就是调用原有的int 9中断例程,即从第1个pushf直到call dword ptr ds:[0]。这段代码也就模拟了原有int 9中断例程,从而实现不改变原有的int 9的中断例程运作逻辑。

也就是说,原来的int 9 该干什么就继续干什么,至于后面要加什么新的功能,由后面的代码说了算。

那后面的代码又干了什么事呢?就是在接收到ESC这个键的信号之后,让字符改变颜色。

====================

cmp al,1

jne int9ret

mov ax,0b800h

mov es,ax

inc byte ptr es:[160*12+40*2+1] ;将属性值加1,改变颜色

int9ret: pop es

          pop bx

          pop ax

          iret

====================

前面提到,in al,60h,就是从端口60h处读出键盘的输入。这里用cmp 和jne指令进行比较,看看al是否为1,如果不为1(即不是ESC键),就跳转到int9ret标号的代码段,然后进行现场信息恢复,最后用iret指令,来对call指令进行返回。

如果al为1,就在0b800h显示缓冲区的[160*12+40*2+1],将字符属性值加1,从而改变颜色。

这里需要说明的是,call指令和iret指令是成对出现的,当用call指令完成对原有int 9中断例程的调用,并且处理完其他的硬件信息之后,必须用iret指令进行返回。

至此,15.4 编写int 9 中断例程的例子分析完毕。

千哥的体会是,魔鬼真的就是在细节之中。王爽老师写的书,有此内容就如同嚼毛牛肉干,一开始很难以下口,但慢慢咀嚼却又风味无穷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值