组合语言之艺术4

本文以组合语言写作为目的,介绍编程相关知识。将编程与画画类比,阐述程式分析、流程制作等概念。还介绍了程式结构,如主程式、副程式等。同时讲解对资料、暂存器的认识,回路指令、分支处理技巧等,以提高程式效率。

  第三章    基本认识

 第一节  应用工具

一、对程式的认识

    写作程式不难,但要写出好程式却不容易。这就好像画图一样,人人都能画,而画出来的图却可能有天壤之别。
    想作一个好画家,首先要有观察及分析的能力,面对着杂乱的事物,先整理出头绪,找到主题。再在画布上勾出轮廓,这叫做「布局」。布局完毕,根据实际的环境,决定作图的先后「顺序」。顺序是一种层次观念,景物及色彩都有一定的层次,绝不可随意所之,想到哪里,画到哪里。
    观察考虑完毕,即开始准备,先将画笔、调色板等工具放妥,把要表现的主要色彩也调好。最后是选择适当的画笔,蘸上色彩,按照所观察的结果,涂在画布上。
    画图颇重风格,有些个人主义的艺术家,技巧并不精通,只因为时代潮流或历史条件,创造了某种独特的风格,就得以成名享利。一般的画家则不然,不论是「工笔」抑或「写意」画,全靠其技巧及素养,始能求生存。至于艺术大师,则首重风格,再加上素养、技巧,方可扬名立万,永垂不朽。
    最糟糕的画匠,既没有观察能力,更谈不上技巧和风格,除了照着别人的作品抄袭、模仿外,创造不出有价值的作品。若程式师也如此,只能照着别人的意思,填填指令,不过是个程式匠罢了。
    在观察分析之下,把欲表现的内容整理成为具体的步骤,用电脑术语来说,是为「程式分析」,相当于画画中的「布局」。再下去,便是「流程」制作,或是作画的顺序。将各种程式的层次安排妥当,才能开始写作程式,相当于开始作画。
    这些观念牵涉甚广,不是三言两语可以说完。本书仅以组合语言写作的训练为目的。如果读者能善用组合语言的各种技巧,又能充份认识所要完成的工作,至少可以满足「工笔画」的条件。对一个电脑程式而言,目前画「工笔画」的价值要比「写意」为高。
    下面,我们要以工笔画的立场,来理解组合语言的应用。对油画或水彩画而言,色料相当于程式用的「资料」,调色盘就是运用资料的「暂存器」,画笔等于「指令」,一切都准备妥当,所谓「作画」就是「写程式」。
    程式是由一系列的定义和指令组织成的可执行的程序,需由一种档案的形式(.ASM)经过编译程式 (MASM.EXE) 的处理,将原始档转变为目的档(.OBJ),然后再将一个或数个目的档经过联结(LINK.EXE)成为执行档(.EXE),或者再用 EXE2BIN. EXE 制成记忆限在64KB以下的命令档(.COM)。
    程式师应熟悉上述过程中的每一细节,方能顺利完成程式写作。
    程式的写作方式本无定则,完全看需求及应用而定。可是正如一幅画,在布局时,程式师应该先有全部的观念,然后逐步实行。为了提高效率,这些步骤,有必要加以归类。结果就是所谓的模组。
    模组的良窳,决定了程式写作、修改及再应用的效能。在写作时要求理念一贯,连续进行。修改应方便灵活,不致错误丛生。而应用上功能要完整,可以独立调用。
    根据上述条件,程式的结构大致上可分为:
  1,主程式:连贯性的处理过程,应该一次考虑清楚,细节暂    时放在一边,先把大架构写出来,以免顾首不顾尾。在空间足够的情形下,大架构应该是一个完整的模组,且在整体的观念下,统一处理。
        这种做法,对程式侦错及修改有很大的帮助。因为修改和调整最多、对功能影响最大的,必然是主程式。若主程式都在同一模组中,比较容易得到理想的效果。

  2,副程式:副程式都是一些细节的处理,可以用‘CALL’的方式执行。原则上说来,细节的处理经常重覆发生在不同    的情况下,作为副程式相当有利。只是应该注意调用的手    续,为了效率,通常将需要处理的参数或资料,经由暂存器或者必要时用缓冲器载入。
        既然是数个程式均可共用的副程式,而且此类程式为一独立的过程,所以应该事先分别测试,保证无误。
        此外,各副程式的入口处,宜明白的交待暂存器的使用方式,且要能一目了然。

  3,子程式:子程式与副程式有一点不同,就是具备完整的机能。所谓完整的机能,指该段程式可以独立执行、有固定的功能。在应用时,两者没有分别,然而在写作时,子程式的考虑要慎重些。

  4,资料档:资料档也可以视为一种静态的程式,虽然不是执行用的,但却是执行时不可或缺的素材。资料档的设计应该注意空间的利用,等长度的资料结构最具效率,最好保证资料起点为双数,以节省16位元汇流排的执行速度。
        在应用缓冲器时,切忌随意设置,往往程式师们设了一缓冲器,等后来发现没有必要,再想删掉就麻烦大了。所以事先应安排妥当,以便于随时查找和调整。安排的方式视使用的情形而定,有的以模组归类;也可以用字母排序为依据;再不然就加上详细注释说明功能及使用的程式标号。

  5,应用表:在本书第四章第六节将介绍应用表的功能和应用方式,此类表一次设计完成以后,很少需要再修改,为了工作效率,独立成为一个档案,自有其必要性。

    此外,各种程式的命名最好能有代表性,以便于应用;程式不能太大,否则编辑耗时费事;分档时,则要注意标号宣告及各段安排的问题。在磁碟中,应该专辟一个子目录,不要把各种不相干的程式,都混合在一起。
    第二章三、四节中已规定了格式的标准,此处仅再补充一点。即各缓冲器的定义与使用时的长度应相等,否则在编译或联结时,容易发生错误。联结时,有时并无足够的错误讯息,供程式师得知错误产生的原委。最难理解的错误,往往与缓冲器的定义有关,即定义的类型与使用的类型不一致。另外一个情况是段值的改变,其补救方法为在应用时,临时加一「前置定义」。
    所谓「前置定义」是指当暂存器为一字元时,其前应加写BYTE PTR,否则用 WORD PTR 以确定其值,即可保证安全。
    如:MOV  BYTE PTR BBSDOT1,AL
    此外,每当段值有所改变时,都加写一条:
    ASSUME  CS:XXX,DS:YYY,ES:ZZZ
    这种用法,完全是给汇编程式「看」的,程式本身并没有增加任何指令。
    其他规定,请参阅各相关手册。

二、对资料的认识

    在画布上,所有色彩都是由红、蓝、黄三原色及白色调制而成,了解色彩的变化是画家的基本素养。在电脑中,所有的资料则都由二进位数据组成,要写程式,必须对二进位的特性先有深刻的认识。
    绝大部份的程式师,都不知道二进位数据的妙用,充其量能够很快地换算二进位与十进位的数值。再不然,由二进位值领会到图形的点阵排列,如此而已。
    二进位就是开关的观念,把一连串的开关联在一起,其所能发生的作用,完全在于每一个开关、以及各开关组合应用的功能。
    说得明确一点,先要将各种需要设计的功能分析清楚,找出其共通的因素,如果这些因素能用「开」及「关」两个简单的状态代表,则可以用二进位制加以控制。在理论上,一开一关只有两种作用,而两组开关就有222 种作用,最理想的设计  是将开关的排列组合数用到极限。
    举例而言,电脑上应用的彩色,就是最理想的设计之一。在电脑中,最基本的应用单位为「字元」(Byte),每一字元有8个「位元」(Bit),相当于8个「开关」。为了要最精简地应用多种彩色,只以三原色与辉度组合,八个开关就能产生 256种不同的彩色。兹将各开关所代表的彩色分列如下:
    开关一 (bit 1):正蓝色
    开关二 (bit 2):正绿色
    开关三 (bit 3):正红色
    开关四 (bit 4):灰色 (高辉度)
    开关五 (bit 5):黑色 (低辉度)
    开关六 (bit 6):浅蓝色
    开关七 (bit 7):浅红色
    开关八 (bit 8):浅青色
      ★上述 (bit n) 是从 n=1 开始计算。

    应该注意的一点,是电脑的基本单位在于八个开关,不用足就是浪费。如果8个不够,再增加便有16个。所以,因事制宜,在设计的时候,唯有用8的倍数才划算。
    但是,宇宙中的事物,不见得刚好是八的倍数。如果设计的人没有这种认识,不能把所处理的资料,以8为限制条件去划分,就无法利用这种有利的条件,当然,也就得不到最理想的结果。
    所以,要想程式具有最高的效率,首先要把资料整理成为八的倍数值结构。把资料整理为最有效的结构方式,称为「资料结构」,关于这一点,在后面将有较详细的例证。
    每个字元有 256种排列组合,即相当于 256个十进位的数字。为了方便人的理解,通常将字元写成十六进位形式,并在其数字后加一‘H’,以别于十进位数字。
    兹将十进制与十六进制对应表列于下面:
    二进位值    八进位值    十进位值    十六进位值
        0           0           0           0H
        1           1           1           1H
      *10           2           2           2H
       11           3           3           3H
     *100           4           4           4H
      101           5           5           5H
      110           6           6           6H
      111           7           7           7H
    *1000         *10           8           8H
     1001          11           9           9H
     1010          12         *10          0AH
     1011          13          11          0BH
     1100          14          12          0CH
     1101          15          13          0DH
     1110          16          14          0EH
     1111          17          15          0FH
   *10000         *20          16         *10H
      ★  凡前有 *者表示进位。
      ★★二进位数后应加‘B’,八进位后应加‘O’。
    由上可知,十六进制仍沿用十进位数字,只是到了10时,已无现成数字可用,只好借用英文字母。在程式中,汇编程式为了分辨ASCII 字符与十六进制数值,通常规定凡十六进位数值以英文字母开始者,在其字母前加一‘0’。

三、对暂存器的认识

    暂存器 (Register) 相当于调色皿,资料相当于色料。把色料放进调色皿里,为的是要得到预定的效果,暂存器对于资料亦然。
    调色皿有大有小,深度有深有浅,其目的是针对不同的情况,以作有效的处理。暂存器也是一样,应用得好,程式会很精简,容易修改、阅读。否则,想到哪一个就用哪一个,没有原则,没有章法,这种程式委实不敢恭维。
    暂存器的重要性,在于处理方便灵活、速度快,占用空间小。不幸8088 CPU的暂存器很少,用起来总是捉襟见肘,辛苦异常。正因为此,暂存器的善用与否,成为程式效能高低的关键技术。
    有些程式师不愿意精打细算,经常设定一些「缓冲器」,利用缓冲器可以任意定名、便于记忆的优点,竟把珍贵的暂存器,当作各缓冲器间、搬运资料的交通工具,只见资料不停的搬进搬出。虽然程式师省了点事,但运行速度白白浪费了,空间也被糟蹋了。写这样的组合程式,远不如去用高阶语言。
    当然,缓冲器是有必要的,但也只限于「必要」的情况,而且,在程式规划时,就要考虑各种应用的条件,把缓冲器内的值取出后,一次处理完毕。如果不能一次解决或是经常要用到的资料,则设法放在暂存器中。
    实际上,任何程式不可能在一个过程中,同时需要很多特殊的资料。好的程式师能把复杂的工作处理得有条不紊,功力不够的,往往把简单的事情弄得令人难以理解。8088的暂存器的确是不够用,但是却不至于少到要以缓冲器取代的地步。
    工作的好坏、成败,与人的组织能力有绝对的关系,限于篇幅,我们不能多谈。可是,利用暂存器的特性,来处理繁杂的资料,倒也是训练组织力的方法之一。
    首先,我们应该把暂存器视为工具,了解工具的功能、性质,然后要能铭记于心,纯熟地加以运用。
    根据个人的理解,暂存器概分六类:
    1,分段用
      程式段 CODE SEGMENT                :CS
      资料段 DATA SEGMENT                :DS
      堆栈段 STACK SEGMENT               :SS
      特设段 EXTRA SEGMENT               :ES
    2,堆栈用:
      堆栈值 STACK POINTER               :SP
      栈用器 BASE POINTER                :BP
    3,记忆转换用:
      源存器 SOURCE INDEX                :SI
      终存器 DESTINATION INDEX           :DI
    4,一般用:
      累积器 ACCUMULATOR                 :AX
      兼用器 BASE                        :BX
      计数器 COUNTER                     :CX
      资料器 DATA                        :DX
    5,标志用:旗号值 STATUS              :FLAG
    6,指示用:执行值 INSTRUCTION POINTER :IP
    为了便于记忆,我们给暂存器定中文名,其定义为:
    凡分段用者率称「段」,做为各段起始位置指示用,其计值方式为:系统中的绝对地址=(本值×16)+各段定址值
    如:资料段为 1600H,乘16即为16000H。
    如源存器为 1234H,则此源存器在系统中由0算起的地址为:17234H。
    应注意者,各种以「器」定名的暂存器,皆有限用的段,切勿混用。
    凡定名为「值」者,皆为不能用来供程式写作的暂存器。如堆栈值(SP)系指示堆栈所在位置;旗号值(FLAG)表示旗号标志的情况;执行值(IP)则代表程式当前所执行的地址。这些暂存器值并非不能改变,但对技巧尚不够纯熟者,最好保持原值,不要妄动。
    经常使用的「器」有两种,一以16位元为单位,如栈用器、源存器及终存器; 另一种则具有两个分别称「高位」及「低位」、各有8位元,可单独使用,也可合并为16位元的暂存器AX,BX,CX,DX。
    暂存器通常作为容器用,但有些多用为记忆区之定址,以便将其中贮存的资料取出应用。前者称为容器功能,可以作计算、逻辑处理等。后者称为定址功能,系供处理各「器」所定位址的资料用。由于8088 CPU的定址方式,受限于当初不成熟的设计理念,偏偏 IBM独具慧眼,选中了它,所谓城门失火,殃及池鱼,读者不得不多花点功夫,小心应付。
    栈用器(BP)属于堆栈段的记忆位置,系提供给高阶语言结构使用,对组合语言来说,功能不大,但若善于运用,也不无价值。
    源存器(SI)固定指向资料段,将源存器中的资料取出,所指的是取出资料段中的资料。设若
    DS=2000H   SI=1234H,则
SI中的1234H 系指系统中 2000H×16加上位址值 1234H。
    不过,使用者不必去计算,只要知道是由资料段起,位址为1234H 即可。
    终存器(DI)较为复杂,通常它是指向资料段,可是有几个指令涉及大量资料转移,需要由源存器搬到终存器。由于受限于分段的设计,为了便于段间应用,所以特别规定:在这种情况下终存器系指向特设段(ES)。也就是说,只能由资料段移向特设段。程式师可以先设定各段的段暂存器,再作转移。若要在同一段中作资料转移,则应使资料段=特设段。
    一般用的暂存器,都可以分成两个8位元、各命名为高、低位暂存器,如:
    累积器:AX  高位 AH ,低位 AL
    兼用器:BX  高位 BH ,低位 BL
    计数器:CX  高位 CH ,低位 CL
    资料器:DX  高位 DH ,低位 DL
    其中累积器的功能最强,可以做乘、除计算,AH尚有贮存旗号的特殊指令。尤其是从记忆区中取值或将值放进记忆区内时,效率最高,如 LODS , STOSW等。
    由于其功能高,运用灵活,所以宜于打杂,千万不要赋与固定的使命。
    兼用器则有一种重要的特性,它是一般用暂存器中,唯一能自记忆区中读取资料者(XLAT指令除外),所以作为「资料及定址转换」 (后文将专门介绍此一功能) 方便异常。
    计数器常用作「回路」或次数的记录,也有专用的指令,除非不得已,或者计数用得不多,最好保留备用。
    资料器功能最少,最好固定其用途,选择经常需要应用的资料,置放其中,以便发挥时间空间的最高效率。

四、对指令的认识

    指令就是「指挥」、「命令」,用以控制电脑,一步一步地实现程式的计划。
    组合语言的格式为:
    ( 下行中凡标“[ ] ”者,表有些指令可省略 )
    [前置元]   指令   [目的操作元,源始操作元]
  1,「前置元」:以下诸例即为前置元的用法。
    11段名:表后面的操作元应属于此临时前置段。如:
          MOV     AX,CS:BUF1
    12定义:表示其后缓冲器的临时定义。BYTE PTR表示以一个字元定义的资料; WORD PTR表双字元资料。
          不论缓冲器的原定义为何,凡有前置元者,皆以临      时定义为准,如:
          ADD     BYTE PTR BUF1,CL
          前置元除了定义缓冲器长度外,亦可表示距离,
          JMP     SHORT ABCD

  2,指令:
    11使用方法:
      1-1 暂存器到暂存器,但限长度相同者。
          MOV     AH,BL     ; 为字元
          XCHG    AX,BX     ; 为二字元
      1-2 暂存器到缓冲器,或缓冲器到暂存器。
          OR      BUF1,AX   ; BUF1为缓冲器,WORD
          ADD     CL,BYTE PTR BUF1
      1-3 数值与暂存器或缓冲器之间。
          TEST    DI,8000H
          AND     SI,0FFH
          SUB     BYTE PTR BUF1,3
          ★数值绝不可作为「目的」操作元
      1-4 将记忆区的地址放在暂存器中,以传送该地址的内容,或传送变数以便间接调用资料。本法限用于源存器(SI)、终存器(DI)、栈用器(BP)及兼用器(BX)。如:
          MOV     AL,BYTE PTR [DI]
          XOR     [BP],DL
          MOV     AX,[DI][SI]
          MOV     AX,BUF1[DI]
          JMP     LAB1[BX]
      1-5 执行指令本身,不需源始或目的操作元。
          PUSH    CS
          POP     DS
          CALL    ABCD
          JMP     ABCD
          CLI
          STD
          LAHF
          RET
      1-6 执行计数者。
          LOOP    ABCD
          REP     MOVSB
          SAL     DL,CL
          ROR     AX,1
          DEC     BX
      1-7 暂存器专用指令。
          OUT     DX,AL
          MUL     BUF1
          DIV     CX
          STOSB
          LODSW
      1-8 条件执行者。
          JNZ     ABCD
          JA      ABCD
          JCXZ    ABCD
          INT     10H
          IRET
         
    12应用功能可分为下列八项:
      2-1 资料转移:1-1,1-2,1-3,1-4皆有可能。
      2-2 旗号控制:1-5 涉及旗号者。
      2-3 段址处理:1-1,1-2 项可能。
      2-4 数学计算:视指令而定,上述各项皆可。
      2-5 字串处理:1-6,1-7 项功能。
      2-6 控制转换:1-5。
      2-7 条件执行:1-8。
      2-8 中断处理:1-8。

  3,操作元:可分成暂存器、缓冲器及数值(Immediate Data)。其书写方式与习惯的由前到后正好相反,使用时要小心,其余细节请参看有关组合语言手册。

 第二节  工作环境

一、系统空间

    IBM PC的记忆区定址,是采用倒装方式 (Big Endian) ,即定址值系由大到小,不同于 一般由小而大(Little Endian) 的定址常识。
    不论当初如此设计的目的何在,这种与人的习惯相反的观念,给写作组合语言者带来极大的困扰。不仅初学者常莫明其妙,连我个人多年来一直与图形处理为伍,都感到汗颜。每次在处理图形时,一定要将原图画在纸上,对照参详,才能了解是怎么回事。
    举例说,有个图形值在AX中,要写进 DI 所指记忆区位置中,写完以后,AX要向右移一位再继续写,直到CX=0。
    这是一个非常简单,而且经常用到的动作,可是在使用「倒装定址」时,麻烦就来了。
    假设AX值为4567H ,DI指向记忆区2000H ,倒装的放法,是先将AL的值放进2000H 的记忆单位中,再将AH放进2001H 的记忆单位里。如果从由小到大的定址观点来看,这就等于是在2000H 中放了一个十六位元的值6745H 。
    这倒不打紧,因为再从记忆位址2000H 中放回 AX 时,仍然成为4567H 。问题是在作图时,一旦4567H 变成了6745H ,图形就左右颠倒了。补救的方法,是在放进记忆区之前,先将AH及AL交换,放完以后,再重新交换回来。说来不算大事,可是白白浪费了两个指令的时间及空间。对速度极关紧要的画图显示而言,要画几万个点,所累积的时间就不可小观了。
    除此之外,在写程式时,对图形的效应要能掌握,才会有良好的成果,像这样每次转来转去,头都昏了,自然而然就失去了耐性。
    现在,80386 CPU 问世了,且不谈效果,读者可以试想,把32位元的 12345678H转换成 78563412H要多少道手续?
    这种痛苦的手续,也是美国人不愿意用组合语言的理由之一。在高阶语言中,有编译器代劳,问题好像不大。但对效率的要求而言,就得不偿失了。图形功能是当今及未来电脑的主流之一,由于当初设计者没有远见,导致无穷的后患。
    问题尚不止于此,IBM PC/AT 的系统空间,在定址的理论上,可以有 1MB(暂时不必考虑记忆扩充及EMS 等问题),然而真正能提供作为程式执行的空间,却不足 600KB。

    我们且看其系统空间的安排:
  0000H 段 0000H-007FH 计 128字元,为32个基本中断。
  0008H 段 0000H-0380H 计 896字元,供系统管理中断。
  0040H 段 0000H-00FFH 计 256字元,为基本程式资料。
  0054H 段 0000H-9C00H 约 34K字元,DOS 程式占用。
    唯有在 00E1H段-09000H段的前半是使用者可以控制的空间,其后,又被系统占用:
  09000H段由0A000H附近直到0FFFFH,为DOS 所用。
  0A000H段,为 VGA图形显示区。
  0A800H段,为 EGA图形显示区。
  0B000H段,为文字态缓冲区,萤幕处理器6845自动管理。
  0B800H段,为图形态显示区,萤幕处理器6845自动管理。
  0C000H段,至0D000H段,各机种不定,供 EMS扩展记忆。
  0C800H段,为唯读记忆体,其内为硬碟控制程式。
  0E000H段,1MB 的主机此处为 RAM,否则此段不能使用。
  0F000H段,为唯读记忆区,其内为基本输出/入程式。

    由上可知,整个系统的规划不尽理想,尤其受限于8088的CPU 原先错误的设计理念(段暂存器现为定址的16倍,即每进一,相当于地址增加16。在最初,如果不考虑与8080兼容,原可轻易地定为 256或更高倍。)所以,当要扩充记忆容量时,便产生了 EMS这种无可奈何的高科技畸形儿。

二、周边设备

    所谓周边设备,率指须透过系统的输出/入汇流埠(I/O Port),及其管理程式所控制的外部各种设置。
    在此定义下,键盘就是一种周边设备,除此之外,萤幕显示器、印表机、磁碟机等,均属周边设备。显然,程式师必须了解每一种周边设备的性质,否则无法下手。
    由于周边设备种类繁多,且各有其使用规格,可以说毫无技巧可言,故本书不拟一一介绍。要之,把各种设备所定义的规格条件,抄录在记事簿中,以便随时查阅。
    此外,为求程式能有效地应用于各种不同规格的周边设备上,千万不可在应用程式中统一处理,最好定妥各种介面,作为附属程式,由使用者自行设定。
    这样规划的第一个原因,是无人能预知到底未来需要多少种不同的设备,挂一漏万,以后程式增改不易,可能导致功能不足,或程式松散的后果。
    第二个原因在,使用者经常使用的设备是固定不变的,将一些永远用不到的程式放在一起,是无谓地浪费空间。
    第三个原因为技术虽在进步,程式应用观念则难以改变,主导程式与周边设备之介面程式不应纠结在一起。一个没有渣滓、精心雕琢的程式才有永恒的价值。终有一天,当电脑技术成熟时,原应用程式无需改动,仅将处理周边设备的附属程式换成新的即可。
    这就是生命,就是新陈代谢,有了这些认识,才能理解组合语言的精义。

三、系统程式

    在 IBM PC/AT系统中,只有两种系统程式,一是磁碟作业系统程式 (MS-DOS 或 PC-DOS ),负责系统启动、记忆区管理以及部份输出/入处理等工作。此系统程式原贮存在系统磁碟中,开机时才调入系统中,所以容易修改。由最初推出的版本1.0 ,到现在已是4.01,其功能还在不断地改进中。
    另一种为基本中断服务程式(BIOS),贮存在唯读记忆体中,除非机种易动,否则永远不会改变。基本中断程式的主要功能为便利程式师,把所有的周边设备所需要的参数,统一由暂存器代为传输。程式师可按照规定,把正确的值,放到规定的暂存器中,基本中断便会优先执行。
    这两种系统程式,程式师必须熟悉,至少,应知道何种功能要用哪一个中断。
    这两种系统程式,都因瞻前顾后,速度不够理想。因之有些程式师,根本不用这些中断,自行控制输出/入埠。这种做法确实能提高速度,自由控制。而相对的,程式的通用性也减低了。是否值得,设计前应先考虑清楚。
    此外,这两种中断程式有些相互重复之处,如键盘输入及萤幕输出等,经常令人不知如何选用。有人建议用磁碟作业的中断,我则认为该用基本中断。
    因为系统容许程式改变基本中断的入口值,所有利用基本中断的程式,都可修改入口,以增加其应用功能。磁碟作业系统则不然,虽然该程式在磁碟上,且在不断地改进中,但在改进之时,又必须兼顾过去的客户。时间一久,问题就发生了。且改进越大,越显得过去的作业方式落伍,兼容就是保留过去渣滓的代名词。兼容性越高,包袱就越重,空间浪费越大。
    建筑在这种基础上的程式,必须冒种风险:是否有一天,磁碟作业系统会面临运转困难或遭解体的厄运?O/S2的问世已经表明了,此系统的大限业已到来。
    基本中断可以改变,意思是说,除了一部份BIOS空间的浪费无可避免外,在PC系列中,系统中断的观念不会再改变。只要程式师能把握基本中断程式的技巧,则不论未来的系统变化到任何地步,一个具有实用价值的程式,理论上其生命期应该是很长的。

四、配备程式

    配备程式指的是一些非必要的基本程式,只因为特殊需要而调用。通常,它是由某些系统提供,配备给某些程式的。
    配备程式包括各种计算的函数及绘图公式,特殊处理用的LIB.等,在某些情况下,也可以将之视为环境,例如视窗管理MS-DOS WINDOW,记忆扩充装置 EMS等。
    配备程式的产生,证明了电脑软体发展的迂回历程,同时也表示出软体的灵活性。在我个人的观念中,配备程式如果能有一定的设计方式,有统一的规格,很可能在大量的、不断发展下,成为一个个「公用模组」,并可专门提供模组,以供用户应用,使得软件的制作变得轻而易举。
    写作或应用这些程式,别无其他法门,唯有熟记于胸,才能得心应手。

五、公用模组

    模组应是未来电脑软件发展的主流,每一类模组的功能,代表了各行各业的经验及诀窍。使用者无需了解模组的制作技巧,只要知道如何调用,就可以完成工作。
    目前尚无厂商提供「公用模组」,但是随着观念的拓广,一旦有了理论,有人先行一步,这种潮流即将形成。我们即将推出的“聚珍整合模组”,第一阶段尚限于程式师使用,再下一步,当客户直接调用的介面完成后,程式的发展方向又将改弦易辙了。
 第三节  处理对象

一、数据资料

    数据资料率指可以输入、处理及计算的二进位资料,在工作过程中,安全性为第一考虑因素,同时要兼顾精确以及完整性。此类资料一般说来数量都相当大,要妥善规划资料长度,否则存贮空间会成为执行程式时的主要课题。
    写作此类程式时,各种进位制的转换,显示区的定位,计算公式的处理等都应该作为子程式,以便任意调用。
    而真正关键问题却在于:数据的极限是否能够明确得知,在有限的范围中,绝对可以设计一种「结构化」的规格,符合效率的需求。否则也应根据其规则性,配合程式的特性,有效地加以处理。

二、文字资料

    文字资料多为字符态,拼音文字所应该注意的是,字与字间的空间调整,齐头、齐尾、齐中等变化,行末断字的规定,以及字体、字形、字号等。
    中文尚有输入码、内码等处理问题。原则上,如果要考虑中、英文兼容,则应注意萤幕上的字形显示与字码记忆区的位置,应占相同的比例。
    目前,由于英文字、码不分,皆占一字元,萤幕上标准格式为25行80字,即采用所谓「文字状态」。而中文字形至少要有16x 16点阵,且需用图形方式(也有采用文字态,再加特殊硬体者,但成本偏高,有碍中文电脑未来发展)。因此,当采用640x 400或近似规格时,中文字形与英文之比,约为2:1。
    在此基础上,以二字元为中文的内码长度,是最常用的结构。但是随着技术及观念的进步,有些英文系统已在使用二字元码,是则,中文有使用四字元的必要。
    从另一方面来看,大陆所用的「国标码」,系抄自日本五十年代的 JIS CODE -日本工业标准,最多仅能容纳8836个符号,其中「汉字」尚不足八千。而国标码更为精简,收字6763个。中文源自中国,现在却借镜东洋的「工业标准」,且摇身一变,竟成为十余亿人口的「国家标准」,真可谓每下愈况。无独有偶,台湾也有所谓的标准,BIG-5 的13,053字,虽然是国标码的两倍,二者终究是五十与百步之差而已。
    为什么要订定这种「辱及先祖」的文字标准呢?谁又够资格订定中文标准呢?从事电脑工作者不过是些「技术专家」,连电脑这一行所有的技术尚未必精通,更何况隔行如隔山,竟然捞过界,捞到文字界这个相离十万八千里的领域去了。
    文字是人类思想、文化的载具,先贤先圣们殚精竭虑所创造的文字,就是用来传达他们对宇宙、人生的认知。我们后代子孙不肖,不能领悟其微言大义倒也罢了。对电脑技术了解不足,没有能力令电脑应用中文,这也可以理解。但自以为是,依权仗势,妄想偷天换日,仅用少数认识的文字,定为整个国家的「文字标准」,并强制国人接受,这种颟顸就难以令人苟同了。
    在运用中文时,由于各家发展的系统观念不一,有的甚至违法盗袭国外软件,为了兼容起见,必须「削足适履」。原则上,中文内码将第一字元中第八位位元设为一,得以与「美国工业标准码」的ASCII (American Standard Code for Infor-mation Interchange)有别。
    文字资料处理上最重要的工作,是排序的技术问题,国标码仅六千多字,却分为二集,把常用字放在前集,次常用字在后部。但是这种顺序与使用人的观念毫无关连。除了统计这种使用频率的学者专家外,不可能有几个人理解何字是常用字,何字不是。
    于是,当我们要利用电脑的高速效率,将输入的中文加以排序整理时,国标码完全起不了作用。也就是说,编码原为提高效率,而我们的编码只为了编码,与效率毫不相干。
    唯一的补救办法是,再建一个排序表,与国标码一一对照使用。
    高科技界因为利润高、地位高,故而高论、高见特多,只是动起手来就难免「眼高手低」,再不然则是「高论调、低效率」。
    相信人人都有查字典或电话簿的经验,对用英文来说,是轻而易举,中文则麻烦多多。国人只知抱残守缺,自卑自怜,而不求了解其因果原理。一般人如此倒也罢了,高科技界倘如此,就有点说不通,甚至令人怀疑是否别具用心。
    英文所以方便无他,因其字母具有直接索引的功能!中文则有前人订定了一套「部首、笔画」的索引观念。这在过去资讯不发达的时代,的确是个创见,也足敷应用,但是现代与字母的直接索引相较,在效率上究竟差了一大截。
    也有人认为,我们要维护中华文化,就应该死抱着古人所定的索引观念。这种说法只有一点不足,就是忘了把大汉衣冠也穿得整整齐齐,甚至用文房四宝取代现代化事务工具!
    麻烦的是,「部首、笔画」是两种不同的索引观念,当没有时间因素介入时,孰先孰后关系不大。可是用到电脑上,就必须定先后次序,否则碍难执行。
    对姓氏笔画少的人,当然主张「笔画」优先,姓氏部首明显的,则主张先排「部首」。这点不难理解,出席一个重要的庆典,或在报上亮相,人数一多,排名先后所涉及的利益,至关重大,不能不争!问题在于,除了私利外,部首笔画这种没有效率的索引观念,还有什么实用的价值?如果一定要保存,作为一种特例,当然可以。可是电脑所追求的是效率,每个中文的部首和笔画,都需要建对照表,才能应用,字集越大,空间需求越大,时间消耗也越长。
    这还不说,索引不仅是提供给电脑用的,人更需要。仅以查电话本为例,「张伟雄」这个名字,我们凭什么知道其前后的「定位」关系呢?表面上看来,只要查三次部首及其笔画、以及数三次这三个字的笔画。而事实上,在查找的过程中,每遇到一个名字,都要重覆前述的手续,才能加以比较。
    有人振振有词说,有些字一眼看过去就知道是几画!至少我个人没有这种本事。而且根据统计,中文平均以十四画的居多,由九至十八画,就很难靠视觉分辨!再若人名一多,视觉就很容易疲劳。
    又有人说话了,现在是给电脑排序,与人不相干!殊不知字母排序可以立即执行,而部首、笔画排序要多作三至六次动作,兹以先部首后笔画为例:

    1,查本字之部首序值。
    2,查对照字之部首序值。
    3,比较两者之大小,决定是否需要再比。
    4,再比时,查本字笔画数。
    5,查对照字之笔画数。
    6,比较两者之大小,以决定序位。
    把这些步骤写成程式,以中文两个字元的内码计,(意思是说中文收字在两万以下)如果用对照表的方式,空间当在64KB以上,速度则较英文慢约50倍。再若采用公式计算,空间或能节省,但速度将慢上千、百倍之多。
    这还是指两万字以下的情况,若采用汉字全字集,后果将不堪设想。所以「专家」们一致认为,为了效率,字收得越少越好!
    怎样才能算是真正的「中文电脑」?我十多年前所面对的「敌人」,是主张将中文字埋葬掉。这种人不难对付,因为到底他们还是中国人,在民族大义的旗帜下,多多少少心中也存着乐见中文电脑成功的意愿。所不同的,只是他们不相信有此可能罢。
    现今的「敌人」则顽强得多,他们同样喊着民族大义的口号,又是公认的中文电脑「专家」。更可怕的,目前使用中文电脑的人,不见得对中国文化有什么明确的认识,有个工具列印一些文件,就相当知足了。于是,这些客户也在其主观的立场,认定目前这种「市场占有率高」的半调子,就是「中文电脑」的标准!
    是吗?如果中文字有六、七万字,而目前只能用几千、甚至一万多字,那么其他的字呢?算不算是中文,如果算,为什么「中文电脑」中没有?这种电脑能说是「中文电脑」吗?
    有人又说了,没有关系,以后再说。怎么说呢?有一种方法,是将文字「分集」,分成:常用字、次常用字、次次常用字、罕用字、罕罕用字等等。且不管是哪位学者有这么大的学问去「分集」,我所知道的只是用这种方法,人无从记忆,中文排序的难度又一倍一倍地加了上去。也难怪当初有人认为中文不科学,这不是明证吗?
    其实,中文排序根本没有问题,我们利用仓颉字母作为索引,效率与英文相等,而且收字可以高达千万个。至于记忆空间,一个字元都不需要。更有利的是,用作字典、电话簿等的索引,一查即得。
    内码是各个系统、根据其不同的需求、所订的一种资料形式,没有任何理由强制规定。当然,如果内码种类多了,姑不论其编码的好坏,各个中文系统之间,自然会形成难以沟通的障碍。于是有必要建立一种「交换码」,供不同系统的内码,统一交换应用。
    这种交换码才有标准化的必要,而且订定之时,应该谨慎从事,要能容纳所有各家系统所收的字,否则无从交换。
    不论是哪种码,必然会有其特殊规定,在写作之前,程式师一定要设法找到该系统所用的「码表」,否则无法处理。

三、图形资料

    在电脑图形资料的处理方面,目前只有点阵及向量两种形式,前者即二进位资料 (Digital Data) ,后者则是绘图用的公式值。实际上,还有所谓「概念资料」的形式,将视觉效应经过分析后,整理成为人能够理解的「概念」。这种概念资料非常精简,便于贮存,取出后,再通过「概念作图」的过程,还原成为图形。
    一个优秀的画家,必然有这种概念作图的能力,只要把画家的经验写成程式,将其记忆的特徵设计为资料,电脑必将忠实地执行,而且每次都画得一模一样。
    如果是处理二进位点阵资料,不外乎是压缩、还原、截取及综合等几种简单功能。绘图向量值则比较复杂,涉及计算、调整、变数、层次等多种技巧。
    简单地说,绘图资料所考虑的,比文字资料难度高,要想得到理想的效率,最重要的应是资料结构的定义,其次是层次的安排,以及特徵性质的描述等。此外,输入变数处理涉及人的应用方式,除了专业人员外,多数人尚未能适应这种新的绘图观念,经验的不足,以致迄今尚未制作出理想的程式来。
    概念绘图必将成为未来的主流,它不仅符合人类的认知习惯,且易于应用。只要概念资料建得周全、完整,略为改变其中一些概念元素,就能得到各种结果。

四、概念资料

    人类系以概念进行思考,并透过概念来认识外界。所以,对人而言,最有效的应用方式,就是人已经熟知的概念。
    概念并不是语言,而是组成语言的最基本因素。每一个人对外在世界的认知,都是独一无二的,由于人类生存在群体空间里,需要经常彼此交换经验,于是利用听觉效应表达概念,便产生了语言;利用视觉符号,则产生了文字。
    前述的图象概念资料属于「具象」资料,除了具象以外,还有抽象的,包括主观的感受、认知、欲望等等,因与主题无关,这里不加讨论。总之,这些概念资料的结构,在电脑中必然是二进制的形式,只是因每一个设计者观点的不同,性能有所区别罢了。
    直到如今,尚未见到实际应用概念资料的程式,但是它将成为电脑的基本结构,却是指日可待的。
    作为程式师,天天与电脑为伍,不能不知道电脑未来的趋势,更不能不多加努力,掌握技术发展的机先。正因为概念资料尚未定形,人人都有相同的机会,做一个开创时代的先河。否则,等到大局底定时,只有在后面苦苦追赶,由不得己了。

五、综合资料

    功能较强的程式,很少仅具有单一的资料。尤其是「整合软件」越来越受到重视,各种资料最终都将综合在一起。
    综合资料有两种意义,一是人所认识的输入资料,一是电脑贮存的处理资料。
    输入资料又可分指令及字符两种,在传统的观念里,不将指令视为资料,因为指令一旦执行以后,即不再发生作用。可是,在桌上型排版软件广泛流行以后,为了控制版面,必须将相关指令随资料同时贮存起来。而排版已经成为电脑重要的功能之一,所以在未来的发展上,输入资料必须考虑到指令。
    在整合观念中,输入资料应有统一的规定,亦即不论是何种性质的软件,其键盘的应用、字符的定义等,都应该有全面的考虑。
    关于资料内容,也有 ASCII字符及「世界字符」之争,对早期的英文系统而言,其他文字无关紧要,所以没有适当的「世界字码」可供应用。然而,资讯时代究竟不是英文使用国家的专利,在各国觉醒之际,都憬悟到字码的重要。不论 ISO国际组织如何面对问题,我个人不相信世界文字在其保留的、极为有限的「编码平面」上,能够发挥多大的效益。充其量,可供一段时间内、某些商业上的应用而已。
    我认为真正的资讯标准,将是以各国文字为根本的自然语言,而目前最理想的方式,则为多字元的字码方案。拼音文字系统以二字元为宜,除了可以同时应用世界各国文字以外,并且符合当前微电脑的发展趋势。
    在中文系统上,我们采用四字元的「自然码」,即将仓颉输入码压缩的方案。如此,我们可以使用上千万个中文字,有人会说没有人需要那么多字,但事实上有谁能预料呢?当初仓颉造字时,相信不会超过一千,如果他武断地订定「标准」限制后人用字,很难想像我们的民族还会有什么文化?
    台湾曾有专家对我这种意见,表示是「不合乎潮流,注定要失败」,然而到底是谁不合潮流呢?四字元的微电脑已经到来了,而且被公认为今后的主流。在四字元的硬体结构上,自以一次读取四字元、其次为二字元最为有效。所以这些观念已经落伍的专家,还是去捞些钞票,把研究发展的工作,交给够资格的人去做吧!

 第四节  指令应用

    组合语言可以说是未经整理的、原始的电脑语言,读者们大可下一番功夫,找出其应用的规则,以发挥最高的效率。在下面,我仅就个人的经验,提供一些浅见,以供切磋研讨。
    要写好程式,首先应熟记8088指令的时钟脉冲(Clock )及指令长度,一般组合语言手册中,都详列了与各指令相关的资料。「工欲善其事,必先利其器」,此之谓也。
    本节所讨论的,是一般程式师容易忽略的细节,所有的例子都是从我所看过的一些程式中摘录下来的。看来没什么大了不起,可是程式的效率,受到这些小地方的影响很大。更重要的是,任何一个人,只要有「小事不做,小善不为」的习惯,我敢断言,这个人不会有什么大成就!
    我最近才查到 Effective Address (EA) 的时钟值,我觉得没有必要死记。原则上,以暂存器为变数,做间接定址时为5个时钟,用直接定址则为6个;若用了两组变数,则为7至9个,三组则为11或12个。
    为了便于叙述,下面以“T”表「时钟脉冲」; “B”表字元。其中
    时钟脉冲T = 1 / 振汤频率

一、避免浪费速度及空间

    组合语言的效率建立在指令的运用上,如果不用心体会下列指令的有效用法,组合语言的优点就难以发挥。
  1,    CALL    ABCD
        RET
    这种写法,是没有用心的结果,共用了 4B,23T+20T,完全相同的功能,如:
        JMP     ABCD  或
        JMP     SHORT ABCD
    却只要 2-3B,15T。
        此外,上述的CALL XXXX 是调用子程式的格式,在直觉认知上,与JMP XXXX完全不同。对整体设计而言,是不可原谅的错误,侦错的时候,也很难掌握全盘的理念。
        尤其是在精简程式的时候,很可能会遇到 ABCD 这个子程式完全独立,是则把这段程式直接移到 ABCD 前,不仅能节省空间,而且使程式具有连贯性,易读易用。

  2,    MOV     AX,0
    同样,这条指令要 3B,4T,如果用:
        SUB     AX,AX 或
        XOR     AX,AX
    只要 2B,3T, 唯一要注意的是,后者会影响旗号,所以不要用在有旗号判断的指令前面。
        在程式写作中,经常需要将暂存器或缓冲器清为0,有效的方法,是使某暂存器保持为0,以便随时应用。
        因为,MOV [暂存器],[暂存器] 只要 2B,2T, 即使是清缓冲器,也比直接填0为佳。
        只是,如何令暂存器保持0,则要下一番功夫了。
        还有一种情况,就是在一回路中,每次都需要将 AH 清0,此时对速度要求很严,有一个指令 CBW 原为将一 个字元转换为双字元,只需 1B,2T 最有效率。可是应该注意,此时 AL 必须小于 80H,否则 AH 将成为负数。
  3,    ADD     AX,AX
    需要 2B,3T不如用:
        SHL     AX,1
    只要2B,2T。

  4,    MOV     AX,4
    除非这时 AH 必为0,否则,应该用:
        MOV     AL,4
    这样会少一个字元。

  5,    MOV     AL,46H
        MOV     AH,0FFH
    为什么不写成:
        MOV     AX,0FF46H
    不仅省了一个字元,四个时钟,而且少打几个字母!

  6,    CMP     CX,0
    需要 4B,4T, 但若用:
     OR      CX,CX
    完全相同的功能,但只要 2B,3T。再若用:
        JCXZ    XXXX
    则一条指令可以替代两条,时空都省。不幸这条指令限用于CX ,对其他暂器无效。

  7,    SUB     BX,1
    这更不能原谅,4B,4T无端浪费。
        DEC     BX
    现成的指令,1B,2T为何不用?
        如果是
 SUB     BL,1
 也应该考虑此时 BH 的情况,若可以用
  DEC     BX
 取代,且不影响后果,亦不妨用之。

  8,    MOV     AX,[SI]
        INC     SI
        INC     SI
    这该挨骂了,一定是没有记熟指令,全部共4B,21T。
        LODSW
    正是为这个目的设计,却只要 1B,16T。

  9,    MOV     CX,8
        MUL     CX
        写这段程式之时应先养成习惯,每遇到乘、除法,就该打一下算盘。因为它们太浪费时间。8位元的要七十多个时钟,16位元则要一百多。所以若有可能,尽量设法用简单的指令取代。
        SHL     AX,1
        SHL     AX,1
        SHL     AX,1
     原来要 5B,137T,现在只要 6B,6T。如果CX能够动用的话,则写成:
   MOV     CL,3
   SHL     AX,CL
     这样更佳,而且CL之值越大越有利。用CL作为计数专 用暂存器,不仅节省空间,且因指令系在 CPU中执行,速 度也快。
        可是究竟快了多少? 我们做了些测试,以 SHL为例,在10MHZ 频率的机器上,作了3072 ×14270次,所测得时间为:
    指  令 :SHL   AX,CL         SHL   AX,n
          CL = 0 , 23 秒     n = 0 , 无效
   CL = 1 , 27 秒     n = 1 , 14 秒
          CL = 2 , 32 秒     n = 2 , 28 秒
          CL = 3 , 36 秒     n = 3 , 42 秒
          CL = 4 , 40 秒     n = 4 , 56 秒
          CL = 5 , 44 秒     n = 5 , 71 秒
          CL = 6 , 49 秒     n = 6 , 85 秒
          CL = 7 , 54 秒     n = 7 , 99 秒
        由此可知,用CL在大于2时即较分别执行有效。
        此外,亦可利用回路做加减法,但要算算值不值得,且应注意是否有调整余数的需要。

 10,    MOV     WORD PTR BUF1,0
        MOV     WORD PTR BUF2,0
        MOV     WORD PTR BUF3,0
        MOV     BYTE PTR BUF4,0
        ..
        我见过太多这种程式,一见就无名火起! 在程式中,最好经常保留一个暂存器为0,以便应付这种情况。即使没有,也要设法使一暂存器为0,以节省时、空。
        SUB     AX,AX
        MOV     BUF1,AX
        MOV     BUF2,AX
        MOV     BUF3,AX
        MOV     BUF4,AL

     14B,59T取代了 24B,76T,当然值得。只是,还是不 如事先有组织,考虑清楚各个缓冲器间的应用关系。以前面举的例来说,假定各缓冲器内数字,即为其实际位置关系,则可以写成:
      MOV     CX,3  
  如已知 CH 为0,则用:
 MOV     CL,3
        SUB     AX,AX
        MOV     DI,OFFSET BUF1
        REP     STOSW
        STOSB  
    这段程式越长越占便宜,现在用10B,37T,一样划算。

 11,子程式之连续调用:
        CALL    ABCD
        CALL    EFGH
        如果 ABCD,EFGH 都是子程式,且调用的次数甚多,则上述调用的方式就有待商榷了。因为连续两次调用,不仅时间上不划算,空间也浪费。
        若ABCD一定与EFGH连用,应将ABCD放在EFGH之前:
        ABCD:
            ..
        EFGH:
            ..
        像这样,只要调用ABCD就够了,但这种情形多半是程式师的疏忽所致,如两个子程式必需独立使用,而上述连续调用的机会超过两次以上,则应该改为:
        CALL    ABCDEF
        而ABCDEF则应为:
        ABCDEF:
               CALL    ABCD
        EFGH:
            ..
        这样的写法速度不会变慢,而空间的节省则与调用的次数成正比。

 12,常有些程式,当从缓冲器中取资料时,必须将暂存器高位置为0。如:
        SUB     AH,AH
        MOV     AL,BUFFER
     这时应该将 BUFFER 先设为:
        BUFFER  DB  ?,0
     然后用:
        MOV     AX,WORD PTR BUFFER
        如此,不但速度快了,空间也省了。

 13,有时看来多了一个指令,但因为指令的特性,反而更为精简。如:
 OR ES:[DI],BH
 OR ES:[DI+1],BL
    这样需要8B,32T,如果改用下面的指令:
 XCHG BL,BH
 OR ES:[DI],BX
 XCHG BH,BL
    则需7B,28T。

 14,PUSH  及 POP  是保存暂存器原值的指令,都只需一个字元,但却很费时间。
        PUSH  占 15T,POP 占12T,除非不得已,不可随便使用。有时由于子程式说明不清楚,程式师为了安全,又懒得检查,便把暂存器统统堆在堆栈上。尤其是在系统程式或子程式中,经常有到堆栈上堆、取的动作。实际上,花点功夫,把暂存器应用查清楚,就可以增进不少效率。
        要知道,系统程式及某些子程式常常应用,有关速度的效率甚大,如果掉以轻心,就是不负责任!
        保存原值的方法很多,其中较有效率的是放到一些不用的暂存器里。以我的经验,堆栈器用途最少,正好用作临时仓库。但最好的办法,还是把程式中暂存器的应用安排得合情合理,不要浪费,以免堆得太多。
        还有一种方法,是在该子程式中,不用堆栈的手续,但另设一个入口,先将暂存器堆起,再来调用不用堆栈的子程式。这两个不同的入口,可以分别提供给希望快速处理,或需要保留暂存器原值者调用。
        当然,更简单有效的方法,则是说明本段程式中某些暂存器将被破坏,而由调用者自行保存之。

二、程式要条理通顺

  1,在比较判断的过程中,邻近值不必连比。
        CMP     AL,0
        JE      ABCD0
        CMP     AL,1
        JE      ABCD1
        CMP     AL,2
        JE      ABCD2
        ..
    应为:
        CMP     AL,1
        JNE     ABCD0
    ABCD1:
        ..
    在标题为ABCD0 中,再作:
        JA      ABCD2
    这种做法端视时间效益而定,似此 ABCD1之速度最快。

  2,未经慎思的流程:
        ADD     AX,4
    ABCD:
        STOSW
        ADD     AX,4
        ADD     DI,2
        LOOP    ABCD
        ..
    稍稍动点脑筋,就好得多了:
    ABCD:
        ADD     AX,4
        STOSW
        INC     DI
        INC     DI
        LOOP    ABCD
        ..

  3,错误的处理方式:
        MOV     BX,SI
    ABCD:
        MOV     BX,[BX]
        OR      BX,BX
        JZ      ABCD1
        MOV     SI,BX
        JMP     ABCD
    ABCD1:
        LODSW
        ..
    上例应该写成:
        MOV     BX,SI
    ABCD:
        LODSW
        OR      AX,AX
        JZ      ABCD1
        MOV     SI,BX
        JMP     ABCD
    ABCD1:
        ..

  4,错误的流程:
        TEST    AL,20H
        JNZ     ABCD
        CALL    CDEF[BX]
        JMP     SHORT ABCD1
    ABCD:
        CALL    CDEF[BX+2]
    ABCD1:
        ..
 应该写成: 
        TEST    AL,20H
        JZ      ABCD
        INC     BX
        INC     BX
    ABCD:
        CALL    CDEF[BX]
    ABCD1:
        ..

  5,下面是时间的损失:
        PUSH    DI
        MOV     CX,BX
        REP     STOSB
        POP     DI
        PUSH,POP 很费时间,应为:
        MOV     CX,BX
        REP     STOSB
        SUB     DI,BX
        同理,很多时候稍稍想一下,就可省下一些指令:
        PUSH    CX
        REP     MOVSB
        POP     CX
        SUB     DX,CX
    为什么不乾脆些?
        SUB     DX,CX
        REP     MOVSB

  6,有段程式,很有规律,但却极无效率:
    X1:
        TEST    AH,1
        JZ      X2
        MOV     BUF1,BL
    X2:
        TEST    AH,2
        JZ      X3
        MOV     BUF2,DX     ; 凡双数用DX,单数用BL
    X3:
        TEST    AH,4
        JZ      X4
        MOV     BUF3,BL
    X4:
        ..                  ; 以下各段与上述程式相似
    X8:
        ..
        这种金玉其表的程式,最没有实用价值,改的方法应由缓冲器着手,先安排成序列,由小而大如:
        BUF1    DB  ?
        BUF2    DW  ?
        BUF3    DB  ?
        BUF4    DW  ?
        ..
    然后,程式改为:
        MOV     DI,OFFSET BUF1      ; 第一个缓冲器
        MOV     AL,BL
        MOV     CX,4       
    X1:
        SHR     AH,1
        JZ      X2
        STOSB
    X2:
        SHR     AH,1
        JZ      X3
        MOV     [DI],DX
        INC     DI
        INC     DI
    X3:
        LOOP    X1

  7,回路最怕千回百转,不畅不顺,如:
        SUB     AH,AH
    ABCD:
        CMP     AL,BL
        JB      ABCD1
        SUB     AL,BL
        INC     AH
        JMP     ABCD
    ABCD1:
        ..
      以上 ABCD1这个入口是多余的,下面就好得多:
        MOV     AH,-1
    ABCD:
        INC     AH
        SUB     AL,BL
        JA      ABCD
        ADD     AL,BL       ; 还原
        ..

  8,当处理字码时,需要字母的序数,有这样的写法:
        CMP     AL,60H 
        JA      ABCD1
        SUB     AL,40H      ; 大写字母
    ABCD:
        ..
    ABCD1:
        SUB     AL,60H      ; 小写字母
        JMP     ABCD
        要知道字母码的特色在于大写为 40H 至4AH,小写为60H 至6AH ,以上程式,其实只要一个指令就可以了:
        AND     AL,1FH
    简单明了!

  9,大多数的程式在程式师自己测试下很少发生错误,而一旦换一另个人执,就会发现错误百出。
        其原因在于写程式者已经假定了正确的情况,当然不会以明知为错误的方式操作。可是换了一个人,没有先入为主的成见,很可能输入了「不正确」的资料,结果是问题丛生。
        要知道真正的使用者,绝非设计者本人,在操作过程中,按键错误在所难免。这种错误应该在程式中事先加以检查,凡是输入资料有「正确、错误」之别者,错误性资料一定要事先加以排除。
        这样做看起来似乎程式不够精简,可是正确的重要性远在精简之上。一旦发生了错误,再精简的程式也没有使 用价值。
        此外,在程式中常有加、减的运算,这时也应该作正确性检查,否则会发生上述同样的问题。

三、指令应用要灵活

    有一段很简单的程式,其写作的方法甚多,但是指令应用的良窳,会使得程式的效率相去天上地下,难以估计。
    这段程式的用途,是要将一段资料中,英文字符大、小写相互转换。当然,转换的选择要由使用者决定,在下面程式且略去使用介面,假设已得知转换的方式。
    设资料在 DS:SI中,资料长度=CX ,大写转小写时BL=0,反之,则BL=1。
    我见过一种写法,简直无法原谅:
    1: LOOP1:
    2:  CALL CHANGE
    3:  JC LOOP11
    4:  ADD AL,20H
    5:  JMP SHORT LOOP12
    6: LOOP11:
    7:  SUB AL,20H
    8: LOOP12:
    9:  MOV [SI-1],AL
   10:  LOOP LOOP1
   11:  RET
   12: CHANGE:
   13:  LODSB
   14:  OR BL,BL
   15:  JZ CHANGS
   16:  CMP AL,61H
   17:  JB CHARET
   18:  CMP AL,7AH
   19:  JA CHARET
   20:  STC
   21: CHARET:
   22:  RET
   23: CHANGS:
   24:  CMP AL,41H
   25:  JB CHARET
   26:  CMP AL,5AH
   27:  JA CHARET
   28:  CLC
   29:  RET
    这种程式错在把由12到29的程式写得太长,共 25B,有共用的价值,于是作为子程式调用。
    试想一下,每一笔资料,都要调用一次,浪费四个字元事小,但每次要费 23+20个时钟脉冲,资料多时,不啻为天文数字。更何况这段程式写得极差,在回路中,又多浪费了几十个时钟。关于这一点,下面会继续讨论。
    照上面这段程式,略加改进,写法如下:
    1: CHANGE:
    2:  LODSB
    3:  OR BL,BL
    4:  JZ CHANGS
    5:  CMP AL,61H
    6:  JB CHARET
    7:  CMP AL,7AH
    8:  JA CHARET
    9:  SUB AL,20H
   10: CHANG0:
   11:  MOV [SI-1],AL
   12: CHANG1:
   13:  LOOP CHANGE
   14:  RET
   15: CHANGS:
   16:  CMP AL,41H
   17:  JB CHANG1
   18:  CMP AL,5AH
   19:  JA CHANG1
   20:  ADD AL,20H
   21:  JMP CHANG1
    这样的写法还是不佳,因为在回路中,用常数与暂存器比较,速度较暂存器相比为慢。应该先将需要比较的值,放在暂存器DH,DL 中,改进如次:
    1:  MOV AH,20H
    2:  MOV DX,7A61H
    3:  OR BL,BL
    4:  JZ CHANGE
    5:  MOV DX,5A41H
    6: CHANGE:
    7:  LODSB
    8:  CMP AL,DL
    9:  JB CHANG1
   10:  CMP AL,DH
   11:  JA CHANG1
   12:  XOR AL,AH
   13:  MOV [SI-1],AL
   14: CHANG1:
   15:  LOOP CHANGE
   16:  RET
    以上这段程式,空间小,速度快,每笔资料,平均仅需不到40个时钟值,以10 MHZ计,十万笔资料,约需半秒钟!
 请注意程式中所用的技巧,由2至6的分支法,就比下面这种写法为佳:
    1:  OR BL,BL
    2:  JZ CHAN1
    3:  MOV DX,5A41H
    4:   JMP SHORT CHANGE
    5: CHAN1:
    6:  MOV DX,7A61H
    7: CHANGE:
    这种分支也可以由另一种技巧所取代,即预设法。事先将所需用的参数放在固定的缓冲区中,此时取用即可:
        MOV  DX,BWCOM   ; 比较之预设值
    这样程式又简单些了:
    1:    MOV AH,20H
    2:  MOV DX,BWCOM
    3: CHANGE:
    4:  LODSB
    5:  CMP AL,DL
    6:  JB CHANG1
    7:  CMP AL,DH
    8:  JA CHANG1
    9:  XOR AL,AH
   10:  MOV [SI-1],AL
   11: CHANG1:
   12:  LOOP CHANGE
   13:  RET

    以上介绍为变数法技巧,即将所要比较的值,放在暂存器中。由于暂存器快速、节省空间,因此程式效率高。更重要的一点,是程式本身的弹性大,只要应用方式统一,事先把参数设妥,即可共用。

四、回路中的指令

    回路最重要的是速度,因为本段程式,将在计数器的范围之内,连续执行下去。如果不小心浪费了几个时钟值,在回路的累积下,很可能使程式成为牛步。
    要想把回路写好,一定要记清楚每个指令的执行时钟,以便选择效率最高者。同时,要知道哪些指令可以获得相同的处理效果,才能有更多的选择。
    其次,在回路中,最忌讳用缓冲器,不仅占用空间大,处理速度慢,而且不能灵活运用,功能有限。另外也应极力避免常数,尽量设法经由暂存器执行,用得巧妙时,常会将整个程式的效率提高百十倍。
    还有便是少用 PUSH,POP,DIV,MUL和 CALL 等浪费时钟的指令。除此之外,小心、谨慎,深思、熟虑,才是把回路写好的不二法门。
    在前例中,把比较常数的指令换为比较暂存器,便是很好的证明。如果用常数,两段程式决不可能共用,时、空都无谓地浪费了。
    以下再举数例,乍看这似乎有些吹毛求疵,但是仔细计算一下所浪费的时间,可能就笑不出声了。
 兹假定以下回路需处理五万字元的资料,频率为 10MHZ,其情况为:
    1: LOOP1:
    2:          LODSB
    3:  XOR AL,[DI]
    4:  STOSB
    5:  LOOP LOOP1
    本程式计数器等于50,000,每次需
    12T+14T+11T+17T=55T 个时钟脉冲
若以50,000次计,需时 47*50,000/10,000,000 秒,即约四分之一秒。
    只要稍稍将指令调整一下,为:
    1: LOOP1:
    2:          LODSW
    3:  XOR AX,[DI]
    4:  STOSW
    5:  LOOP LOOP1
    这样计数器只要25,000次,每次
 16T+18T+15T+17T=66T
    则25,000次需时 66*25,000/10,000,000 秒,约六分之一秒,比前面的程式快了二分之一。
    同理,在回路中加回路,而每个回路需 17T,也是很大的浪费。倘若加调用 CALL 指令,则需 23T+20T=43T,浪费得更多,读者不可不慎。
    当某一段程式用得很频繁时,理应视作子程式,例如下面的 LODAX:
    1: LOOP1:
    2:  CALL LODAX
    3:  LOOP LOOP1
    4:  RET
    5: LODAX:
    6:  LODSW
    7:  XOR AX,[DI]
    8:  STOSW
    9:  RET
    其实这是贪小失大,仅四个字元的程式,竟用三个字元的调用指令去交换,是绝对得不偿失的。
    再如同下面的程式,颇有值得商榷之处。
    1: LOOP1:
    2:  MOV DX,NUMBER1
    3:  MOV CX,NUMBER2
    4: LOOP2:
    5:  PUSH CX
    6:  MOV CX,DX
    7: LOOP3:
    8:  LODSW
    9:  XOR AX,[DI]
   10:  STOSW
   11:  LOOP LOOP3
   12:  INC  DI
   13:  INC  DI
   14:  POP CX
   15:  LOOP LOOP2
   16:  RET
    第二个回路是多余的,这是高阶语言常用的观念,对组合语言完全不适用。
    稍加改动,不损上面程式原有的条件,得到:
    1: LOOP1:
    2:  MOV DX,NUMBER1
    3: LOOP2:
    4:  MOV CX,NUMBER2
    5: LOOP3:
    6:  LODSW
    7:  XOR AX,[DI]
    8:  STOSW
    9:  LOOP LOOP3
   10:  INC  DI
   11:  INC  DI
   12:  DEC     DX
   13:  JNZ LOOP2
   14:  RET
这样回路少了一个,程式中将5,6,14,15 各条中原来为15T+2T+12T+17T=46T的时间,省为12,13,14条的2T+16T+17T=35T。

            第五节  分支处理

    比较资料后,作条件分支 (Conditional Jump ),是程式中不可避免的手续。程式一长,分支距离超过 128个字元,条件分支就无法到达。当然,精简程式有时可以避免这种情形,但却不尽然。
    处理条件分支的技术很多,其效率端视情况而定。最要紧的是事先规划,要比较些什么?在何种情况下?分支到哪里?做些什么工作等等。
    不仅是写程式,人的各种能力,都可以由工作的方式判断出来。智慧高的人,很快就能抓住重点,再分门别类,钜细无遗的理出完整的系统。经过良好训练的专家,则能根据一套法规,逐步地整理归纳,也能推出合情合理的结果来。
    老实说,电脑程式的写作技术还没有到成熟的阶段,当今所有的从业人员,都只能算是「拓荒者」,并没有真正的「专家学者」。充其量,像我个人一样,比别人机会好些,天天得以与电脑为伍,多一点经验而已。
    因此,目前写程式几乎可以说没有可资遵循的法规,海阔天空,爱怎样写,就怎样写,只要能够使用,程式卖得出去,赚了大钱,就会被人视为大师。
    只是这种情况维持不了多久了,初民的壁画,仅具有历史意义。今天的程式师,如果不认清现实,立刻觉醒,多致力于法规的制定,电脑将永远是个不成熟的孩子。一旦这些法规经得住考验,为未来的专家学者奠定基础,那才能真正的被视为大师。
    我不讳言我们正朝着这个方向努力,但是,我却不认为做得到。因为电脑的硬体设计在今后的十年内,必然会有重大的突破,谁都难以预测会有什么结果。软体的制作观念虽然不可能有很大的改变,却难免会受到影响。只有各位年轻朋友,你们成长在电脑时代,肯多一分耕耘,必有收获!
    下面,且介绍一些我对条件分支的处理技巧:

一、资料的分类

  1,位元分类:
        在本书第四章第五节所举的,由输入码作为输出字形的处理依据之例,就是采用位元分类的例证。
        但凡以资料位元作为共同的分类讯息,而且各类皆有独特的处理方式者,皆应以其位元为顺序,用间接定址或分支技巧,作为程式处理之手段。

  2,字元分类:
        每一个字元具有 256种排列组合,设若有 128种以内的分类项目,应该取双数分类,否则须用连续分类。
        分类之值,立即可以用间接定址执行。但须注意,各分类的入口标题应先行定义。由于定义必须用到双字元,所以,凡采用连续分类者,其值应乘二。

  3,间隔分类:
        在有些情况下,原有资料不容许重新安排,而且其中若干资料已具备分类之特性,这种情况,我们称之为间隔分类。
        在处理此类资料时,应该先将可以作分类处理的资料提取出来,并视为字串,定义在一缓冲区内。当须要类比时,可利用「比对字串」 (SCAS) 的指令以求得其定义位置,再作间接定址。设有
        4700H,4900H,4F00H,5100H,4A2DH,4EABH
    等键盘输入数据。设上述值在AX中,需要作特殊处理,分别进入COD1至COD6等子程式。
    11将资料定义在缓冲器 ABC中,程式则定义在DEF:
      ABC    DW   4700H,4900H,4F00H,5100H,4A2DH,4E2BH
      DEF    DW   COD1,COD2,COD3,COD4,COD5,COD6
    12使DI=ABC,CX=6:
        MOV     DI,OFFSET ABC
        MOV     CX,6
    13由比对字串后,判断是否AX中有上述之值,如有,则用间接定址的方式执行之。
        REPNZ   SCASW               ; 比对六组字串
        JCXZ    NOTHING             ; 没有所比之字串
        SUB     DI,OFFSET ABC+2     ; 得到比对位置值
        CALL    CS:DEF[DI]          ; 或作JMP
          上述之DEF 如果放在DG段中,还可以节省一字元,并可加快速度:
        CALL    DEF[DI]

二、程式的结构

    若在程式规划之初,未先做好准备工作,临时想用前述的方法,并非绝不可能。但是,东添一点,西补一段,这种程式不仅会导致测试的麻烦,更可能影响未来的维护和调整。
    因此,每当了解了工作任务后,需要作间接定址的部份,最好能集中在一个模组内。万一性质不同必须分割,也应该将间接定址的程式,置放在模组的起头处。
    这样做的好处很多,一方面便于扩充功能,每次增加定址因素时,不必在程式中寻来找去,立刻可以安排妥当。其次,这种定址的需求,必然与整体功能有关,而且定义表相当于一个目录,把纲领放在前面,按图索骥,一目了然。更重要的,是可以表现出程式结构的层次,层次处理是网状流程中最难以掌握的一环,不可不慎。
    还有,就是各子程式的标题安排,其位置的先后应以功能的集中性为准。这样做的好处是,如果有可以共用的程式段,很容易就可合并为一,节省空间。

三、次序与条件「真」「假」

    条件分支的「时钟数」有二个可能,条件符合时,执行分支为 16T,不符合则为 4T ,且继续下一指令。两者相差有四倍之多,我们正该利用这一特点,速度重要的条件,都应该设为主流程,否则为分流程。
    尤其是在需要高速的回路中,分支处理得好坏,效率相去甚远。这种分支需要平时多加小心,培养出良好的习惯。
    CDEF:
        CMP    AL,'?'
        JZ     ABCD       ; 各比较符号中,'?' 者最少
        LOOP   CDEF       ; NZ条件仅需4T速度较快
    ABCD:
        ..

四、JMP 与 JMP SHORT

    当程式师专心写作或侦错之时,常无法瞻前顾后。然而侦错完毕程式无误时,最好彻底检查一下所有的JMP 指令,经常会大有斩获!
    因JMP 需要三字元,而JMP SHORT 只要两个,其条件是所跳越的位址不能超过128 字元。
    在程式编译时,若向上JMP 的距离在 128字元以内,编译器会自动译为两字元。往下则不然,如在128 字元内,会再多加一个 NOP指令,不仅浪费一字元且多了两个时钟。
    因此,细心检查一下,凡是向下跳,在128 字元以内,皆应改为JMP SHORT 才是。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值