汇编实验16 编写包含多个功能子程序的中断例程——浅谈直接地址表

时间:2021-03-05 01:15:26

汇编实验16 编写包含多个功能子程序的中断例程——浅谈直接地址表

这是王爽《汇编语言(第三版)》的第16个实验,本章的内容就是介绍了一种编程技巧——直接定址表,可以认为是一种以空间换时间的编程策略,相对于算法竞赛中的“打表法”,可以使程序变得更加简洁优美,避免过于繁琐的分支结构。

好吧,好话就说到这里。本次实验如果要照搬以前的套路,肯定是失败的!具体来说,如果要照搬书中代码(稍微做些修改),再把中断例程安装到0:200h之后的内存空间中,那么别用直接定址表,那是无效的,程序一定跳转到天边去了。为什么会这样,请继续看下去。

任务

安装一个新的int 0中断例程,为显示输出提供以下功能的子程序:

  1. 清屏
  2. 设置前景色
  3. 设置背景色
  4. 向上滚动一行

入口参数说明:

  • 用寄存器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汇编语言:从实模式到保护模式》这本书,作为我的第二本汇编教材。说实话,我对汇编语言的要求也不高,能读懂就行,同时通过汇编对计算机体系结构有一个侧面的认识和理解就心满意足了。一句话,就是看看热闹,摸摸门道,好好玩一玩就行(打个不恰当的比方,如果把自己精通的高级语言比作正式的配偶,学汇编语言就是在外面找个小情人……这么说的话,我现在还没有老婆……笑)。