汇编实验16 编写包含多个功能子程序的中断例程——浅谈直接地址表
这是王爽《汇编语言(第三版)》的第16个实验,本章的内容就是介绍了一种编程技巧——直接定址表,可以认为是一种以空间换时间的编程策略,相对于算法竞赛中的“打表法”,可以使程序变得更加简洁优美,避免过于繁琐的分支结构。
好吧,好话就说到这里。本次实验如果要照搬以前的套路,肯定是失败的!具体来说,如果要照搬书中代码(稍微做些修改),再把中断例程安装到0:200h之后的内存空间中,那么别用直接定址表,那是无效的,程序一定跳转到天边去了。为什么会这样,请继续看下去。
任务
安装一个新的int 0中断例程,为显示输出提供以下功能的子程序:
- 清屏
- 设置前景色
- 设置背景色
- 向上滚动一行
入口参数说明:
-
用寄存器ah传递功能号:
- 0表示清屏
- 1表示设置前景色
- 2表示设置背景色
- 3表示向上滚动一行
-
对于1、2号功能,用al传递颜色值,(al) = 0,1,2,…,7
预备知识
标号
有这样一类标号,它不但代表内存单元地址,而且还隐含长度信息,即在此标号下的内存单元是字节(byte)单元,还是字(word)单元,还是双字(dword)单元。
例如
data segment
s dd 2
a db 1,2,3,4,5,6,7,8
b dw 0
data ends
assume ds:data
这里的标号a,b,s之后没有冒号,他们同时描述了内存单元地址和单元长度。标号a描述了的地址ds:4
,从这个地址开始(程序中使用标号a的)内存单元都是字节单元。标号b描述了地址ds:12
,从这个地址开始 (程序中使用标号b的)内存单元都是字单元。标号s描述了地址ds:0
,从这个地址开始 (程序中使用标号c的)内存单元都是双字单元。
举几个例子:
mov ax,b
相对于mov ax,ds:[12]
mov b, 2
相对于mov word ptr ds:[12],2
inc b
相对于inc word ptr ds:[12]
这些指令中,标号b代表一个内存单元,地址为ds:12
,长度为2个字节。
指令mov al,b
是错误的,因为al是8位寄存器,而b代表字单元。
mov al,a[si]
相当于mov al,ds:[4+si]
mov al,a[3]
相当于mov al,ds:[4+3]
mov al,a[bx+si+3]
相当于mov al,ds:[4+bx+si+3]
要想在某一个段中使用标号访问数据, 必须要使用伪指令assum将标号所在的段和一个段寄存器联系起来,否则编译器无法确定标号的段地址。这里的标号在data段中,我把data段作为数据段使用,因此用伪指令assume ds:data
将告诉编译器标号所在的段地址。
再比如
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b
;相当于 c dw offset a,offset b
d dd a,b
;相当于 d dw offset a,seg a,offset b,seg a
data ends
这里seg
操作符是取得某一标号的段地址。
陷阱
下面给出setscreen子程序(但还不是中断处理程序,用ret返回主程序,而不是iret,注意一下)这里用了直接定址表的技巧,但是这种技巧有一些注意点,我们不能像之前一样依葫芦画瓢地完成实验任务。
;*************************************************************
;子程序setsreen
;功能:(1)清屏(2)设置前景色(3)设置背景色(4)向上滚动一行
;参数:
;(1)ah寄存器传递功能号: 0表示清屏
; 1表示设置前景色
; 2表示设置背景色
; 3表示向上滚动一行
;(2)对于1、2号功能,用al寄存器传送颜色,al=0,1,2,...,7
;**********************************************************
setscreen: jmp short set
table dw func0,func1,func2,func3
set: push bx
cmp ah,3
ja sret
mov bl,ah
mov bh,0
add bx,bx
call word ptr table[bx]
sret: pop bx
ret
;------------------------------------------------------
;0号功能:清屏
func0: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
func0s: mov byte ptr es:[bx],' '
add bx,2
loop func0s
pop es
pop cx
pop bx
ret
;------------------------------------------------------
;1号功能:设置前景色
func1: push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
func1s: and byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop func1s
pop es
pop cx
pop bx
ret
;------------------------------------------------------
;2号功能:设置背景色
func2: push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
func2s: and byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop func2s
pop es
pop cx
pop bx
ret
;------------------------------------------------------
;3号功能:向上滚动一行
func3: push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160
mov di,0
cld
;书中给出的程序用一个循环来处理第n+1行复制到第n行,虽然思路清晰
;但是过于复杂,由于si和di总是相差一行(160字节),直接进行如下的串处理
;就可以了。
mov cx,23*160
rep movsb
mov cx,80
mov si,0
func3s1: mov byte ptr [160*24+si],' '
add si,2
loop func3s1
pop ds
pop es
pop di
pop si
pop cx
ret
如果这段代码被安装到0:200h处的内存单元中,直接地址表就会失效!。原因就出在标号上,编译器会将标号翻译为在当前代码段中相应的偏移地址,也就是说在只有当前的代码段,地址表的定位是正确的。但是如果把这段(表现为二进制数据形式的)指令复制到另外一块内存区域,每条指令在内存中的位置发生巨大变化,原来的直接定址表就不适用了,它提供的偏移地址和现在的子程序位置不对应了,程序的跳转就会失控。
实现
如果我们一定要用直接定址表,那该怎么办呢?很简单,直接修改中断向量表即可。代码如下:
mov word ptr ds:[0*4],offset setscreen
mov word ptr ds:[0*4+2],cs
也就是说,我们不能将子程序安装到特定位置。这样的结果是,一旦主程序执行完毕,我们写的新的中断程序也就完成使命,不能一直在内存中逗留,最后被其它数据覆盖。
下面只给出主程序:
assume cs:code,ss:stack
stack segment
db 256 dup(0)
stack ends
code segment
main: mov ax,stack
mov ss,ax
mov sp,256
mov ax,0
mov ds,ax
;保存原来的0号中断向量
push word ptr ds:[0*4]
push word ptr ds:[0*4+2]
;修改0号中断向量
mov word ptr ds:[0*4],offset setscreen
mov word ptr ds:[0*4+2],cs
mov ax,0204h
int 0
mov ax,0102h
int 0
;恢复0号中断向量
pop word ptr ds:[0*4+2]
pop word ptr ds:[0*4]
int 0
mov ax,4c00h
int 21h
;------------------------------------------------------
setscreen:
;子程序部分略
;------------------------------------------------------
code ends
end main
如果一定要安装都内存空间中的安全区域0:200h中,就不应该使用直接地址表。子程序的主体部分可以这么写:
setsreen: cmp al,0
je do0
cmp al,1
je do1
cmp al,2
je do2
cmp al,3
je do3
jmp short sret
do0: call func0
jmp short sret
do1: call func1
jmp short sret
do2: call func2
jmp short sret
do3: call func3
sret: iret
但这样做实在是有些难看。
总结
两个星期紧张的期末复习过去了,也终于放假了。此中有无数的槽点不吐不快,可笑的是我恰恰是个懒人,懒得吐槽。之前由于复习,学习的进程中断了,正好假期里我有的是时间。马上王爽的书我也要看完了,前面的内容却已经变得模糊,不像刚开始学的时候那么清晰,没关系,学习的过程是充实的,我已经很满足了,知识总是随着时间慢慢流失(如果不经常使用的话),还好汇编并非我的饭碗(再说不会的时候可以翻书嘛,知道书上哪里有这个东西就行)。学完王爽的汇编教材后,我也不打算在复习梳理一遍(一个字,懒),毕竟我要把注意力转向数据结构和算法的学习上去了(也不是说就完全抛下汇编语言了)。汇编语言还是很有趣,很重要的。我已经买了《x86汇编语言:从实模式到保护模式》这本书,作为我的第二本汇编教材。说实话,我对汇编语言的要求也不高,能读懂就行,同时通过汇编对计算机体系结构有一个侧面的认识和理解就心满意足了。一句话,就是看看热闹,摸摸门道,好好玩一玩就行(打个不恰当的比方,如果把自己精通的高级语言比作正式的配偶,学汇编语言就是在外面找个小情人……这么说的话,我现在还没有老婆……笑)。