CSDN博客

img dot99

组合语言之艺术5

发表于2001/8/4 22:25:00  902人阅读

 第四章   程式设计

    学习任何一种艺术,只能从了解观念及熟练地运用工具、技巧上下功夫。至于个人的成就及表现,有待于不断地自我要求和探索,不是仅仅通过学习就可以轻易获得的。
    组合程式的写作亦然,虽说尚有时、空效率的客观标准,但是一个功能复杂、应用广泛的程式,其价值的全面评估却 非如此单纯。要言之,目前这种组合程式艺术仅在启蒙阶段,尚无成规可循,必须到大众都认识到其价值时,才会有定论。
    因此,我只能在此举一个例子,说明程式写作的观念,希望读者能够举一反三,灵活应用。

 第一节 主题认识

一、任务

    兹假定有一任务,是要设计一些应用符号,其过程由编码到将符号绘制完成为止。这是一个实际且实用的例子,我利用仓颉码定义符号码,并以程式处理之,既精简,又快速。事实上,字形产生器就是利用类似原理设计的,在此特为参考。
    这些应用符号,是用来画表格,要与文字混用,故将各种表格符号,分解成为文字字符,并予以定码。
    首先考虑编码,在此,「编码」就是前面所说的「资料设计」。一种考虑周全的编码,在程式处理上,可以节省大量的时间和空间; 对使用的人而言,循着一种规则,也可达到易记易用效果。
    要达到上述目的,必须先了解一些相关的因素,只要把这些因素纳入考虑范围,且其结果能表现在所编的「码」中,上述的任务就达成了。
    这些相关的因素是:
  1,编码的限制:
 仓颉码取一至五码,限用 24 个字母,此类符号也不   能例外。要能与中文字同时输入,而且不能混淆,故不能 与已存在的中文字码有重码的情况发生。

  2,功能的需求:
 假定要设计四种不同粗细的格子,且每种皆能相互配   合。如:

    ┌ ┬  ┬ ┬  ┬ ┐     ┌  ┬  ┬  ┬  ┬  ┐

    ├ ┼  ┼ ┼  ┼ ┤     ├     ┤

    ├ ┼  ┼ ┼  ┼ ┤     ├     ┤

    ├ ┼  ┼ ┼  ┼ ┤     ├     ┤

    ├ ┼  ┼ ┼  ┼ ┤     ├     ┤

    └ ┴  ┴ ┴  ┴ ┘     └  ┴  ┴  ┴  ┴  ┘
   图  一
    ┌ ┬  ┬ ┬  ┬ ┐     ┌  ┬  ┬  ┬  ┬  ┐

    ├   ┤     ├     ┤

    ├   ┤     ├     ┤

    ├   ┤     ├     ┤

    ├   ┤     ├     ┤

    └ ┴  ┴ ┴  ┴ ┘     └  ┴  ┴  ┴  ┴  ┘

   │  │  │  │      ─  ─  ─  ─
      图 二

二、分析

    以上符号共有 104种形状(后来我发现不敷应用,又加入一些图形)可以细分为四类,分别为:

    横向,左右延伸,上下等距。
    纵向,左右等距,上下延伸。
    横向,或左或右,上下等距。
    纵向,或上或下,左右等距。

    由此可知,我们已经能够定出其规律:
    1,只有纵向横向,皆连接于格子的中央点。
    2,只有四个位置,即上、下、左、右。
    这种归纳方法相当有用,因为我们发现了二进位的影子,正该加以利用。
    先以四个位置来考虑,上下形即为纵向,左右形为横向,是个标准的二进位结构,如果以对角线来看,左、上,右、下又可以组成另一维二进位,可以各用一个位元来表示。我选用了四种粗细,以凑成四个位元。以一字元示意(x 表与该性质无关的位元)如下:

    凡属细点者:      xxxxxxx0
    凡属粗点者:      xxxxxxx1
    凡前述点不加粗:  xxxxx0xx
    凡前述点粗细加倍:xxxxx1xx
    凡属于横向者:    xxxxxx0x
    凡属于纵向者:    xxxxxx1x
    凡属于左上者:    xxxx0xxx
    凡属于右下者:    xxxx1xxx

    以上各值用了四个位元,共有16种组合,若以码代表之,其组合数当视取码数而定。再参考图一,每种因素取一码,横、直、位置共有四种,结论是最多应取四码。
    取四码虽然理想,但仓颉码的设计原本是为了全部六、七万个中文字,而文字的产生为约定俗成,不可能恰好有一连续空余的四码区段,可以安排表格码。
    我在输入码的组合中,好不容易找到一区,即YYX 码后,没有已存在的中文字,故此决定把表格码安排在此区。
    仓颉码最多取五码,YYX 已用去三码,仅余两码可用。
    再看前面的分析,只有16种组合,而符号有24个之多。这种搭配很不理想,需要再加考虑。
    需要取三、四码的,都是要贯穿格子中心的形状,如果再设几个贯穿格子的形状,也当作基本图形,则每个图形仅取两码即可。
    到底我们要多少贯穿形呢?八个!(见图二)八加十六,恰恰好廿四个,完全符合我们前面所强调的精简法则。
    这几种定义,实际上仅需将第五个位元设为一即可:
    凡属半形者:     xxx0xxxx
    凡属贯穿形者:   xxx1xxxx
    最后,这24个码再加 41H就可以得到与文字相等的码值,读者如有兴趣,不妨试着将这些码的值求出,今后应用时就可循着这种思路得出其输入码,而不必死记。
    不过,编码还没有完成,因为在电脑中,一字一代码是相当重要的原则,精确、效率都建立在这个原则上。假如有一代码所代表的形状,与其他代码所代表者相同,就相当于有「重码」字,这种情况应设法避免。此外,为了便于使用,取码也应依一定规则,配合字母顺序,且以符合程式的处理为宜。
    例如 YYXAK与 YYXKA所得形状完全相同,要避免困扰,应规定依字母顺序,只有 YYXAK为有效码。
    又如 YYXAI所得之形,为左侧细横加右侧细横,相当于一条贯穿的细横。而这种横与 YYXU 一码完全雷同。必要时,可以仅以 YYXU 为正确码,将YYXAI, YYXBJ, YYXCK, YYXDL, YYXEM, YYXFN, YYXGO, YYXHP等码建表拦掉。
    注:关于这一点,由于我当年的疏忽,编错了几个,若现在改过来,就会造成过去所建资料不能再用的痛苦,所以从事规划者,千万慎始!
三、改进

    上述的编码方式,是否很理想呢?绝对不是,不仅由于当年的经验不足,考虑的不够周全,已经有了一些错误的编码,而且观念上也有疏忽处。希望读者能进一步的研究出更好的方法,至少,应该能把我个人所发生的缺点加以改进。关于这一点,不妨当作考试题目,在参考本章第五节程式写作时,留心一点,就会发现其中大有文章。
    编码确定以后,就可以写程式了。由于编码时已经把程式的「位元资料」一并考虑,所以立刻分支,利用绘图的程式,轻松愉快地,用几条指令,就完成了任务。

 第二节 系统分析

    所谓系统分析,即为有系统、有规则地去分析「对象」,以电脑术语而言,对象指的是所使用的硬体及软体。
    系统是一系列有组织且统属的整体,从宏观角度来看,系统所涵盖的有:
  1,目的:
 任何一种系统,必然有一个特定的目的,如果不能对其目的先作分析,甚至于不知道目的为何,又怎能发挥系统的功能?

  2,结构:
 系统之组成,是基于其特殊结构,也可以说是组件、部件或或部门,端视其系统特性而定。了解结构,分析结构的性质,再配合各种条件,才能达到目的。

  3,效应:
 效应指预期的结果,系统分析的意义是为了实现,唯有透过对目的的认识,对结构的了解,才能设计出方案。可是这种方案所产生的效应,如不能料于机先,又怎知是否符合理想?
    同时,系统分析还有另一层意义,天下所有的事都是相互的,单一的系统没有分析的可能与必要。比如说分析一部汽车而不理会其行经的路况,不顾使用的条件、油料、配件、人员素养等因素。则我们可以断言,这种分析的结果,除了提供参考外,毫无实用价值。
    在电脑上亦不例外,电脑术语上的系统分析,概指对硬体机种的性质、设备以及使用者的需求范围等的分析、了解和设计而言。在下面我们将对这些一一进行分析。

一、硬体机种

    以 IBM PC/AT作为发展对象,本来不必讨论其硬体机种。只是,站在系统分析的立场,则不能不加以说明,因为这是系统分析必要的步骤之一。
    根据市场分析,国内 IBM及与其兼容的微电脑几乎占 80%以上,中文系统是特为国人发展的,而且是种「附属程式」。以目前的形势看来,可以说是别无他途。
    既然决定了系统,进一步是探讨系统的规格。
    硬体系统的规格,不妨参考随机而来的手册或说明书,资料越齐全,对工作越方便。尤其是「程式师手册」(Programm-er's Manual)更是必备,应事先收集妥当,随时可以取用。
    有了硬体的规格资料,再看哪些与程式本身有关,摘录下来,一条一条地比对,直到全部有了答案,机体的系统分析才算完成。
    兹以一、所述的任务为目标,并假定使用的机种为 IBM的AT 兼容机。
    其相关的规格有:
    系统记忆体   640KB
    硬磁碟   20 MB
    软磁碟   360KB
    萤幕显示卡   Hercules Graphic Card
    键盘   101键,型号 CSK-1101
    操作系统   DOS 3.3 版本
    其他周边无关宏旨,从略。
    此外,为了应用中文,假定采用了“聚珍整合系统”,本系统程式包括所有中文字形及七大应用软件,占 440KB。
    作为一个系统,使用者能支配的空间越大越好,既然要用中文,而中文系统程式又是一种附属程式,难免就会占用部份空间。
    因此,要想使中文的应用功能不弱于原系统,必须非常有效地利用时间及空间。既然已经用了 440KB,千万不要再多事浪费。
    有些中文系统的设计,是把中文字形存贮在硬磁盘中,从表面上看来,空间并没有浪费多少。但是不要忘了,硬盘的读写次数有其极限,如果每个中文字要读取一次,大概不到半年的时间,硬盘就会损坏。不仅如此,这种方法速度很慢,用起来也不方便。
    有些设计为了避免上述的缺点,便以扩充记忆体的办法,先将「常用字」装入扩充区内。这样速度加快了,硬盘的损耗也减低了,可是成本却大大增加。也有人认为,硬体在不断改进,光碟问世后,容量不再是问题,何必多虑?
    软体工程师的责任,是要在自己的技术能力上,发挥最大的边际效益。不论硬体是否能够改进,都应该不辞辛劳,努力将程式写好。一方面当目前硬体尚不够理想时,效率高的软件有立足之地,即使未来硬体改进了,优良的软件仍然占有高效率的优势。
    当今电脑的应用观念尚未充份发挥,似乎光碟的容量大得超过我们的想像。可是,在未来的电脑时代,才真是「寸土寸金」,分秒必争的关头。尤其是一旦电脑成为普遍使用的「大众化产品」后,成本之高低将决定产品的成败。中国本来就贫穷,需要的电脑数量又多,能节省一分钱,对整个市场说来,就是一笔天文数字。更何况硬体技术越进步,电脑的体积越缩小,笔记型、口袋型电脑相继问世,其所面临的储存问题并不是光碟能解决的。
    假如真能因为多花一点钱,一劳永逸地解决了问题,倒也值得,事实上却不然。文字是民族、文化的根本,收字不足将形成「残缺文化」。同时,电脑技术在进步,使用者的需求也跟着增加,私人用的排版软件以及「视窗」的方兴未艾,中文还能梦想徒以增加记忆体的方式去与拼音文字竞争吗?
    那么,有没有其他的办法,用少量的空间,一次解决中文字形的困境呢?
    所谓中文系统分析,就是要针对这类问题,提出正确的答案。最理想的答案,就是用事实来证明,我们已经完成了全部中文字形的字库,共收能见诸字典的「有效字」六万余,且能组合出符合仓颉输入法的字形「六百万」个!不仅如此,每个字的大小、长宽级次不限,(即所谓「无级次放大」)字型、字体也无限!而这些功能,仅用了 260KB的空间,每秒钟可组字 500至1000个!
    我们还在努力改进,几十年的苦工算不了什么,只要我们还有余力,只要能保中国文化源远流长,这一切都是值得的。
    前面所提到的任务,正是这些技术之一,因为其范围小,不涉及文字的专门知识,所以特地摘取作为分析的例证,以供有心人士参考。
    因为我们能用 440KB完成任务,20MB的硬盘就可以完全由使用者支配了。反过来说,如果我们的中文字形要占用20MB硬盘,对客户而言,等于没有硬盘可用。于是,除了改变系统的规格,便得宣告该系统无法设计。
    能不能使用软磁碟来存放字形呢?显然也行不通,软磁碟的读取速度慢,容量也嫌小,一般只用来做资料转换。
    至于 Hercules 图形板,只是显示模组的一小段程式,我们提示在此,是因为这种显示板很适合中文系统。
    键盘是一项经常被系统分析所忽略的组件,我个人过去就如此,这次特别提出,并在下文中详细说明。

二、原则

    由前面任务已知有一种编码,能利用仓颉码特色,加入所有的表格符号,现在要找出一种有效率的设计及应用方式。
    谁都知道在电脑作业过程上,产生功率最直接的步骤是机器码。而组合语言就是机器码的介面,根据效率定律一,组合语言的作业效率最高。
    任务前面已经详细说明,效率的要求非常高,因此,我们决定利用组合语言作为基本工具。
    再看效率定律二,生产成本的效益,取决于工作时间及损耗。如果采用组合语言写作,能不能在时间上有所改进?我们可以用事实证明,只要受过良好的训练,掌握了所有的技术,而且一切工具准备齐全,用组合语言写作所需要的时间,绝对不会比高阶语言多。
    根据效率定律三,要达到应用方便的目标,在于功能的累进。不论将来硬体多进步,人类对其速度及容量的要求永远不会满足。采用组合语言只是第一步,举凡模组、介面等结合的技巧,及「整合」的观念,都有待大家努力去研究、开发。

三、工作性质
    在谈到实际工作前,我们先说明一下中文系统。有人认为此为一系统程式,但我个人认为不如称之为附属程式。附属程式是一种环境,透过这种环境,可以增加中文的功能。因此,这种程式一定要驻留在系统中,而且,要改变原系统程式的中断程式位址。
    所有的附属程式都需要一种「介面」,我们的工作自不例外。此处首先根据工作的性质,将所需的介面列举如下:
    1,初始化系统图形态。
    2,检查系统使用空间,决定是否能够执行。
    3,检查和安排所有必要的参数值。
    4,改变必要的中断入口位址。
    5,申请程式的驻留,并回到系统。
    在中文系统上,改变最大的首推图形字符显示INT 10H和列印INT 17H,其次是键盘处理INT 16H,和利用时序中断 INT 1CH 作为图形游标。这些本属系统功能,加以修改后,成为新的中断入口,在此仍称之为系统程式。
    我们过去曾犯了一个很大的错误,为了想全面控制系统的功能,在改键盘中断程式时,把 INT 9H 也改了。原因在于对系统分析不够彻底,以致对该段程式的了解有了偏差。
    INT 9H是在键盘输入后,每键产生两个字元的讯号,分别称为:「扫瞄码」(Scan Code) 及「字符码」(ASCII Code)。早期的机种,其键数不多,字符码完全符合 ASCII  的规定,即最高位的位元保持为0。
    中文系统之所以能与若干英文软件兼容,就是利用该最高位元做分辨。我们当时误认为字符码不可能再改变,所以在写INT 9H时,该位元并未一一检查。不料新键盘的键数增加了,由于字符码不够,新加的「页控制键」,即键盘右中一列、键色较深的一区,其字符码皆为0E0H,亦即其高位不为0。这一来,在中文系统下,一碰到那些键,就被当作中文,事实上却又非中文码,因而天下大乱。
    但是,在经过IBM BIOS的处理后,INT 16H 所得到的值却不然。那是为了与以往的系统兼容,把高位的位元放到一个缓冲器里去了。如果我们不改 INT 9H ,就不会有任何问题,而改了以后,又没有注意到这种情况,以致产品性能极不稳定,为人所诟病。
    因为程式本身非常简单,介面解决以后,就只是程式的安排了。

四、相关问题

    再来看看我们的工作还有什么应该注意的,除了程式本身的制作外,还要把前述的编码放到“聚珍整合系统”中。该系统的规格,请参考2聚珍整合系统操作手册。
    我们已再三强调,表格之加入,一定要精简,否则喧宾夺主,就不值得了。
    这104 种图形如果用点阵来画,共需 21,632B,竟占了系统空间的百分之廿。这还不说,点阵不能放大缩小,就无法供绘图、排版用。
    于是决定了第一个原则,这些形状必须用程式绘制而不是存点阵资料,而且程式越小越好。
    由于中文系统已经是图形态,且具备多种功能,所以不必再写绘图的程式,只要将输入码转换成该程式可以接受的资料结构即可。
    “聚珍整合系统”内表格的制作,与下面所举的例子差异不大。为便于说明,在此以早期另一中文系统的表格符为例,使用的技术虽不成熟,但较为简洁。
    当时所定的结构,是以暂存器的性质为本,规格如下:
    AX=图形指令,如:横、直、斜、捺、点以及卅多种复杂       的字形
    BX∽X1,X2
    DX∽Y1,Y2
    SI= 指令贮存区起始位址
    DI= 图形点阵贮存区起始位址
    因此,只要资料结构符合,就可以运用原有的子程式。
    第二个原则也因之决定,本程式应纳入原组字程式中,作为其中一个模组。
    第三个原则,是在内码及资料表中,要增加一组表格码的资料。这并不难办到,因为 YYX后没有其他有效的中文字码,所以不会妨碍中文字码的排序。
    凡是涉及编码的作业,千万不可忽略排序的功能。对系统分析来说,排序的功能是资料处理上,有关效率的重要手段。例如有个人口达一百万的都市,这一百万人的各种资料,都需要极为昂贵的电脑来处理。假定排序的功能良好,每天能够准时完成其固定的工作,而且排序的结果能令人一目了然,我们可以认定其效率正常。
    再假设文字资料并不具备排序的功能,或者说其排序的结果,与使用者的观念无法配合,则电脑所处理的资料就等于废物。再若为了要用这些资料,须要用「对照表」的方式,重新排序,那么又要一倍以上的时间,也可以说还要耗费钜资增购一套电脑。
    问题还没有解决,人所能接受的「文字序列」倒底又是什么呢?中文本身并不具备序列观念,这才是中文处理效率不足的,最严重的症结所在! 仅以字典为例,国人查字典时每个字要多久时间?一个「一」字,有多少同音字?如果有一个字,不知其正确读音,又该怎样去查?再问下去,恐怕能回答的人不多了,就是包括大学生以及学有专长的知识份子在内,到底能正确地读出多少音?我不讳言自己的无知,对我而言,只有三千个。
    我承认当年研究仓颉输入码时,采用了拼音字母的排序优点。任何人只要能够正确地输入,就能与拼音文字一般快速地在经过排序的资料中,找到该中文字的位置。
    目前中文电脑的发展还在字码输入,及字形显示中留连徘徊,但是,从事这项工作的人,有几个曾认真地考虑过这些问题?有几个真正了解系统分析的要求?所以,我期望读者们能够不贪急功,不求近利,扎扎实实地先把各种问题考虑清楚,然后再一步一步地研究解决之。

 第三节 模组观念

    当宇宙中还没有人类文明时,是一个整体,循着一定的规律,无休无止地运行。人类的出现,因为意识到自我与非我的分别,就产生了是、非,有、无,善、恶,好、坏的认知。世事的复杂性,即源自于这种分辨心,而忽略了本体的完整及规律性。
    人类对知识的追求,是利用概念作为工具,有系统地对宇宙各种现象分门别类的分析。而分析的结果,可以视为各种模组,人则藉着各式模组,来认知宇宙这个大环境。
    因此模组并不局限于具体的、客观的事物,任何可以经由分析、归纳而认知者,均具有模组结构。而且每每在一些模组的结构上,又有另一层次的模组展开。
    电脑软件的模组,则是一些功能或性质独立的「目标档」(OBJ Files )。此类目标档与介面模组相互联接后,即可执行。为了追求工作效率,将此类模组有系统的予以分类整理,公开上市,不仅可以避免程式的重复制作,且可便利程式师的组合应用。
    「集装货柜」增进了航运的效率,其原因就在于将货物「模组」化,有的以运送目的地、有的以货物性质而定。此外,如「预铸房屋」,甚至于流水线式的生产等,都具有相同的观念。这和程式制作时,为了效率所作的模组分割,有异曲同工之妙。
    软件模组化的要求由来已久,此与工业生产的经验有关,但是迄今很少有具体的成功先例。因为软件的变化无穷,很难建立一种「工业标准」,既没有标准,就难以统一规划。
    因为标准是人制订的,在初,不论如何深思熟虑,不周之处在所难免。但只要有了先例,而且能提供大家参考、应用,集合众人之智慧,总会有完善的一天。
    问题在于,当今有经验的程式师们太忙了,有的忙于自我创造,有的忙于模仿抄袭,有的则忙于享受、玩乐。学校里所教的,全是与现实脱节的理论,而一出校门,由于软体程式师供不应求,炙手可热,立刻就投入了市场的洪流,推波逐澜,还有谁管什么模组!

一、模组定义

  1,空间小、结构完整,能独立调用的程式。
 模组不能太大,否则无法灵活应用,程式的结构又必须独立且完整,以便于分解、组合,供二次开发的程式自由调用。
 有些模组需要与公用模组配合使用,由于公用模组为全部模组设计的基础,故仍可视为独立调用。

  2,效率高、弹性大,便于其他程式调用者。
 二次开发的程式系建立在模组结构上,如果模组的效率不佳,必然会影响其操作。同时,各种应用程式都有特殊的需求和条件,而模组则要考虑通用性,否则很难符合各种条件。所以,在设计模组时,要考虑应用的弹性,使之既能适应各种需求,又能达成特殊的任务。

  3,功能明确,其变化以参数设定实现。
 功能不明确,将会令应用者困惑,也就达不到预期的目的。功能明确与否,除了程式本身外,手册及说明也占了极大的比重。
 所谓功能明确,并非指明确的单一功能,而是指功能的分类。在分类时,需要有一种容易分辨的方式,而最简单的,便是设置参数,凭参数作分支的条件。

  4,程式之间必须利用暂存器传送资料者。
 基于程式、模组之间相互独立,各自应用的缓冲器无法统筹运用。故在执行时,必须利用暂存器以传资料或参数,完成彼此的沟通。

二、模组种类

    将模组分类,为的是便于说明,以下的分类法,并非一成不变。原则上,模组可分为:
  1,功能模组:以所执行的功能加以分割者。
 此类模组要考虑应用上的方便及功能的完整。然而,所谓功能的完整,只限于独立、单一的目标,绝不可将多个不同的功能,设计在一个难以分割的模组中。

  2,公共模组:没有明确的功能,但具有共用的效益,或其他某种特殊的目的,也可以设计成为模组。
 这种模组经常附属于其他模组中,设计时应注意其调用的灵活性,并应专设一目录,详细记载其用途,以便随时查寻。

  3,介面模组:介于两「面」之间,以解决两者问题之模组。一般在程式中,凡属资料与资料间、程式与程式间、硬体与硬体间者,皆为介面关系,处理这种关系的模组即为介面模组。

  4,应用模组:应用者为人,工作者为程式,故提供给应用者操作的模组,概称应用模组。
 此种模组在实质上,仍属一种介面,但因为其实用价值关系到一个程式的成败,必须独立考虑。
 这种模组变化最大,随时有必要根据使用者的需求修改增减,故设计时要特别留意。

三、模组分割的基本原理

    中国文化之博大精深,可以由古人的思想略窥一二,许多人自以为学了一点西方科学技术的皮毛,就像夜郎一样了不得了。其实,除了知识在不断累积外,从古至今,人的智慧并没有丝毫增长。
    模组是一种极有价值的观念,任何事物的形体、结构、步骤,甚至于概念、认知等,都可在以某个目标为前提之下,分析成为若干模组。有了模组,范围就会缩小、问题也就变得单纯,比较容易掌握。如果能有效地利用模组,以之作为解决问题的手段,将是无往而不利。
    庄子早见于此,曾经以寓言的方式,在其内篇《养生主》中,就曾对模组的分割,作了透彻的剖析介绍。时到今日,科学昌明之际,读来仍有「振聋起聩」,一新耳目之慨。
    为此,特将原文抄录于下,再作浅释。

 养生主第三

    吾生也有涯,而知也无涯。以有涯随无涯,殆已。已而为知者,殆而已矣。为善无近名,为恶无近刑。缘督以为经。可以保身,可以全生,可以养亲,可以尽年。
    庖丁为文惠君解牛,手之所触,肩之所倚,足之所履,膝之所□。砉然向然,奏刀□然,莫不中音,合于桑林之舞,乃中经首之会。
    文惠君曰:『焦,善哉,技盖至此乎?』
    庖丁释刀对曰:『臣之所好者道也,进乎技矣。始臣之解牛之时,所见无非牛者。三年之后,未尝见全牛也。方今之时,臣以神遇,而不以目视。官知止而神欲行,依乎天理。批大却,导大□,因其固然。技经肯綮之未尝,而况大轱乎?良庖岁更刀,割也;族庖月更刀,折也。今臣之刀十九年矣,所解数千牛矣,而刀刃若新发于硎。彼节者有闲,而刀刃者无厚;以无厚入有闲,恢恢乎其游刃必有余地矣,是以十九年而刀刃若新发于硎。虽然,每至于族,吾见其难为,怵然为戒。视为止,行为迟。动刀甚微,□然已解,如土委地。提刀而立,为之四顾,为之踌躇满志,善刀而藏之。』
    文惠君曰:『善哉!吾闻庖丁之言,得养生焉。』

  1,前文释义
 人的生命有限,而知识无尽,以有限之生命追求无尽的知识,是自陷于绝地。既已自陷,还妄想藉助于知识,以求自救,当然更是匪夷所思了。
 其实,只要了解事物的道理,以及各种问题的症结。不要执著于表象,不要迷惑于现状,顺理而行。如是,不仅可以安身立命,且能功成名就。
 梁惠王有一厨师,宰牛时,姿态潇洒,动作俐落,颇有艺术家的风采。梁惠王见了,赞不绝口:
 『真了不起!你的刀法神奇到这个地步!』
 厨师听了,忙把刀放下,向梁惠王禀告说:
 『臣子我一向重视观念,并不在意小技巧。最初,臣宰牛时,只看到牛的全身。又过了三年,每宰牛时,所看到的只是牛的结构。现在,臣已能心领神会,按照自然的原则,不论是剖肉、卸骨,刀尖只要顺着关键的间隙轻轻一挑,其组织立即迎刃而解。
 好的厨师每年换刀,这是因为切割过多,难免有所损耗;技术较差的,经常用力砍,刀锋易折,每月都需要换一次。而臣所用之刀,已经有十九年了,宰牛不下千头,而刀刃锋利如昔。
 其实,道理很简单,牛体是由很多不同的组织组成,其间必然有衔接的空间。因为刀锋很薄,在组织间隙中移动,轻轻松松,甚至还有多余的地方。
 当然,也有碰到棘手时。臣就会全神贯注,先找到问题所在,小心谨慎地处理。一旦刀锋稍动,刹时之间,组织分离,大功告成。
 那种成就之后欣慰的感受,简直难以比拟。最后,为了下次的工作,把刀擦拭乾净,好好保护收存。』
 梁惠王听了,道:『好极了,听你一席话,领悟到不少人生的大道理。』

  2,主旨精要
 人生是有限的,为了有效的利用精力,以解决一切问题,应该学习掌握事物的原理。原理之一,即为事物皆具有模组的结构,模组是事物组成之基础。有能力的人,一定善于分析事理,了解事物的结构基础,得以从容不迫,在面对问题时,找到有问题的模组,在其症结上下刀,问题便会迎刃而解。

四、模组分割

    既然称为模组,本不存在分割与否的问题。如果一个程式师一开始就具备模组的观念,彻底了解其性质,当然知道如何设计,自然就没有分割的必要。即令如此,在程式的制作过程中,经常是信马游缰,想到哪,写到哪。所以,养成模组分割的观念,对实际工作上,自有其必然的功效。
    模组分割的原因不定,大约可分下列数种:
  1,为了制作的方便,把程式分成模组,易于维护。
  2,为了工作效率,或需要速度、或为节省空间的程式,分别用不同的技巧制作。
  3,因程式师的工作能力,或工作条件而进行分割。
  4,因程式的功能分类,分开制作,以便于灵活应用。
    不论原因为何,分割模组前,一定要先确定目标,全面认知程式的格局,再加以整理、归类,才能根据类别来分割。
    分类确定后,再以分类来命名,以便于记忆及应用。这时格式的统一定义便是关键所在。因为模组一多,相互间的调用极为频繁,如果格式不能统一,程式间常常需要因应调整,反而增加了使用上的难度,得不偿失。
    其次,各模组必须建立一个总表,而且要经常维护,务必与实际上所使用者一一对应,切勿偷懒。每次调用时,还需修正记录,如使用次数,调用的程式等。
    如果程式制作的规模大,参与人数多,最理想是由专人负责模组管理。从事这种工作的人,必须头脑清楚,反应敏锐,而且要能任劳任怨,勤于更新。
    在做模组分割前,应先备妥 .REF 及 .LST 档,设有一程式 ABCD.ASM ,其做法为:
    C:>M ABCD,,,;
    M 为汇编程式,即 MASM.EXE 之简化名,请参见第二章第二节。其后之“,,,;”表示需要 .OBJ,.LST和 .CRF三个档,而且不必显示在萤幕上。
    汇编完成后,再用 CREF.EXE 生成 ABCD.REF 如下:
    C:>CREF ABCD;
    这时,将 ABCD.LST 及 ABCD.REF 印出,相互对照,先将程式精简、整理,再行分割。
  1,在 ABCD.REF 档中,每个「标题」都来自 ABCD.LST 档,标题之后,有若干组数字,其中带#者,表示标题出处,余者为调用之处。若仅有来处,而无调用处,则此标题可删除。再如标题前之指令为 JMP 或 RET ,则此标题所代表的程式毫无用处,亦应删除之。

  2,如 ABCD.LST 档中有 NOP指令,除非是有意安排者,否则亦应删除。如在 JMP XXXX 之后,可将之改为 JMP SHORT    XXXX 。
 若 XXXX 之前无连接的程式,亦可将之合并。此外,凡指令 CALL 所调用的子程式,如仅调用一次,最好将此段子程式合并在程式内,或附属于其后,以便于分辨。

  3,程式精简及合并后,再行检查各段程式,凡功能独立者,应先分割。分割后,再检查其中是否调用其他子程式,如有,应先记录下来,此段程式即可视为「功能模组」。

  4,凡前述功能模组中调用的子程式,如仅供该模组调用,则可附在该模组后,否则应置于「共用模组」中。

  5,但凡程式之「主流程」,大多属「应用模组」。此类应用模组极难分割,也无分割的必要。

  6,分割完毕后,应详细注记,以便备用。

五、模组特性

    所谓模组特性,是指各个模组在设计或分割之初所考虑的因素。特性包括了功能,效率的特别要求以及使用时应行注意的重点等有关模组的细节。
    不论模组设计的目标为何,既然有了模组,就应该高度发挥其应有的效率。各个模组之分割,皆有其必然因素,不论是为了功能,或是为了制作方便,目的都在追求效率。而每个模组皆有其特性,要达到效率要求,应先了解模组的特性。
    下面以前节所举的中文系统为例,将其中各个模组的特性一一详细分析如下:
    【模组一】:中文处理的系统模组-功能模组
      1,键盘中断:因为人输入的速度远逊于电脑处理速度,所以,本中断程式应该以节省空间为主。此外,键盘的应用,以灵活达变为重点,宜多采用「建表」法,便于修改、变更。

      2,显示中断:显示速度非常重要,本模组应牺牲空间以争取时效。此外,显示的弹性要大,凡字符的大小、位置、属性等,都应该一次考虑清楚。
     目前显示器的规格很多,彼此互不相容,一般多在程式载入前,先作安排。在本模组中,尤其应该注意不同的显示程式,如何兼顾速度的运行。

      3,列印中断:列印也面临不同的机种介面问题,但因不涉及速度,其技巧完全属于设定及载入方面。

      4,通讯中断:通讯所需考虑的,是如何保证在资料传输过程中的安全、正确及保密。

    【模组二】:中文内码模组-辅助模组
   因为仓颉码可以组出既有的,以及排列组合上可能产生的中文字,照理应该不受内码的限制。但是仓颉输入码之长度不定,为了电脑的处理效率,我们采用了四字元内码,每一字元的最高位元设置为1 ,以与英文字符有别。
   内码的转换也应考虑速度,而且要能双向转换,程式制作不难,但处理的技巧也不低。

    【模组三】:中文字形结构模组-介面模组
   贮存中文基本字形结构,根据输入的仓颉码,得到文字结构的基本讯息,以之组成字形。
   中文字数极多,字形结构的规划是成败的关键,只要每个字形多加几个字元,空间就会恶性膨胀。
   本模组采用多层结构的技巧,其中子模组甚多,空间及速度兼顾,才能在极小的空间中,完成大量字形输输出,而且变化灵活,完全拜模组分割之助。

    【模组四】:字形分析模组-辅助模组
   举凡字形大小、种类、笔形的变化及位置等,皆在本模组中完成。
   字形分析以变化多为目的,故本模组主要功能在处理变数,应该妥善安排各缓冲器,以达到效率要求。

    【模组五】:英文字形模组-功能模组
   英文字形与中文有异,所以另成一模组。其字数较中文为少,但却不具备任意组合的功能。

    【模组六】:绘图模组-功能模组
   这段程式是字形产生器的核心,当得到模组四、五的资料以后,要以高速将字形绘出。
   绘图程式的关键在计算,计算虽然是电脑的原始功能,如果不另外加上「辅助计算器」,电脑的运算效率就大为降低。
   一般说来,仅在萤幕上绘图,或做字形绘制时,其范围有限,且可以预知,故可采用「对照表」或快速运算法,换取显示效率。在列印输出时,则可采高精度运算方式。

    【模组七】:各种介面-应用模组
   介面即为介于两个模组,或两个独立的系统间的机构,在大型模组工程中,介面之良窳,是使用效率的关键所在。
   介面是各个模组得以顺利配合的重要程式,只要了解介面的结构,便可以轻易地与原程式沟通。

六、模组调用

    模组的先决条件是要能灵活调用,否则就失去了其设计的意义。而且模组的目的之一,是为了提供给其他程式师,作二次开发用。所以调用的方法,至关重要,必须面面俱到。
    调用的方法很多,为了兼顾多方面的效应,我主张利用系统程式的中断处理。这样做的好处是,系统设计者不必考虑模组的设置,应用的程式师也无须担心入口的位置,直接以参数调用即可。更有利的一点,则是可以在空间不足时,利用覆盖的技术,灵活调用贮存在磁碟中的其他模组。
    「中断」是系统所提供的公用介面,其唯一的缺点是执行效率较差,但应用的程式师可以将中断所提供的位址,移到自己的缓冲区内,代之以长距离的调用,即可改进之。
    以下且以中文字形产生器应用介面为例,说明模组调用方法。本字形产生器提供见诸字典、文献之有效汉字六万余,仓颉输入法所可能组合产生的「新字码」约近六百万字,在本字形产生器中皆有相应之字形。
    除了字数外,字级由1*1 至 128*128,无级次变化。字体在目前仅提供明体、黑体、圆体及长、扁宋等,其横直粗细比值由1至8点,上限为直粗的十二分之一。字形变化有空心、斜体及十种填花体、横向粗细变化体等。
    至于繁体、简体字形或 ASCII字符,也可由使用者自选。
    ┌────┬──────────────────┬─────────┐
    │功    能│    参     数     说     明     │   备   注 │
    ├────┼──────────────────┼─────────┤
    │BX=0    │        │   │
    │ 取字形│DL=n, n 为字形点阵左端字元的起点位置│n=0-7,使用者自定 │
    │       │    *  *     *     │   │
    │       │1,中文状态: 置五个仓颉序码     │序码= 仓颉码 A至Y │
    │       │    DS:[SI],[SI+1],[SI+2],[SI+3],   │     即序位 1至25│
    │       │       [SI+4]       │[SI-1]=0  │
    │       │    例:[01,00,00,00,00] 为'日'      │00表示无输入码    │
    │       │       [14,07,08,01,06] 为'鹳'      │                  │
    │       │    *  *     *     │   │
    │       │2,英文状态:       │   │
    │       │    [SI]=ASCII       │   │
    │       │   20H= 英文为中文之半     │半形  │
    │       │   7FH= 英文同中文大小     │全形  │
    │       │   2EH= 连续虚点      │用做删节号 │
    │       │    例:[SI]=41H 为'A'               │                  │
    │       │    *  *     *     │   │
    │       │3,特殊状态:       │   │
    │       │    [SI]=ASCII       │ASCII专用  │
    │       │    [SI-1]=指令       │   │
    │       │   11H= 直向排列 (转90度)   │   │
    │       │   40H= 外加圆圈, 如1123    │最多为 3个字符 │
    │       │   C0H= 外加圆圈且反白,2AB   │   │
    ├────┼──────────────────┼─────────┤
    │BX=1    │无        │   │
    │ 繁体字│        │   │
    ├────┼──────────────────┼─────────┤
    │BX=2    │无        │   │
    │ 简体字│        │   │
    └────┴──────────────────┴─────────┘

    ┌────┬──────────────────┬─────────┐
    │功    能│    参     数     说     明     │   备   注 │
    ├────┼──────────────────┼─────────┤
    │BX=3    │        │   │
    │ 定义  │CL=0 单线体 直粗一点     │供横向19点以下用 │
    │       │CL=1 细明 直粗两点     │   │
    │       │CL=2 轻明 直粗三点     │   │
    │       │CL=3 中明 直粗四点     │   │
    │       │CL=4 粗明 直粗六点     │   │
    │       │CL=5 重明 直粗八点     │   │
    │       │    *  *     *     │   │
    │       │CL=129 细黑 横直粗二点     │   │
    │       │CL=130 轻黑 横直粗三点     │   │
    │       │CL=131 中黑 横直粗四点     │   │
    │       │CL=132 粗黑 横直粗六点     │   │
    │       │CL=133 重黑 横直粗八点     │   │
    │       │    *  *     *     │   │
    │       │CL=149 中圆 横直粗四点     │限用于48点以上 │
    │       │CL=150 粗圆 横直粗六点     │同上  │
    │       │CL=151 重圆 横直粗八点     │同上  │
    │       │    *  *     *     │   │
    │       │DH=△ X        │即X2-X1  │
    │       │DL=△ Y        │即Y2-Y1  │
    ├────┼──────────────────┼─────────┤
    │BX=4    │        │   │
    │ 空心体│DL=0 取消空心体      │   │
    │       │  =1 设定空心体      │   │
    ├────┼──────────────────┼─────────┤
    │BX=5    │        │   │
    │ 斜体  │DL=0 取消斜体      │   │
    │       │  =1 设定斜体      │横向右斜纵向的1/8 │
    └────┴──────────────────┴─────────┘
    ┌────┬──────────────────┬─────────┐
    │功    能│    参     数     说     明     │   备   注 │
    ├────┼──────────────────┼─────────┤
    │BX=6    │        │   │
    │ 填花体│DL=0 取消填花体      │   │
    │       │  =1 设定填花体,其中     │   │
    │       │  DH=0 全黑,与空心体共用则增大一点│   │
    │       │    =1 粗点形       │   │
    │       │    =2 细网形       │   │
    │       │    =3 疏斜线向右      │   │
    │       │    =4 密斜线向左      │   │
    │       │    =5 残碑形       │   │
    │       │    =6 水泡形       │   │
    │       │    =7 直柱形       │   │
    │       │    =8 梅花形       │   │
    │       │    =9 龙纹形       │   │
    ├────┼──────────────────┼─────────┤
    │BX=7    │        │   │
    │ 加宽形│每次直加粗一点,至8 点为止     │   │
    ├────┼──────────────────┼─────────┤
    │BX=8    │        │   │
    │ 减细形│每次直减细一点,至1 点为止     │   │
    ├────┼──────────────────┼─────────┤
    │BX=9    │        │   │
    │ 加厚形│每次横加厚一点,但不超过8 点     │   │
    ├────┼──────────────────┼─────────┤
    │BX=10   │        │   │
    │ 减薄形│每次横减一点,最细为1      │   │
    └────┴──────────────────┴─────────┘

    第二种方法,则是提供模组的目标码 (.OBJ ),由应用程式师自行联接成执行档。这种做法,每个程式相互独立,兼容性不高,通用性也不强,如果同时想应用多个程式,则需要极大的系统空间。
    小型且专用性极强的应用程式,适用这种方法。但模组一旦与应用程式联接后,就很难再灵活应用。

 第四节 程式规划

    程式规划是指在整体设计的观念上,事先对全部程式周详地、有系统地分析,再定出一个明晰的架构,以便于制作。
    最理想的方式,是从使用者的角度,先决定应用功能、用键、输入形式、空间结构及模组划分等。
    这些都确定了,才能按图索骥,根据蓝图写程式。也就是说,规划即先把目标介定妥当,以便按步就班,循序执行。
  1,工作的认识:程式的规划,相当于设计建筑篮图,首先要明确认识工作性质、特徵、条件,选择取适当的方式,以求得到最理想的结果。

  2,程式的结构:结构要事先定案,是采用流程抑或模组?全部或部份采用组合语言?是依工作人员的素质分工,或者是用生产线统一制作?程式段、资料段及堆栈段的空间分配,每个程式联接次序的安排,暂存器的定义,缓冲器的设置等,都应该慎重地考虑清楚,并记录下来,以供工作之依据。

  3,参数的设定:参数是程式的处理对象,必须合情合理地安排,且要有扩充的余地。因为参数与功能分类及程式的效率息息相关,对程式师而言,参数即相当于程式的处理对象;对应用程式的人,则等于工作的分类标准。其安排的结果,影响功能价值甚钜。

  4,效率的要求:在第一章第三节中,我们特别强调了效率的法则,使用电脑的目的,就在于效率的追求。故在规划之初,就应该规定每一段程式的效率要求。
 另外,参数的运用,也应考虑其使用的频率,凡能在事先一次准备妥当的,切不可等到运用时再临时计算。

  5,测试的标准:测试就是品质管理,程式所应达到的指标,可由测试求证之。一般程式师只对程式的正确性负责。其实正确性与指标无关,乃是程式师最起码的责任。
 组合语言程式的品质,应该包括原程式的写作规格及时限;程式的思路、理念;执行时的时效;程式空间的大小及安排;应用的方便与否;指令运用是否恰当等等。
 执行程式的品管,则可以透过测试程式完成之。这种测试程式,也应该在规划之初,一并考虑。

  6,制作的进度:程式制作的进度很难控制,而正因为其难以控制,更要加倍小心,事先规划。其方法是先按照工作性质,设定工作「难度值」。再对工作的程式师评估其「能力值」,依此设定一个「合理」的进度。最后,根据实际的工作进度调整之。一般说来,程式师需要三年以上的写作经验,才能养成进度的观念。
    由于程式本身占有空间,所以其结构的好坏,对程式制作及执行效率影响极大。良好的结构应根据程式的性质、使用的频率、处理的先后过程等,在效率的立场,作全面的考虑。

一、程式性质

    前文曾介绍过程式的种类,在规划时,要更进一步了解程式的性质。因为所谓的效率,一是指程式的制作、维护,一是指程式的执行及调用。这两者必须根据程式的性质,作适当的安排。
    大体说来,程式的性质只有两种,一种是主动的,使用者可以直接控制执行;另一种是被动的、公用的,为其他程式所调用。
    前者也可以说是应用程式,而后者比较类似系统程式。只是在这里强调的是其性质,以及如何根据其性质进行规划。
    主动程式最好能放在同一模组中,当程式太大,必须分割时,也要设法联接在一起。但若在功能不同,所调用的模组亦无交集的情况下,为了避免跨越段与段时的效率损失,则无须考虑是否安置在同一模组内。
    由于主动程式涉及使用人的习惯和设计者的理念,经常需要修改调整,变动极大。正因为这种因素,其「再利用」价值不高,对效率的要求也较低。
    规划时,主动程式一定要与被动程式分开,而在制作时,则应注意其所占用的空间,与各段的关系。
    被动程式既然是公用的,必然具备一些基本的功能。所谓被动是表示该程式仅在某种条件下,才被调用。由于其「再利用」的特色,应该设计成为精简的子程式。再依情况需要,或者为了节省空间,放在磁碟中,随时以覆盖其他子程式方式调用,或者为了效率,直接联接备用。

二、使用频率

    对程式执行的效率而言,占用空间与处理时间,经常需要作些取舍。如果程式太大,空间不敷应用,则在规划时,先行统计各个程式的使用频率。
    不常用到的程式,不表示并不重要,但若占用了空间,则于效率有损。
    这种程式最好独立成一个模组,以便随时可以因应空间的条件,再作打算。
    因此在程式规划时,对空间的安排,应该优先考虑使用频率。也就是说,要设法将使
三、程式流程

    程式有一定的运行规律,称为「流程」,意即在执行时,依照预先安排的顺序,一一流经的过程。
    传统的程式写作法,流程非常重要,因为人需要根据一种理念,以逐步检验并付诸实现。但自从模组受到重视后,每个模组代表一种完整的功能,而功能的集合所能实现的效果,远非流程可以表达。这一来,流程的重要性减低了,其地位降到只属于结构中的一部份而已。
    因为模组是可组合的,模组越多,主流程便越短。虽然每一个模组也都各有其流程,结构良好的模组,又可再细分为若干子模组,以此类推。因此,流程远不如模组灵活,观念上也显得呆板。
    不过,即使在模组设计过程中,有时也有必要藉着流程逐步推理,以了解细部的结构。在这种立场,流程的应用原无可厚非。可是一旦反客为主,程式师不用流程即无从思考,就大谬不然了。
    流程属于单线思考,人虽然经常使用这种流程推理,但更重要的能力却来自「抽象思考」。抽象思考是指人在许多错综复杂、相互纠结的现象中,能立即掌握重点,针对目的,解决问题。
    在程式上,这种抽象思考相当于对所有模组的全面认知,而非仅仅是流程的推理。有了全面的认知,灵活地加以调用,程式的功能就更上层楼,大大的提高了。
    如果程式师只知道根据流程写程式,习惯养成后,不仅程式笨拙不堪,连人的思考方式都连带受到影响。一般所谓的「匠气」、「呆滞」,就是这种机械式训练的结果。
    因此,我不赞成利用流程来规划程式,但在解释或说明某种过程时,并不排除流程的方便性。

四、流程图

    流程的第一步是画流程图,根据流程图再作细部流程,然后根据细部流程去了解或编写程式。
    以上面例子,先制作流程图如下:
    ┌─────┐
 ┌>───┤表格码输入│
 │   └──┬──┘
 │   ┌──┴──┐
 │   │取 一  码│
 │   └──┬──┘
 │┌──────┴──────┐
 ││根 据 各 码 的 条 件 作 图│
 │└──────┬──────┘
 └───────┘
    请注意上图并没有出口,这是模组的特色之一,当码=0时,就进入结束程式。本子模组的工作,便是要安排好资料,再回到来处。
    上面说过,每个码是根据组码定义而来,每次取一码后,立即根据码的预设条件作图。(见第一节一、任务)这样不仅速度快,空间也精简。
    当然各码都有其子流程图,由于太简单,几句话便交待完毕,不必再作图了:首先确定位置、宽度、长度,即可画直线或横线,如此而已。

五、细部流程

    细部流程最重要的,是要确定暂存器的功能,另外需要缓冲器辅助的,也应在此详细说明。至于要详细到什么程度,则须根据程式的复杂性及写作的程式师而定。
    如果写作流程及程式为同一人,且无留供他人参考的必要时,细部流程甚至可以免掉。但若是集体制作,而且需要保留档案,则不仅有必要,更须写得明确,让他人能够一目了然,不论任何人都能接手才是。
    由于前面所举的例子太简单,如果要写细部流程,不待写完,程式早写好了。所以,有时也不必过分拘泥形式。在下节指令应用中,仅以附注的方式说明细部流程,与程式相对照,反而更有效率。

六、工作进度

    工作进度最重要的意义,不在于增加制作的速度,而是培养一种敬业的精神以及对工作的判断能力。
    工作进度的掌握靠进度表,而制作进度表需要对全部的工作有相当的认识。所以,进度相当于全面地、有系统地思考工作的细节,对程式师的工作能力的提高有极大的帮助。
    当然,进度能有效地督促工作,每当进度如期完成,对程式师而言,即是一种成就感。在制作大型程式时,时间往往拖得很久,如果没有进度作参考,人往往会有一种迷失在汪洋,不知身在何处的失落感。
    能力强的人,经常能正确判断工作所需的时间;反之,不知道工作需要多久,或者所预定的进度与事实相差太远,都表示着程式师的能力有限或不足。
    不过,写作程式完全要符合进度,也是不正确的观念。前文讨论过,写程式是种艺术,而非流水作业式的生产,创造性强的艺术工作是不可能用进度来加以控制的。
    进度的制作方式没有定则,只要记下工作项目,预计完成时间,实际完成时间即可。只是制作是一回事,执行又是另一回事,唯有认真负责的执行,才会有实用的价值。
    执行的方式不外每天或定期查阅进度,如果发现进度不符合,应该立刻分析原因,并做修正调整。至于所分析的结果,最好能记载下来,以作下次改进的参考。
    如此这般养成了习惯,自然而然,就有了自动分析判断的能力,而且还会增进规划和设计的理念。

 第五节 程式写作

    说了不少,才真正到了写作程式的时候,运用指令就相当于写程式。只是,在运用指令前,一定要充份了解一应相关的课题。否则,应用指令如同和稀泥一般,堆砌出一团可以运作的成品,我个人不认为那能叫做「写程式」,充其量只是涂鸦罢了。

一、暂存器安排

    因为暂存器不足,必须事先安排妥当,才能有效应用。
    再以前例说明,需要安排的因素有:
  1,字形大小:此项有两个变数需要安排,一是横向之始、终值; 另一是纵向之始、终值。因为在设计之初,我已经   考虑到极限值的问题,将上限定在 256点,恰在一个字元 的范围内。所以我们可以把横向始值放在暂存器BL中,终值放在BH,而纵向始值放在DL,终值放进DH。

  2,笔画粗细:有四个变数值,放在CL中,并使CH为0。( 这点相当重要,为了精简和效率,最好有一个暂存器为0)

  3,字码送入:在始存器SI中。

  4,字形输出:在终存器DI中,根据BX及DX值求得。

二、程式规格

    第二章第四节已介绍过,在此从略。

三、程式及说明

       ------程  式  部  份-----     ----说明部份----
    1: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    2: ;功能:仓颉表格码绘图用。";"后,皆为注解。   ;
    3: ;输入参数:DS:SI=字码(1-2 码),=0为终止。;
    4: ;   BL=X1   BH=X2       ;
    5: ;   DL=Y1   DH=Y2       ;
    6: ;   CX= 笔画粗细值。      ;
    7: ;输出结果:点阵在ES:DI中。      ;
    8: ;破坏暂存器:全部。       ;
    9: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   10: CCODIN:        ; 字码输入
   11:  LODSB       ; 取一码以作图
   12:  MOV CL,AL      ; 取粗细值
   13:  INC AX      ; 调整码值
   14:  AND AX,001EH     ; 24个有效双数值
   15:  PUSH BX      ; 保留后用
   16:  PUSH DX      ; 同上
   17:  MOV SI,AX      ; AX不能间接定址
   18:  AND CL,07H      ; 有效值
   19:  CALL CCODTB[SI]   ; 假设CS=DS
   20:  POP DX
   21:  POP BX
   22:  JMP CCODIN      ; 本程式主流程
   23: CCOD00:        ; CODe为码
   24:  ADD SP,6      ; 本程式为子程序
   25:  RET       ; 码为0执行完毕
   26: CCOD06:        ; 06为 E,F
   27:  SHL CL,1      ; E,F 粗细值加倍
   28: CLFT02:        ; LeFT指左横
   29:  ADD BH,BL      ; 02为输入码 A,B
   30:  SHR BH,1      ; 左起BH中点值
   31:  JMP CHOR00      ; HORizon 画横
   32: CCOD08:        ; 08为G,H
   33:  SHL CL,1
   34: CLEF04:        ; 04为C,D
   35:  ADD DH,DL
   36:  SHR DH,1      ; 上起DH为中点值
   37:  JMP CVER00      ; VERtical画直
   38: CCOD0E:
   39:  SHL CL,1
   40: CRGT0A:        ; RiGhT 指右横
   41:  ADD BL,BH      ; 0A为码I,J
   42:  SHR BL,1      ; BL为中点值
   43:  JMP CHOR00
   44: CCOD10:
   45:  SHL CL,1
   46: CCOD0C:
   47:  ADD DL,DH
   48:  SHR DL,1      ; DL为中点值
   49:  JMP CVER00
   50: CCOD16:
   51:  SHL CL,1
   52: CCOD12:
   53:  JMP CHOR00
   54: CCOD18:
   55:  SHL CL,1
   56: CCOD14:
   57:  JMP CVER00
   58: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   59: ;以下为各码之模组程式,间接定址表    ;
   60: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   61: CCODTB DW CCOD00      ; 结束
   62:  DW CCOD02      ; 码 A,B左横细轻
   63:  DW CCOD04      ; 码 C,D上直细轻
   64:  DW CCOD06      ; 码 E,F左横粗重
   65:  DW CCOD08      ; 码 G,H上直粗重
   66:  DW CCOD0A      ; 码 I,J右横细轻
   67:  DW CCOD0C      ; 码 K,L下直细轻
   68:  DW CCOD0E      ; 码 M,N右横粗重
   69:  DW CCOD10      ; 码 O,P下直粗重
   70:  DW CCOD12      ; 码 Q,R长横细轻
   71:  DW CCOD14      ; 码 S,T长直细轻
   72:  DW CCOD16      ; 码 U,V长横粗重
   73:  DW CCOD18      ; 码 W,X长直粗重

    程式到此全部完毕,唯画横及直与本主题无关,在此不做说明。由这段程式,可以看出最初规划输入码不理想之处,以致于需要在各处加粗细值,读者不妨自行研究改良之。
    程式长度为88个字元,共用程式原来已有,在此不计。
    此外,本段程式重复应用了几个指令,有很多方法可以精简,也有待读者动手。
    至于画横及直的子程式CHOR00和CVER00,其位置远近也影响写作方式,在此,且假定在 128B 之内,皆为短跳。否则还要动脑筋,设法简省之。
    还有,读者应该注意到,这段程式中,没有检查错误的手续,那是因为在内码处理时,已经保证无误。一般说来,检查错误是必要的程序。
    改进之方法,姑在此略作导引,设若编码时,将四个连续码视作由细至粗,而把位置分为六组,是则更易记忆,且程式可以减少28个字元之多。由此可见,像这样精简的程式,因资料结构上的缺陷,仍有改进的余地。
 第六节 特殊技巧的运用

    技巧的运用,关系组合语言的效率甚钜,虽然其重要性比不上整体规划,但也可以弥补规划的不足。
    由于「技巧」无法严格定义,兹将几种较常用的技巧介绍如下:

一、变数法:

    我非常反对在程式中采用「常数」的观念,因为常数是固定的,无法灵活应用。例如在 IBM PC 的 BIOS 中,萤幕游标上、下、左、右位置固定设为 0,25,0,80 等常数值。每次移动都受到这四个值的限制,故而形成不变的「视区」。若将这些常数改为变数,且容许使用者自行改变,则立即有了可变「视窗」的功能。
    也就是说,萤幕上、下、左、右四个位置,所围起来的区域,就是我们视觉及资料所限制的「视窗」,所有资料显示,在系统程式的控制下,无法超出此区。
    如果此四个位置值是变数,则使用者可以随意设定所需数值,如是则灵活方便,也就是所谓的视窗处理。
    在下文三、虚拟法例中,CKFUN 该段程式即为用变数处理视窗的范例。下面这段程式,亦为变数法的一种应用, MAP87和MAP9A 中,均需调用子程式DYBPJ1,唯一不同者,是在该子程式中,又需分别调用不同的子程式。共用DYBPJ1的方法,是先将不同子程式的位址,放在BP中,再行调用。
    1:MAP87:
    2:  MOV BP,OFFSET MAPF4
    3:  CALL MOVS211
    4:  CALL DYBPJ1
    5:  MOV DL,AH
    6:  ..

   11:MAP9A:
   12:  MOV BP,OFFSET MAP46
   13:  CALL DYBPJ1
   14:  MOV AH,1
   15:  ..

   20:DYBPJ1:
   21:  PUSH BX
   22:  PUSH DX
   23:  MOV DH,DL
   24:  CALL BP
   25:  POP AX
   26:  XCHG DH,AH
   27:  POP BX
   28:DYBPJRT:
   29:  RET

二、对称法:

    本法实际上就是利用间接定址的指令,将原系对称处理,或可以调整成为对称型的程式,灵活调用。
    下面这段程式,表面看来似乎毫不相干,但经过整理后,就有了眉目,然后再以暂存器间接定址,合并为一。像这种程式,规模越大,所节省的空间就越多。
    1:ABCD:
    2:  CMP DX,BUFA
    3:  JB ABCD1
    4:  CMP CL,BUFD
    5:  JB ABCD1
    6:  MOV BUFC,CL
    7:  INC BUFE
    8:  MOV BUFB,DX
    9:  DEC BUFF
   10: ABCD1:
   11:  ..
   12:  ..
      与另一段程式:
   20:EFGH:
   21:  CMP BX,BUFG
   22:  JB EFGH1
   23:  CMP CH,BUFH
   24:  JB EFGH1
   25:  INC BUFK
   26:  DEC BUFL
   27:  MOV BUFI,BX
   28:  MOV BUFJ,CH
   29:EFGH1:
   30:  ..
   31:  ..
    看来分别很大,先经过整理,得到左右两组程式:
    EFGH:        ABCD:
 CMP BX,BUFG    CMP     DX,BUFA
 JB EFGH1    JB     ABCD1
 CMP CH,BUFH    CMP     CL,BUFD
 JB EFGH1    JB     ABCD1
 INC BUFK    INC     BUFE
 DEC BUFL    DEC     BUFF
 MOV BUFI,BX    MOV     BUFB,DX
 MOV BUFJ,CH    MOV     BUFC,CL
    EFGH1:        ABCD1:
 ..     ..
 ..     ..
    对照之下可以看出,其所不同的,只是暂存器及缓冲器的分别而已。这种程式的出现,是由于事先规划不当,未能通盘考虑,头痛医头,脚痛医脚。原可以把暂存器及缓冲器统一运用,现在木已成舟,想要变更很可能影响大局。
    其次是写作的风格及习惯没有养成,在用指令时,信手拈来,以致两段程式,两种写法!
    现在唯一的补救方法,是利用间接定址法,先将两组不同的缓冲器,照相对次序排列妥当,再改写程式。
    1:ABCD:
    2:  MOV SI,OFFSET BUFA
    3:  MOV DI,OFFSET BUFB
    4:  MOV BX,DX
    5:  MOV CH,CL
    6:  JMP SHORT EFGH1
    7:EFGH:
    8:  MOV SI,OFFSET BUFG
    9:  MOV DI,OFFSET BUFI
   10:EFGH1:
   11:  CMP BX,[SI]
   12:  JB EFGH2
   13:  CMP CH,[SI+2]
   14:  JB EFGH2
   15:  MOV [DI],BX
   16:  MOV [DI+2],CH
   17:  INC BYTE PTR[DI+3]
   18:  DEC BYTE PTR[DI+4]
   19:EFGH2:
   20:  ..
    其缓冲器的相对顺序,如下所示:
   35:BUFA DW 0
   36:BUFD DB 0
   37:BUFB DW 0
   38:BUFC DB 0
   39:BUFE DB 0
   40:BUFF DB 0
   41:..
   65:BUFG DW 0
   66:BUFH DB 0
   67:BUFI DW 0
   68:BUFJ DB 0
   69:BUFK DB 0
   70:BUFL DB 0
    凡对称形式或结构相同的程式,都可以采用这种技巧。

三、虚拟法:

    对来处不同的资料,只要性质相同,都可以采用虚拟的技巧,将各种参数事先设妥,利用参数统一处理。
    兹有一程式,系供萤幕画图之用,所有功能皆用游标完成之,特以此段处理游标的程式为例说明如后。
    先虚拟各种功能及缓冲器:
    区段位移:      BBBLKMOV  DB  ?  ;?= 位移值
    十字游标:      BBCROCSR  DB  ?  ;?= 位移值
    视框移动:      BBFRMMOV  DB  ?  ;?= 位移值
    画笔作图:      BBDRW  DB  ?  ;?= 画笔宽
    橡皮擦:      BBDEL  DB  ?  ;?= 橡皮宽
    闪动游标:      BBCSR  DB  ?  ;?= 游标宽
    文字显示:      BBCHRDSP  DB  ?  ;?= 字框值
    游标移动值:     BWMOV  DW  ?  ;依当前功能先            ;设定
    视框上限:      BWTOP  DW  ?  ;?= 设定值
    视框下限:      BWBTM  DW  ?  ;?= 设定值
    视框左限:      BWLFT  DW  ?  ;?= 设定值
    视框右限:      BWRGT  DW  ?  ;?= 设定值
    右界边际值:     BWADDX  DW  ?  ;?= 字或图宽
    ..
    程式入口:      CHKKEY  DW  CK47; 向左上移
     DW  CK48; 向上移
     DW  CK49; 向右上移
     DW  CRET; 无效
     DW  CK4B; 向左移
     DW  CRET; 无效
     DW  CK4D; 向右移
     DW  CRET; 无效
     DW  CK4F; 向左下移
     DW  CK50; 向下移
     DW  CK51; 向右下移
    AX =  输入游标键扫瞄码
    BP =  萤幕X向移动值
    DX =  萤幕Y向移动值
    ES:DI=萤幕记忆区位址

    主流程: ( 已知 AL=0 AH=Scan-Code )
    1:KEYIN:
    2:  SUB AH,47H     ; =HOME
    3:  JB KEYRET     ; 无效
    4:  CMP AH,11     ; >扫瞄码范围
    5:  JA KEYRET
    6:  MOV BL,AH
    7:  SUB BH,BH
    8:  SHL BX,1
    9:  MOV AX,BWMOV    ; 移动值
   10:  JMP CHKKEY[BX]  ; 进入各处理程式

   ..
   40:CK47:
   41:  SUB DX,AX     ; Y 向
   42:CK4B:
   43:  NEG AX     ; 向左为负
   44:CK49:
   45:  ADD BP,AX     ; X 向
   46:  JMP SHORT CKFUN
   47:CK4D:
   48:  SUB DX,AX
   49:  JMP CK49
   50:CK51:
   51:  ADD DX,AX
   52:  JMP CK49
   53:CK48:
   54:  NEG AX     ; 向上为负
   55:CK50:
   56:  ADD DX,AX
   57:CKFUN:       ; 用变数观念检查「视窗」
   58:  CMP DX,BWTOP    ; 超出上限?
   59:  JNS CKFUN1
   60:  MOV DX,BWTOP    ; 上限值
   61:CKFUN1:
   62:  CMP DX,BWBTM    ; 超出下限?
   63:  JBE CKFUN2
   64:  MOV DX,BWBTM    ; 下限值
   65:CKFUN2:
   66:  CMP BP,BWLFT    ; 超出左限?
   67:  JNS CKFUN3
   68:  MOV BP,BWLFT    ; 左限值
   69:CKFUN3:
   70:  PUSH BP
   71:  ADD BP,BWADDX   ; 右限+边际值再比
   72:  CMP BP,BWRGT    ; 超出右限?
   73:  POP BP
   74:  JBE CKFUN4
   75:  MOV BP,BWRGT    ; 右限值
   76:CKFUN4:
   77:  ..
    CKFUN4以下为功能处理,由前面的功能参数决定。
    用这种方法,多种功能可以共用一个入口,程式精简且速度快。

四、桥式法:

    桥式法是利用读写记忆体的特性,将程式中若干指令直接填入,作为临时便桥,以改变此段程式的功能。
    例如在显示时,希望能提供多种变化,而又不愿减低速度及增加太多的程式。最好的方法,便是利用桥式法,在同一位址,填入需要的指令。
    桥式法用得好而又灵活时,对程式的效率极有助益。但是应该注意一点,就是只能用在可读可写的记忆区中,如若要制成「韧体」,即置入仅读记忆体(ROM) 中的程式,绝不可使用此法。
    下面的实例,即为萤幕显示的桥式应用。首先,把架桥的「材料」设置在缓冲器中,如:
    CDSPMOD   DB      88H ;8805 = MOV [DI],AL
       DB      30H ;3005 = XOR [DI],AL
       DB      08H ;0805 = OR  [DI],AL
       DB      20H ;2005 = AND [DI],AL
    CDSPMOD 即为缓冲器,其中有四个数据,分别为机器码的相异部份,如分号后所注。因为四组机器码皆有 05 ,不必再填。=右边部份,即为该机器语言相对应的指令。
    程式部份先设妥功能定义,利用一、所说的变数应用法,依序由0至3先载入暂存器BX中。根据 BX 值,将所需机器码载入程式中。
   10:CLOD:
   11:  MOV AL,CDSPMOD[BX] ;用BX取预存码
   12:  MOV CS:CDSP2[1],AL ;载入CDSP2+1
   13:CDSP:
   14:  SUB SI,SI  ;资料由0起
   15:CDSP1:
   16:  LODSB   ;取资料
   17:CDSP2 LABEL BYTE  ;载入的位置
   18:  XOR ES:[DI],AL ;原码26 30 05
   19:  INC DI  ;须改 30 部份
   20:  LOOP CDSP1
   21:  RET

五、流水线法:

    工业上的流水线生产作业,需要极为严格的规格限制,原器件分别研制完毕后,统一送到生产线上组装。
    程式亦可采用同样的方法,只可惜一时手头上找不到现成的、适用的例子,只得将方法概述如下:
    先设定处理流程,凡是能用这种生产流程者,皆适用。
    再设定处理流程中所采用的「生产线」,也就是缓冲器。因为流水线上所用的资料都需要由缓冲器提供。
    此缓冲器的长度由流程决定,缓冲器中的资料则由各调用本流程的原程式载入。
    各调用程式可视个别条件,将所需处理的资料,放在缓冲器内(全部或部份)。待调用后,再从原缓冲器中取出经过处理后的资料。

六、对应表法

    凡是指根据某种需要,将经过整理的资料,以某种固定的格式,安排在一特定区域中。每当需要时,立刻可以按照排列的位置取出来使用的,皆可称之为对应表。
    这种对应表是我最喜欢利用的技巧,速度奇快不说,修改也极其容易。尤其是我做事一向不拘小节,写起程式来,专出小错。自从采用了表格对照法后,凡是适合这种形式的程式,只要想通了最理想的结构,几个指令就把程式写完了。
    兹将附录中所举的例子,对字形放大所采用的查表法,在此作进一步的介绍。
    假设有一组图形,要在萤幕上左右放大一倍。一般程式师做这种题目,都是在暂存器内移来移去,每一个字元的资料,起码要移八次之多,每次都要用借位作为转换值。而转换时,又要放进一个16位元的暂存器中,尽管可以用回路去做,时间的延误相当大,读者可参考附录二以做比较。
    当然,表格要占用空间,以本例而言,如果一次用256B,取足则要512B。
    因此这种技术可以说是以空间换取时间。在第一章第三节「效率」的第四条定律下,我们知道键盘输入速度,决定于人的操作速度,而人的反应远远不及电脑,故应以人的速度为时间边际值,尽量设法节省。
    目前,所涉及的是显示时间,每个人在电脑前,都期望着立即得到结果。因此,显示速度不仅要快,而且越快越好。所以,前述的空时交换应在可能范围中,视实际的边际效应,以作取舍。
    现在看看资料分析,下面列举的二进位资料,在左边为原图形点阵,在右边则为放大一倍后的点阵:
    原点阵        左右放大一倍
    00000001      00000000 00000011
    00000010      00000000 00001100
    00000011      00000000 00001111
    ..
    01010101      00110011 00110011
    ..
    11111111      11111111 11111111
    现在有两个因素非常明显,第一,不论什么点阵,放大后长度加一倍,一字元有256 种。放大后点形种类不变,但字元数加倍为 512个。其次,由于放大后的 512个中,有一半皆相同,故仍可用256 种表示。
    至于取前者或后者,当视情况而定。
    决定以后,将之定义在缓冲器中,以原图形的点阵资料作为索引值,即可采间接定址法,立即取得放大后点阵。
    在制作对应表时,应养成良好的习惯,根据资料的规则,以等长度、固定的格式输入。这样不仅对表中的资料能一目了然,而且容易输入、侦错、修改,一举数得。
    如某表格为:
    100 TBXXX  DB  0,1,3,7,0FH,1FH,3FH,7FH,0FFH,2,6,0EH,1EH,3EH,7EH,0FEH
    此表看去远不如下表来得清楚、规律:
    100 TBXXX DB  000H,001H,003H,007H,00FH,01FH,03FH,07FH
    200       DB  0FFH,002H,006H,00EH,01EH,03EH,07EH,0FEH
    从事程式写作,规律的思考方式及追求,经常事半功倍。这种小技巧看似没有多大作用。事实上,在输入时,规则化的结构可以轻易地利用现有的功能,或复制,或修改。更有利的是能一眼看出该表的意义及正确性,在程式侦错时,往往可以节省大量的时间。

七、模式法

    所谓模式法,是指在程式的处理过程中,分析其规律,以期找到一种共同具有的「模式」。并用此模式,设计成为一个个程式单元,以追求最高效率。
    这种模式,可用「概念」来代表,但最理想的表达方法,仍以视觉图形为宜。也就是说,最好能把分析出来的模式,用图形表示,并据以理解及设计程式。
    兹以常用的功能「排序」为例,来说明模式法的应用,并设计成为程式。
    先假定需要排序的资料结构为:
    11每笔资料之长度固定为一字元。
    12资料形式为 ASCII码,16进位值,由 20H到 7EH。
    13排序时,资料数值小者排在低位,大者排在高位。
    14程式开始时参数设定为:
      AL= 高位之资料。
      AH= 低位之资料。
      DS:SI=资料存贮处。
    资料由低位开始检查,并同时排序,直到全部查完为止。排序时,交换高位及低位之资料,以使
    高位住址资料≥低位住址中之资料。
    由于人类行为与视觉息息相关,故最有效的认知方式,是以作图来说明。以下即为上一陈述之图形说明。

        │? │
    模式一供检查      ├─┤
    AL,AH 之大小    ┌ AH <--│? │<-- AL ┐
   模 │     ├─┤  │模
    模式二交换资料 式 ┤ AL <--│? │<-- AH ├式
    其中 AH>AL  一 │     ├─┤  │二
      └ SI =  │? │ =  SI ┘
        ├─┤

    由上图可见在模式一中,AH为低位资料,AL为高位资料。比较 AL,AH 之大小,即可知是否符合序列规定。如符合,则继续做下去,否则依模式二,将小值放进低位,大值放进高位住址中。程式只要设法保持此一处理之形式,即可简单明了地完成任务。
    1: COMPAR:
    2:  MOV AH,AL   ;设AH为低位值
    3: COMPAR1:
    4:  LODSB    ;取资料
    5:  CMP AL,AH   ;比大、小
    6:  JAE COMPAR   ;高位大,不变
    7:  MOV [SI-2],AX ;交换AH,AL,排序
    8:  DEC SI   ;向低位再查
    9:  MOV AH,[SI-2]
   10:  JMP COMPAR1

    当然,上面这段程式并不成立,因为没有出口,永远做不完。程式的终止有很多方法,一是用计数器,一是用位置来比较,也有用终止指令的,不一而足,各有长短。
    首先,假设在DS:SI 中,有一长度值,兹以计数器的回路来试试看:
    1:  LODSW
    2:  MOV CX,AX   ;似此,3B 18C
  ;若用 MOV   CX,[SI]
   INC   SI
   INC   SI
  ;则需 6B,21T
    3:  SUB AL,AL   ;先设最小值,备用
    4: COMPAR:
    5:  MOV AH,AL   ;设AH为低位值
    6: COMPAR1:
    7:  LODSB    ;取资料
    8:  CMP AL,AH   ;比大、小
    9:  JB COMPAR2   ;低位大,需排序
   10:  LOOP COMPAR   ;回路
   11:  RET    ;完成
   12: COMPAR2:
   13:  MOV [SI-2],AX ;交换AH,AL,排序
   14:  DEC SI   ;向低位再查
   15:  MOV AH,[SI-2]
   16:  JMP COMPAR1
    程式中的回路,对前面有一比较分支不太有利,因为回路每次要17T ,比较分支就是现成的回路,不利用形成浪费。
    若把回路改为位置比较,程式即为:
    1:  MOV CX,SI
    2:  ADD CX,[SI]
    3:  INC SI
    4:  INC SI
    5:  SUB AL,AL   ;先设为最小值,备用
    6: COMPAR:
    7:  MOV AH,AL   ;设AH为低位值
    8: COMPAR1:
    9:  LODSB    ;取资料
   10:  CMP SI,CX   ;比位置到终点?
   11:  JAE COMRET   ;完成
   12:  CMP AL,AH   ;比大、小
   13:  JAE COMPAR   ;高位大,再查
   14:  MOV [SI-2],AX ;交换AH,AL,排序
   15:  DEC SI   ;向低位再查
   16:  MOV AH,[SI-2]
   17:  JMP COMPAR1
   18: COMRET:
   19:  RET
    如此,在分支时,在第13条指令做回路,10,11 则比较住址以决定是否完成。这一来,完成结束只有一次,需时 16T,其余所有执行时间皆为4T,较前一回路快了13T 之多。
    再试用「终止指令」法,其必要条件为资料中有多余的组合可供选择。一般多以 00H,0FFH 等极端值比较理想,下面且以0FFH作为终止指令,并置于资料终止处。
    1:  MOV CL,0FFH   ;终止检查用
    2:  SUB AL,AL   ;先设为最小值,备用
    3: COMPAR:
    4:  MOV AH,AL   ;设AH为低位值
    5: COMPAR1:
    6:  LODSB    ;取资料
    7:  CMP AL,CL   ;比是否终止指令?
    8:  JAE COMRET   ;完成
    9:  CMP AL,AH   ;比大、小
   10:  JAE COMPAR   ;高位大,再查
   11:  MOV [SI-2],AX ;交换AH,AL,排序
   12:  DEC SI   ;向低位再查
   13:  MOV AH,[SI-2]
   14:  JMP COMPAR1
   15: COMRET:
   16:  RET
    似此,程式较短,其他效果差不多。
    这段程式,在处理速度上,还大有油水。因为已经检查过的资料,因为回路关系,还会不断地重复检查,是否能够避免这种情况呢?
    事实上,当排序到某住址时,即表示由该住址起,上面已经检查完毕。因此,只要记录下来,下次再查时,将住址还原即可。
    1:  MOV CL,0FFH   ;终止检查用
    2: COMPAR0:
    3:  SUB AL,AL   ;先设为最小值,备用
    4: COMPAR:
    5:  MOV AH,AL   ;设AH为低位值
    6: COMPAR1:
    7:  LODSB    ;取资料
    8:  CMP AL,CL   ;比是否终止指令?
    9:  JAE COMRET   ;完成
   10:  CMP AL,AH   ;比大、小
   11:  JAE COMPAR   ;高位大,再查
   12:  MOV DI,SI   ;暂时保存
   13: COMPAR2:
   14:  MOV [SI-2],AX ;交换AH,AL,排序
   15:  DEC SI   ;向低位再查
   16:  MOV AH,[SI-2]
   17:  LODSB    ;取排序资料
   18:  CMP AL,AH   ;比是否该排
   19:  JB COMPAR2   ;是
   20:  MOV SI,DI   ;否,将原位址还原
   21:  JMP COMPAR0   ;从头再做
   22: COMRET:
   23:  RET
    总而言之,程式的变化无穷无尽,尤其是用组合语言制作程式,更是灵活精妙。就像下围棋一般,往往一两个指令就足以将整个局势扭转过来。
    程式的效率经常决定在回路上,读者千万不要以为一两个时钟脉冲算不了什么。要知道,汪洋大海,也是由一点一滴的水珠累积而成的。
    这段程式还有不少值得深思的,读者们不妨自行研究吧!想得多了,自然会有生花妙笔。

    真要作大量的资料排序,还有更有效的方法,也是应用模式分析的原则,先找出资料的「型」。
    假如以同样性质的资料为例,为了避免资料一一查找,浪费时间。我们不妨研究一下,是否有可能,直截了当,就把资料依据大小,予以定位,一次排好?
    电脑的好处,就在于资料的规律性,我们理应利用这种优点,来找出其排序的模式。
    前例曾分为两个模式,一是查找,一是搬移。如果我们把查找改为记录,把搬移改为安排,则情形就大大的不同了。
    记录时利用间接定址技巧,每笔取到的资料,皆可视为一个数值,对应于一记录的缓冲区。如果资料中每笔资料总值大于256 且小于 65536,则可以用二字元记录之。再若资料为二进位值,可由 0至 255,即设 512个对应单位。
    假定原资料在 DS:SI中,长度在CX中。
    首先,设一记录区为:
    1: RECORD DB 512 DUP (0)
    2:  PUSH SI   ;程式开始
    3: CHECK:
    4:  LODSB    ;取资料,AH永远为0
    5:  MOV BX,AX   ;利用BX间接定址
    6:  INC WORD PTR RECORD[BX]
    7:  LOOP CHECK
    8: STORE:
    9:  MOV SI,OFFSET RECORD+512;指向最
         ;后记录
   10:  MOV BP,OFFSET RECORD    ;供检查
   11:  POP DI   ;资料贮存处
   12: STORE1:
   13:  CMP SI,BP   ;查是否完毕?
   14:  JE RECEND   ;完成
   15:  DEC SI   ;向上取
   16:  DEC SI
   17:  MOV CX,[SI]   ;取记录值
   18:  JCXZ STORE1   ;无记录,重取
   19:  MOV AX,SI   ;当前之位址
   20:  SUB AX,BP   ;差值
   21:  SHR AX,1   ;原有值
   22: STORE2:
   23:  REP STOSW   ;重新载入
   24:  JMP STORE1   ;继续
   25: RECEND:
   26:  RET
    程式的变化无穷无尽,尤其是用组合语言写作程式,简直没有止境。只要稍稍用点心,加一点点变化因素,一个巧妙无比的程式,就会跃然而出。
    写程式的乐趣,就在于心智的投入。学者们不妨试着把这   式再加以改良,其中还有不少可以下手的地方,养成习惯以后,程式自然就会精简了。

八、预置法

    预置法适用于流程的安排,尤其是在不确定的情况下,有时需要作多项检查,不仅浪费时间,对空间也不利。
    例如有一段程式,其目的在于处理使用者所选择的流程。由于使用者事先通过介面程式,选妥各项工作,现在必须依某一顺序执行。
    这是一项难度相当大的工作,要执行固定顺序不难,下面的程式就可以达到目的。当然,一如既往,我们会尝试着将程式一再改进。最后,我们再来讨论如何能执行使用者所安排的顺序。
    设子流程有八种,使用者选用时,可令BX值等于子程式的代号。选用方式为「开关式」,即单数次为开,设定参数,复数次为关,取消设定。
    设定后,因为共有八种程式,可以用八个位元来设置所需要执行的旗号。当然,这要看程式的多少而定,八位元正好用一个旗号FLAG:
    1: SETUP:
    2:  CMP BX,MAXVAL ; 最大值检查
    3:  JA SETRET  ; 超过,无效
    4:  SHL BX,1  ; 参数乘2
    5:  JMP SUBTB[BX] ; 各种程式
    6: SUBTB DW SUB1  ; 各种程式
    7:  DW SUB2  ; 程式中设定
    8:  ..   ; FLAG
    9:  DW SUBN
   10: ENTER:
   11:  SHR FLAG,1  ; 检查FLAG
   12:  JNC ENTER1
   13:  CALL SUB1  ; 有设定
   14: ENTER1:
   15:  SHR FLAG,1
   16:  JNC ENTER2
   17:  CALL SUB2
   18: ENTER2:
   19: .. ; 如此连续进行八次
    显然这种做法其笨无比,第十条以后,可用回路取代:
   10: ENTER:
   11:  MOV CX,8
   12:  MOV AL,FLAG  ; 暂存器较有效
   13:  OR AL,AL
   14:  JZ ENTRET  ; 不必做
   15:  SUB BX,BX
   16: LOOP0:
   17:  SHR AL,1
   18:  JNC LOOP1
   19:  PUSH AX
   20:  PUSH BX
   21:  PUSH CX
   22:  CALL SUBTB[BX]
   23:  POP CX
   24:  POP BX
   25:  POP AX
   26: LOOP1:
   27:  INC BX
   28:  INC BX
   29:  LOOP LOOP0
   30: ENTRET:
   31:  RET
    这样好得多了,可是,还能不能再加改进呢?组合语言的妙处就在于变化无穷,且看看是否还能变出花样来。
    从设置开始,方式稍微改变一下,旗号的观念是供程式检查用。在应用时,要占用一个暂存器,而暂存器有限,浪费了可惜。此外,八个不同的子程式,又要占用一个计数用的暂存器,最好能够省掉。
    因此,设置的重要性就显而易见了,程式的好坏,并非仅仅在于指令的应用。原始的理念,及程式的规划,经常在程式设计之前已经决定了。
    我们称之为「预置法」,把前述的设置方式改变一下,用一组缓冲区,先定义如下:
  DB 0  ; 计数用
     BUFER DW 8 DUP (0) ; 存程式入口用
  DW 1  ; 终止信号
    然后再设计程式,预置及执行如次:
    1: SETUP:
    2:  CMP BX,MAXVAL ; 最大值检查
    3:  JA SETRET  ; 超过,无效
    4:  SHL BX,1  ; 参数乘2
    5:  ADD BX,SUBTB
    6:  JMP BX  ; 各种程式
    7: SUB3:
    8:  XOR BUUER,BX ; 设为第三组
    9:  JNZ SUB31  ; 开
   10: SUB30:
   11:  DEC BUFER-1  ; 取消
   12:  RET
   13: SUB31: INC BUFER_1  ; 计数
   14:  RET
   15: SUBTB DW SUB1
   16:  DW SUB2
  ..
   21:  DW SUBN
   22: ENTER:
   23:  MOV SI,OFFSET BUFER-1
   24:  LODSB   ; 查是否需要
   25:  OR AL,AL  ; 为0则无
   26:  JZ ENTRET
   27: ENTER1:
   28:  LODSW   ; 取程式资料
   29:  CMP AX,1  ; 查程式入口
   30:  JB ENTER1  ; 0表示不做
   31:  JZ ENTRET  ; 1表示终止
   32:  PUSH SI
   33:  CALL AX  ; 执行
   34:  POP SI
   35:  JMP ENTER1
   36: ENTRET:
   37:  RET
    前一段调用程式需要31个字元,而现在只要21个字元,速度也快得多。不仅如此,前段程式仅能提供八个子程式,最多用十六位元,不过十六个子程式。本程式则不然,只要预留的缓冲器够,可提供的子程式可以说是无限。
    更重要的功能,是程式执行的顺序。除了这种预置法外,其他的方法,都受限于 SUBTB的安排次序,无法变更。但本方法则完全可任依使用者的需要,来决定子程式执行的顺序,以及是否执行。
    请注意在 SETUP时,BX的参数就同时代表了执行的顺序。如果要想依照设定的次序决定顺序,只要将缓冲区加大,再加一组预设程式即可,如下所示:
    1: SETUP:
    2:  SHL AX,1  ; 输入参数
    3:  ADD AX,OFFSET SUBTB ; 子程式入口
    4:  MOV BX,BUFER-2 ; 位置序数
    5:  SHL BX,1  ; 指向位置
    6:  MOV BUFDER[BX],AX ; 存入缓冲区
    7:  INC WORD PTR BUFER-2; 序数加一
    8:  RET
    这一来,先调用的程式放在前面,后调用的放在后面,使用者只要知道子程式的代号,就可以随意安排调用。
    甚至于各子程式所需的参数,也可以用类似的方法,预先设置妥当,然后一次取出运用。
    预置法最宜于「用户」接口,而且作为应用程式,既简单又容易,方便灵活。
    比如有一些应用模组,即可应用此方法,分别归类、编号后,书于手册中,以提供使用者选择、应用。
    使用者选择介面的方法,可以通过萤幕提示,将各种模组显示在指定位置上。使用者利用游标,或其它选择方法,以求得到正确的编号,再依序置于缓冲区中。
    各种模组都可能需要输入参数或资料,所以,另外要准备一个参数缓冲区,在选择模组时,同时选择参数。由于各模组会自动取用参数,故只要置入即可。
    假设有一个「使用者自行设计程式」的工具套件,(“聚珍整合系统”就建立在这观念上,惜因我们人手不足,产品可能要到1991年才能上市。)萤幕提示有介面、功能、共用等各类模组,使用者选完一类后,萤幕再度提示该模组的编号。
    萤幕上的模组编号经过程式转换,得到程式编号,将此编号存入缓冲区,再查是否需要输入资料。即可按照原有流程设计,逐步执行下去,完全可以利用这种预置法。
    1: GETMOD:
    2:  SUB AX,AX
    3:  INT 16H  ; 使用者输入
    4:  CALL GETSUB  ; 转换为代号
    5: SETUP:    ; 代号置于AX
    6:  SHL AX,1  ; 次序乘二
    7:  ADD AX,OFFSET SUBTB ; 子程式入口
    8:  MOV BX,BUFER-2
    9:  SHL BX,1
   10:  MOV BUFFER[BX],AX
   11:  INC WORD PTR BUFER-2
   12:  JMP GETVAR  ; 查取参数否
    当然,真正可以应用的程式,还要考虑很多因素,但大致上,结构就这样简单。
    写程式和画画没有两样,多看、多参考别人的程式,多想、多钻研各种方法,最后则是要多多动手,除此之外,别无其他法门。


0 0

相关博文

我的热门文章

img
取 消
img