组合语言之艺术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 才是。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值