管理员
|
阅读:3942回复:0
[系统教程]汇编语言的艺术(组合语言的艺术)--基本认识(3)
楼主#
更多
发布于:2011-10-11 19:48
| | | | 二、程式要条理通顺
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 MOVSB6,有段程式,很有规律,但却极无效率: 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 才是.
| | | | |
|