第三章 基本认识
第一节 应用工具
一、对程式的认识
写作程式不难,但要写出好程式却不容易。这就好像画图一样,人人都能画,而画出来的图却可能有天壤之别。
想作一个好画家,首先要有观察及分析的能力,面对着杂乱的事物,先整理出头绪,找到主题。再在画布上勾出轮廓,这叫做「布局」。布局完毕,根据实际的环境,决定作图的先后「顺序」。顺序是一种层次观念,景物及色彩都有一定的层次,绝不可随意所之,想到哪里,画到哪里。
观察考虑完毕,即开始准备,先将画笔、调色板等工具放妥,把要表现的主要色彩也调好。最后是选择适当的画笔,蘸上色彩,按照所观察的结果,涂在画布上。
画图颇重风格,有些个人主义的艺术家,技巧并不精通,只因为时代潮流或历史条件,创造了某种独特的风格,就得以成名享利。一般的画家则不然,不论是「工笔」抑或「写意」画,全靠其技巧及素养,始能求生存。至于艺术大师,则首重风格,再加上素养、技巧,方可扬名立万,永垂不朽。
最糟糕的画匠,既没有观察能力,更谈不上技巧和风格,除了照着别人的作品抄袭、模仿外,创造不出有价值的作品。若程式师也如此,只能照着别人的意思,填填指令,不过是个程式匠罢了。
在观察分析之下,把欲表现的内容整理成为具体的步骤,用电脑术语来说,是为「程式分析」,相当于画画中的「布局」。再下去,便是「流程」制作,或是作画的顺序。将各种程式的层次安排妥当,才能开始写作程式,相当于开始作画。
这些观念牵涉甚广,不是三言两语可以说完。本书仅以组合语言写作的训练为目的。如果读者能善用组合语言的各种技巧,又能充份认识所要完成的工作,至少可以满足「工笔画」的条件。对一个电脑程式而言,目前画「工笔画」的价值要比「写意」为高。
下面,我们要以工笔画的立场,来理解组合语言的应用。对油画或水彩画而言,色料相当于程式用的「资料」,调色盘就是运用资料的「暂存器」,画笔等于「指令」,一切都准备妥当,所谓「作画」就是「写程式」。
程式是由一系列的定义和指令组织成的可执行的程序,需由一种档案的形式(.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 才是。