组合语言之艺术
第六节 特殊技巧的运用

  技巧的运用,关系汇编语言的效率甚钜,虽然其重要性比不上整体规划,但也可以
弥补规划的不足。
    由于「技巧」无法严格定义,兹将几种较常用的技巧介绍如下:
一、变量法:
    我非常反对在程序中采用「常数」的观念,因为常数是固定的,无法灵活应用。例
如在 IBM PC 的 BIOS 中,屏幕游标上、下、左、右位置固定设为 0,25,0,80 等常
数值。每次移动都受到这四个值的限制,故而形成不变的「视区」。若将这些常数改为
变量,且容许使用者自行改变,则立即有了可变「窗口」的功能。
    也就是说,屏幕上、下、左、右四个位置,所围起来的区域,就是我们视觉及数据
所限制的「窗口」,所有数据显示,在系统程序的控制下,无法超出此区。
    如果此四个位置值是变量,则使用者可以随意设定所需数值,如是则灵活方便,也
就是所谓的窗口处理。
    在下文三、虚拟法例中,CKFUN 该段程序即为用变量处理窗口的范例。下面这段程
序,亦为变量法的一种应用, MAP87和MAP9A 中,均需调用子程序DYBPJ1,唯一不同者,
是在该子程序中,又需分别调用不同的子程序。共享DYBPJ1的方法,是先将不同子程序
的地址,放在BP中,再行调用。
    1:MAP87:
    2:  MOV BP,OFFSET MAPF4
    3:  CALL MOVS211
    4:  CALL DYBPJ1
    5:  MOV DL,AH
    6:  ..
   11:MAP9A:
   12:  MOV BP,OFFSET MAP46
   13:  CALL DYBPJ1
   14:  MOV AH,1
   15:  ..
   20:DYBPJ1:
   21:  PUSH BX
   22:  PUSH DX
   23:  MOV DH,DL
   24:  CALL BP
   25:  POP AX
   26:  XCHG DH,AH
   27:  POP BX
   28:DYBPJRT:
   29:  RET
二、对称法:
    本法实际上就是利用间接寻址的指令,将原系对称处理,或可以调整成为对称型的
程序,灵活调用。
    下面这段程序,表面看来似乎毫不相干,但经过整理后,就有了眉目,然后再以寄
存器间接寻址,合并为一。像这种程序,规模越大,所节省的空间就越多。
    1:ABCD:
    2:  CMP DX,BUFA
    3:  JB ABCD1
    4:  CMP CL,BUFD
    5:  JB ABCD1
    6:  MOV BUFC,CL
    7:  INC BUFE
    8:  MOV BUFB,DX
    9:  DEC BUFF
   10: ABCD1:
   11:  ..
   12:  ..
     与另一段程序:
   20:EFGH:
   21:  CMP BX,BUFG
   22:  JB EFGH1
   23:  CMP CH,BUFH
   24:  JB EFGH1
   25:  INC BUFK
   26:  DEC BUFL
   27:  MOV BUFI,BX
   28:  MOV BUFJ,CH
   29:EFGH1:
   30:  ..
   31:  ..
    看来分别很大,先经过整理,得到左右两组程序:
    EFGH:    ABCD:
 CMP BX,BUFG  CMP   DX,BUFA
 JB EFGH1  JB   ABCD1
 CMP CH,BUFH  CMP   CL,BUFD
 JB EFGH1  JB   ABCD1
 INC BUFK  INC   BUFE
 DEC BUFL  DEC   BUFF
 MOV BUFI,BX  MOV   BUFB,DX
 MOV BUFJ,CH  MOV   BUFC,CL
    EFGH1:    ABCD1:
 ..   ..
 ..   ..
    对照之下可以看出,其所不同的,只是寄存器及缓冲器的分别而已。这种程序的出
现,是由于事先规划不当,未能通盘考虑,头痛医头,脚痛医脚。原可以把寄存器及缓
冲器统一运用,现在木已成舟,想要变更很可能影响大局。
    其次是写作的风格及习惯没有养成,在用指令时,信手拈来,以致两段程序,两种
写法!
    现在唯一的补救方法,是利用间接寻址法,先将两组不同的缓冲器,照相对次序排
列妥当,再改写程序。
    1:ABCD:
    2:  MOV SI,OFFSET BUFA
    3:  MOV DI,OFFSET BUFB
    4:  MOV BX,DX
    5:  MOV CH,CL
    6:  JMP SHORT EFGH1
    7:EFGH:
    8:  MOV SI,OFFSET BUFG
    9:  MOV DI,OFFSET BUFI
   10:EFGH1:
   11:  CMP BX,[SI]
   12:  JB EFGH2
   13:  CMP CH,[SI+2]
   14:  JB EFGH2
   15:  MOV [DI],BX
   16:  MOV [DI+2],CH
   17:  INC BYTE PTR[DI+3]
   18:  DEC BYTE PTR[DI+4]
   19:EFGH2:
   20:  ..
    其缓冲器的相对顺序,如下所示:
   35:BUFA DW 0
   36:BUFD DB 0
   37:BUFB DW 0
   38:BUFC DB 0
   39:BUFE DB 0
   40:BUFF DB 0
   41:..
   65:BUFG DW 0
   66:BUFH DB 0
   67:BUFI DW 0
   68:BUFJ DB 0
   69:BUFK DB 0
   70:BUFL DB 0
    凡对称形式或结构相同的程序,都可以采用这种技巧。
三、虚拟法:
    对来处不同的数据,只要性质相同,都可以采用虚拟的技巧,将各种参数事先设妥,
利用参数统一处理。
    兹有一程序,系供屏幕画图之用,所有功能皆用游标完成之,特以此段处理游标的
程序为例说明如后。
    先虚拟各种功能及缓冲器:
    区段位移:   BBBLKMOV  DB  ?  ;?= 位移值
    十字游标:   BBCROCSR  DB  ?  ;?= 位移值
    视框移动:   BBFRMMOV  DB  ?  ;?= 位移值
    画笔作图:   BBDRW  DB  ?  ;?= 画笔宽
    橡皮擦:   BBDEL  DB  ?  ;?= 橡皮宽
    闪动游标:   BBCSR  DB  ?  ;?= 游标宽
    文字显示:   BBCHRDSP  DB  ?  ;?= 字框值
    游标移动值:   BWMOV  DW  ?  ;依当前功能先      ;设定
    视框上限:   BWTOP  DW  ?  ;?= 设定值
    视框下限:   BWBTM  DW  ?  ;?= 设定值
    视框左限:   BWLFT  DW  ?  ;?= 设定值
    视框右限:   BWRGT  DW  ?  ;?= 设定值
    右界边际值:   BWADDX  DW  ?  ;?= 字或图宽
    ..
    程序入口:   CHKKEY  DW  CK47; 向左上移
     DW  CK48; 向上移
     DW  CK49; 向右上移
     DW  CRET; 无效
     DW  CK4B; 向左移
     DW  CRET; 无效
     DW  CK4D; 向右移
     DW  CRET; 无效
     DW  CK4F; 向左下移
     DW  CK50; 向下移
     DW  CK51; 向右下移
    AX =  输入游标键扫瞄码
    BP =  屏幕X向移动值
    DX =  屏幕Y向移动值
    ES:DI=屏幕记忆区地址
    主流程: ( 已知 AL=0 AH=Scan-Code )
    1:KEYIN:
    2:  SUB AH,47H   ; =HOME
    3:  JB KEYRET   ; 无效
    4:  CMP AH,11   ; >扫瞄码范围
    5:  JA KEYRET
    6:  MOV BL,AH
    7:  SUB BH,BH
    8:  SHL BX,1
    9:  MOV AX,BWMOV  ; 移动值
   10:  JMP CHKKEY[BX]  ; 进入各处理程序
   ..
   40:CK47:
   41:  SUB DX,AX   ; Y 向
   42:CK4B:
   43:  NEG AX   ; 向左为负
   44:CK49:
   45:  ADD BP,AX   ; X 向
   46:  JMP SHORT CKFUN
   47:CK4D:
   48:  SUB DX,AX
   49:  JMP CK49
   50:CK51:
   51:  ADD DX,AX
   52:  JMP CK49
   53:CK48:
   54:  NEG AX   ; 向上为负
   55:CK50:
   56:  ADD DX,AX
   57:CKFUN:    ; 用变量观念检查「窗口」
   58:  CMP DX,BWTOP  ; 超出上限?
   59:  JNS CKFUN1
   60:  MOV DX,BWTOP  ; 上限值
   61:CKFUN1:
   62:  CMP DX,BWBTM  ; 超出下限?
   63:  JBE CKFUN2
   64:  MOV DX,BWBTM  ; 下限值
   65:CKFUN2:
   66:  CMP BP,BWLFT  ; 超出左限?
   67:  JNS CKFUN3
   68:  MOV BP,BWLFT  ; 左限值
   69:CKFUN3:
   70:  PUSH BP
   71:  ADD BP,BWADDX   ; 右限+边际值再比
   72:  CMP BP,BWRGT  ; 超出右限?
   73:  POP BP
   74:  JBE CKFUN4
   75:  MOV BP,BWRGT  ; 右限值
   76:CKFUN4:
   77:  ..
    CKFUN4以下为功能处理,由前面的功能参数决定。
    用这种方法,多种功能可以共享一个入口,程序精简且速度快。
四、桥式法:
    桥式法是利用读写存贮器的特性,将程序中若干指令直接填入,作为临时便桥,以
改变此段程序的功能。
    例如在显示时,希望能提供多种变化,而又不愿减低速度及增加太多的程序。最好
的方法,便是利用桥式法,在同一地址,填入需要的指令。
    桥式法用得好而又灵活时,对程序的效率极有助益。但是应该注意一点,就是只能
用在可读可写的记忆区中,如若要制成「韧体」,即置入仅读存贮器(ROM) 中的程序,
绝不可使用此法。
    下面的实例,即为屏幕显示的桥式应用。首先,把架桥的「材料」设置在缓冲器中,
如:
    CDSPMOD   DB   88H ;8805 = MOV [DI],AL
      DB   30H ;3005 = XOR [DI],AL
      DB   08H ;0805 = OR  [DI],AL
      DB   20H ;2005 = AND [DI],AL
    CDSPMOD 即为缓冲器,其中有四个数据,分别为机器码的相异部份,如分号后所注。
因为四组机器码皆有 05 ,不必再填。=右边部份,即为该机器语言相对应的指令。
    程序部份先设妥功能定义,利用一、所说的变量应用法,依序由0至3先加载暂存
器BX中。根据 BX 值,将所需机器码加载程序中。
   10:CLOD:
   11:  MOV AL,CDSPMOD[BX] ;用BX取预存码
   12:  MOV CS:CDSP2[1],AL ;加载CDSP2+1
   13:CDSP:
   14:  SUB SI,SI  ;数据由0起
   15:CDSP1:
   16:  LODSB   ;取数据
   17:CDSP2 LABEL BYTE  ;加载的位置
   18:  XOR ES:[DI],AL ;原码26 30 05
   19:  INC DI  ;须改 30 部份
   20:  LOOP CDSP1
   21:  RET
五、流水线法:
    工业上的流水线生产作业,需要极为严格的规格限制,原器件分别研制完毕后,统
一送到生产线上组装。
    程序亦可采用同样的方法,只可惜一时手头上找不到现成的、适用的例子,只得将
方法概述如下:
    先设定处理流程,凡是能用这种生产流程者,皆适用。
    再设定处理流程中所采用的「生产线」,也就是缓冲器。因为流水线上所用的数据
都需要由缓冲器提供。
    此缓冲器的长度由流程决定,缓冲器中的数据则由各调用本流程的原程序加载。
    各调用程序可视个别条件,将所需处理的数据,放在缓冲器内(全部或部份)。待
调用后,再从原缓冲器中取出经过处理后的数据。
六、对应表法
    凡是指根据某种需要,将经过整理的数据,以某种固定的格式,安排在一特定区域
中。每当需要时,立刻可以按照排列的位置取出来使用的,皆可称之为对应表。
    这种对应表是我最喜欢利用的技巧,速度奇快不说,修改也极其容易。尤其是我做
事一向不拘小节,写起程式来,专出小错。自从采用了表格对照法后,凡是适合这种形
式的程序,只要想通了最理想的结构,几个指令就把程序写完了。
    兹将附录中所举的例子,对字形放大所采用的查表法,在此作进一步的介绍。
    假设有一组图形,要在屏幕上左右放大一倍。一般程序员做这种题目,都是在寄存
器内移来移去,每一个字符的数据,起码要移八次之多,每次都要用借位作为转换值。
而转换时,又要放进一个16位的寄存器中,尽管可以用回路去做,时间的延误相当大,
读者可参考附录二以做比较。
    当然,表格要占用空间,以本例而言,如果一次用256B,取足则要512B。
    因此这种技术可以说是以空间换取时间。在第一章第三节「效率」的第四条定律下,
我们知道键盘输入速度,决定于人的操作速度,而人的反应远远不及计算机,故应以人
的速度为时间边际值,尽量设法节省。
    目前,所涉及的是显示时间,每个人在计算机前,都期望着立即得到结果。因此,
显示速度不仅要快,而且越快越好。所以,前述的空时交换应在可能范围中,视实际的
边际效应,以作取舍。
    现在看看数据分析,下面列举的二进制数据,在左边为原图形点阵,在右边则为放
大一倍后的点阵:
    原点阵    左右放大一倍
    00000001   00000000 00000011
    00000010   00000000 00001100
    00000011   00000000 00001
    ..
    01010101   00110011 00110011
    ..
    11   11 11
    现在有两个因素非常明显,第一,不论什么点阵,放大后长度加一倍,一字符有25
6 种。放大后点形种类不变,但字符数加倍为 512个。其次,由于放大后的 512个中,
有一半皆相同,故仍可用256 种表示。
    至于取前者或后者,当视情况而定。
    决定以后,将之定义在缓冲器中,以原图形的点阵数据作为索引值,即可采间接寻
址法,立即取得放大后点阵。
    在制作对应表时,应养成良好的习惯,根据数据的规则,以等长度、固定的格式输
入。这样不仅对表中的数据能一目了然,而且容易输入、侦错、修改,一举数得。
    如某表格为:
    100 TBXXX  DB  0,1,3,7,0FH,1FH,3FH,7FH,0FFH,2,6,0EH,1EH,3EH,7EH,0FEH
    此表看去远不如下表来得清楚、规律:
    100 TBXXX DB  000H,001H,003H,007H,00FH,01FH,03FH,07FH
    200    DB  0FFH,002H,006H,00EH,01EH,03EH,07EH,0FEH
    从事程序写作,规律的思考方式及追求,经常事半功倍。这种小技巧看似没有多大
作用。事实上,在输入时,规则化的结构可以轻易地利用现有的功能,或复制,或修改。
更有利的是能一眼看出该表的意义及正确性,在程序侦错时,往往可以节省大量的时间。
七、模式法
    所谓模式法,是指在程序的处理过程中,分析其规律,以期找到一种共同具有的
「模式」。并用此模式,设计成为一个个程序单元,以追求最高效率。
    这种模式,可用「概念」来代表,但最理想的表达方法,仍以视觉图形为宜。也就
是说,最好能把分析出来的模式,用图形表示,并据以理解及设计程序。
    兹以常用的功能「排序」为例,来说明模式法的应用,并设计成为程序。
    先假定需要排序的数据结构为:
    11每笔数据之长度固定为一字符。
    12数据形式为 ASCII码,16进位值,由 20H到 7EH。
    13排序时,数据数值小者排在低位,大者排在高位。
    14程序开始时参数设定为:
     AL= 高位之数据。
     AH= 低位之数据。
     DS:SI=数据存贮处。
    数据由低位开始检查,并同时排序,直到全部查完为止。排序时,交换高位及低位
之数据,以使
    高位住址数据≧低位住址中之数据。
    由于人类行为与视觉息息相关,故最有效的认知方式,是以作图来说明。以下即为
上一陈述之图形说明。
                 │? │
    模式一供检查       ├─┤
    AL,AH 之大小  ┌ AH <--│? │<-- AL ┐
   模        │    ├─┤    │模
   模式二交换数据式 ┤ AL <--│? │<-- AH ├式
    其中 AH>AL  一 │    ├─┤    │二
            └ SI =  │? │ =  SI ┘
                 ├─┤
    由上图可见在模式一中,AH为低位数据,AL为高位数据。比较 AL,AH 之大小,即
可知是否符合序列规定。如符合,则继续做下去,否则依模式二,将小值放进低位,大
值放进高位住址中。程序只要设法保持此一处理之形式,即可简单明了地完成任务。
    1: COMPAR:
    2:  MOV AH,AL   ;设AH为低位值
    3: COMPAR1:
    4:  LODSB  ;取数据
    5:  CMP AL,AH   ;比大、小
    6:  JAE COMPAR   ;高位大,不变
    7:  MOV [SI-2],AX ;交换AH,AL,排序
    8:  DEC SI   ;向低位再查
    9:  MOV AH,[SI-2]
   10:  JMP COMPAR1
    当然,上面这段程序并不成立,因为没有出口,永远做不完。程序的终止有很多方
法,一是用计数器,一是用位置来比较,也有用终止指令的,不一而足,各有长短。
    首先,假设在DS:SI 中,有一长度值,兹以计数器的回路来试试看:
    1:  LODSW
    2:  MOV CX,AX   ;似此,3B 18C
  ;若用 MOV   CX,[SI]
  ; INC   SI
  ; INC   SI
  ;则需 6B,21T
    3:  SUB AL,AL   ;先设最小值,备用
    4: COMPAR:
    5:  MOV AH,AL   ;设AH为低位值
    6: COMPAR1:
    7:  LODSB  ;取数据
    8:  CMP AL,AH   ;比大、小
    9:  JB COMPAR2   ;低位大,需排序
   10:  LOOP COMPAR   ;回路
   11:  RET  ;完成
   12: COMPAR2:
   13:  MOV [SI-2],AX ;交换AH,AL,排序
   14:  DEC SI   ;向低位再查
   15:  MOV AH,[SI-2]
   16:  JMP COMPAR1
    程序中的回路,对前面有一比较分支不太有利,因为回路每次要17T ,比较分支就
是现成的回路,不利用形成浪费。
    若把回路改为位置比较,程序即为:
    1:  MOV CX,SI
    2:  ADD CX,[SI]
    3:  INC SI
    4:  INC SI
    5:  SUB AL,AL   ;先设为最小值,备用
    6: COMPAR:
    7:  MOV AH,AL   ;设AH为低位值
    8: COMPAR1:
    9:  LODSB  ;取数据
   10:  CMP SI,CX   ;比位置到终点?
   11:  JAE COMRET   ;完成
   12:  CMP AL,AH   ;比大、小
   13:  JAE COMPAR   ;高位大,再查
   14:  MOV [SI-2],AX ;交换AH,AL,排序
   15:  DEC SI   ;向低位再查
   16:  MOV AH,[SI-2]
   17:  JMP COMPAR1
   18: COMRET:
   19:  RET
    如此,在分支时,在第13条指令做回路,10,11 则比较住址以决定是否完成。这一
来,完成结束只有一次,需时 16T,其余所有执行时间皆为4T,较前一回路快了13T 之
多。
    再试用「终止指令」法,其必要条件为数据中有多余的组合可供选择。一般多以 0
0H,0FFH 等极端值比较理想,下面且以0FFH作为终止指令,并置于数据终止处。
    1:  MOV CL,0FFH   ;终止检查用
    2:  SUB AL,AL   ;先设为最小值,备用
    3: COMPAR:
    4:  MOV AH,AL   ;设AH为低位值
    5: COMPAR1:
    6:  LODSB  ;取数据
    7:  CMP AL,CL   ;比是否终止指令?
    8:  JAE COMRET   ;完成
    9:  CMP AL,AH   ;比大、小
   10:  JAE COMPAR   ;高位大,再查
   11:  MOV [SI-2],AX ;交换AH,AL,排序
   12:  DEC SI   ;向低位再查
   13:  MOV AH,[SI-2]
   14:  JMP COMPAR1
   15: COMRET:
   16:  RET
    似此,程序较短,其它效果差不多。
    这段程序,在处理速度上,还大有油水。因为已经检查过的数据,因为回路关系,
还会不断地重复检查,是否能够避免这种情况呢?
    事实上,当排序到某住址时,即表示由该住址起,上面已经检查完毕。因此,只要
记录下来,下次再查时,将住址还原即可。
    1:  MOV CL,0FFH   ;终止检查用
    2: COMPAR0:
    3:  SUB AL,AL   ;先设为最小值,备用
    4: COMPAR:
    5:  MOV AH,AL   ;设AH为低位值
    6: COMPAR1:
    7:  LODSB  ;取数据
    8:  CMP AL,CL   ;比是否终止指令?
    9:  JAE COMRET   ;完成
   10:  CMP AL,AH   ;比大、小
   11:  JAE COMPAR   ;高位大,再查
   12:  MOV DI,SI   ;暂时保存
   13: COMPAR2:
   14:  MOV [SI-2],AX ;交换AH,AL,排序
   15:  DEC SI   ;向低位再查
   16:  MOV AH,[SI-2]
   17:  LODSB  ;取排序数据
   18:  CMP AL,AH   ;比是否该排
   19:  JB COMPAR2   ;是
   20:  MOV SI,DI   ;否,将原地址还原
   21:  JMP COMPAR0   ;从头再做
   22: COMRET:
   23:  RET
    总而言之,程序的变化无穷无尽,尤其是用汇编语言制作程序,更是灵活精妙。就
像下围棋一般,往往一两个指令就足以将整个局势扭转过来。
    程序的效率经常决定在回路上,读者千万不要以为一两个时钟脉冲算不了什么。要
知道,汪洋大海,也是由一点一滴的水珠累积而成的。
    这段程序还有不少值得深思的,读者们不妨自行研究吧!想得多了,自然会有生花
妙笔。
    真要作大量的数据排序,还有更有效的方法,也是应用模式分析的原则,先找出数
据的「型」。
    假如以同样性质的数据为例,为了避免数据一一查找,浪费时间。我们不妨研究一
下,是否有可能,直截了当,就把数据依据大小,予以定位,一次排好?
    计算机的好处,就在于数据的规律性,我们理应利用这种优点,来找出其排序的模
式。
    前例曾分为两个模式,一是查找,一是搬移。如果我们把查找改为记录,把搬移改
为安排,则情形就大大的不同了。
    记录时利用间接寻址技巧,每笔取到的数据,皆可视为一个数值,对应于一记录的
缓冲区。如果数据中每笔数据总值大于256 且小于 65536,则可以用二字符记录之。再
若数据为二进制值,可由 0至 255,即设 512个对应单位。
    假定原数据在 DS:SI中,长度在CX中。
    首先,设一记录区为:
    1: RECORD DB 512 DUP (0)
    2:  PUSH SI   ;程序开始
    3: CHECK:
    4:  LODSB  ;取数据,AH永远为0
    5:  MOV BX,AX   ;利用BX间接寻址
    6:  INC WORD PTR RECORD[BX]
    7:  LOOP CHECK
    8: STORE:
    9:  MOV SI,OFFSET RECORD+512;指向最
       ;后记录
   10:  MOV BP,OFFSET RECORD  ;供检查
   11:  POP DI   ;数据贮存处
   12: STORE1:
   13:  CMP SI,BP   ;查是否完毕?
   14:  JE RECEND   ;完成
   15:  DEC SI   ;向上取
   16:  DEC SI
   17:  MOV CX,[SI]   ;取记录值
   18:  JCXZ STORE1   ;无记录,重取
   19:  MOV AX,SI   ;当前之地址
   20:  SUB AX,BP   ;差值
   21:  SHR AX,1   ;原有值
   22: STORE2:
   23:  REP STOSW   ;重新加载
   24:  JMP STORE1   ;继续
   25: RECEND:
   26:  RET
    程序的变化无穷无尽,尤其是用汇编语言写作程序,简直没有止境。只要稍稍用点
心,加一点点变化因素,一个巧妙无比的程序,就会跃然而出。
    写程序的乐趣,就在于心智的投入。学者们不妨试着把这   式再加以改良,其中还
有不少可以下手的地方,养成习惯以后,程序自然就会精简了。
八、预置法
    预置法适用于流程的安排,尤其是在不确定的情况下,有时需要作多项检查,不仅
浪费时间,对空间也不利。
    例如有一段程序,其目的在于处理使用者所选择的流程。由于使用者事先通过界面
程序,选妥各项工作,现在必须依某一顺序执行。
    这是一项难度相当大的工作,要执行固定顺序不难,下面的程序就可以达到目的。
当然,一如既往,我们会尝试着将程序一再改进。最后,我们再来讨论如何能执行使用
者所安排的顺序。
    设子流程有八种,使用者选用时,可令BX值等于子程序的代号。选用方式为「开关
式」,即单数次为开,设定参数,复数次为关,取消设定。
    设定后,因为共有八种程序,可以用八个位来设置所需要执行的旗号。当然,这要
看程序的多少而定,八位正好用一个旗号FLAG:
    1: SETUP:
    2:  CMP BX,MAXVAL ; 最大值检查
    3:  JA SETRET  ; 超过,无效
    4:  SHL BX,1  ; 参数乘2
    5:  JMP SUBTB[BX] ; 各种程序
    6: SUBTB DW SUB1  ; 各种程序
    7:  DW SUB2  ; 程序中设定
    8:  ..   ; FLAG
    9:  DW SUBN
   10: ENTER:
   11:  SHR FLAG,1  ; 检查FLAG
   12:  JNC ENTER1
   13:  CALL SUB1  ; 有设定
   14: ENTER1:
   15:  SHR FLAG,1
   16:  JNC ENTER2
   17:  CALL SUB2
   18: ENTER2:
   19: .. ; 如此连续进行八次
    显然这种做法其笨无比,第十条以后,可用回路取代:
   10: ENTER:
   11:  MOV CX,8
   12:  MOV AL,FLAG  ; 寄存器较有效
   13:  OR AL,AL
   14:  JZ ENTRET  ; 不必做
   15:  SUB BX,BX
   16: LOOP0:
   17:  SHR AL,1
   18:  JNC LOOP1
   19:  PUSH AX
   20:  PUSH BX
   21:  PUSH CX
   22:  CALL SUBTB[BX]
   23:  POP CX
   24:  POP BX
   25:  POP AX
   26: LOOP1:
   27:  INC BX
   28:  INC BX
   29:  LOOP LOOP0
   30: ENTRET:
   31:  RET
    这样好得多了,可是,还能不能再加改进呢?汇编语言的妙处就在于变化无穷,且
看看是否还能变出花样来。
    从设置开始,方式稍微改变一下,旗号的观念是供程序检查用。在应用时,要占用
一个暂存器,而寄存器有限,浪费了可惜。此外,八个不同的子程序,又要占用一个计
数用的暂存器,最好能够省掉。
    因此,设置的重要性就显而易见了,程序的好坏,并非仅仅在于指令的应用。原始
的理念,及程序的规划,经常在程序设计之前已经决定了。
    我们称之为「预置法」,把前述的设置方式改变一下,用一组缓冲区,先定义如下:
  DB 0  ; 计数用
     BUFER DW 8 DUP (0) ; 存程序入口用
  DW 1  ; 终止信号
    然后再设计程序,预置及执行如次:
    1: SETUP:
    2:  CMP BX,MAXVAL ; 最大值检查
    3:  JA SETRET  ; 超过,无效
    4:  SHL BX,1  ; 参数乘2
    5:  ADD BX,SUBTB
    6:  JMP BX  ; 各种程序
    7: SUB3:
    8:  XOR BUUER,BX ; 设为第三组
    9:  JNZ SUB31  ; 开
   10: SUB30:
   11:  DEC BUFER-1  ; 取消
   12:  RET
   13: SUB31: INC BUFER_1  ; 计数
   14:  RET
   15: SUBTB DW SUB1
   16:  DW SUB2
  ..
   21:  DW SUBN
   22: ENTER:
   23:  MOV SI,OFFSET BUFER-1
   24:  LODSB   ; 查是否需要
   25:  OR AL,AL  ; 为0则无
   26:  JZ ENTRET
   27: ENTER1:
   28:  LODSW   ; 取程序数据
   29:  CMP AX,1  ; 查程序入口
   30:  JB ENTER1  ; 0表示不做
   31:  JZ ENTRET  ; 1表示终止
   32:  PUSH SI
   33:  CALL AX  ; 执行
   34:  POP SI
   35:  JMP ENTER1
   36: ENTRET:
   37:  RET
    前一段调用程序需要31个字符,而现在只要21个字符,速度也快得多。不仅如此,
前段程序仅能提供八个子程序,最多用十六位,不过十六个子程序。本程序则不然,只
要预留的缓冲器够,可提供的子程序可以说是无限。
    更重要的功能,是程序执行的顺序。除了这种预置法外,其它的方法,都受限于 S
UBTB的安排次序,无法变更。但本方法则完全可任依使用者的需要,来决定子程序执行
的顺序,以及是否执行。
    请注意在 SETUP时,BX的参数就同时代表了执行的顺序。如果要想依照设定的次序
决定顺序,只要将缓冲区加大,再加一组预设程序即可,如下所示:
    1: SETUP:
    2:  SHL AX,1  ; 输入参数
    3:  ADD AX,OFFSET SUBTB ; 子程序入口
    4:  MOV BX,BUFER-2 ; 位置序数
    5:  SHL BX,1  ; 指向位置
    6:  MOV BUFDER[BX],AX ; 存入缓冲区
    7:  INC WORD PTR BUFER-2; 序数加一
    8:  RET
    这一来,先调用的程序放在前面,后调用的放在后面,使用者只要知道子程序的代
号,就可以随意安排调用。
    甚至于各子程序所需的参数,也可以用类似的方法,预先设置妥当,然后一次取出
运用。
    预置法最宜于「用户」接口,而且作为应用程序,既简单又容易,方便灵活。
    比如有一些应用模块,即可应用此方法,分别归类、编号后,书于手册中,以提供
使用者选择、应用。
    使用者选择界面的方法,可以通过屏幕提示,将各种模块显示在指定位置上。使用
者利用游标,或其它选择方法,以求得到正确的编号,再依序置于缓冲区中。
    各种模块都可能需要输入参数或数据,所以,另外要准备一个参数缓冲区,在选择
模块时,同时选择参数。由于各模块会自动取用参数,故只要置入即可。
    假设有一个「使用者自行设计程序」的工具套件,("聚珍整合系统"就建立
在这观念上,惜因我们人手不足,产品可能要到1991年才能上市。)屏幕提示有界面、
功能、共享等各类模块,使用者选完一类后,屏幕再度提示该模块的编号。
    屏幕上的模块编号经过程序转换,得到程序编号,将此编号存入缓冲区,再查是否
需要输入数据。即可按照原有流程设计,逐步执行下去,完全可以利用这种预置法。
    1: GETMOD:
    2:  SUB AX,AX
    3:  INT 16H  ; 使用者输入
    4:  CALL GETSUB  ; 转换为代号
    5: SETUP:  ; 代号置于AX
    6:  SHL AX,1  ; 次序乘二
    7:  ADD AX,OFFSET SUBTB ; 子程序入口
    8:  MOV BX,BUFER-2
    9:  SHL BX,1
   10:  MOV BUFFER[BX],AX
   11:  INC WORD PTR BUFER-2
   12:  JMP GETVAR  ; 查取参数否
    当然,真正可以应用的程序,还要考虑很多因素,但大致上,结构就这样简单。
    写程序和画画没有两样,多看、多参考别人的程序,多想、多钻研各种方法,最后
则是要多多动手,除此之外,别无其它法门。

上一页    下一页