菜鸟看懂算法以后之一:头痛的64次左移


    由于jney2上网在线的时间不多,加上老大的论坛也经常上不了,有限的在线时间也就是从天空软件站里拉些共享软件供自己练练手、在论坛里看看新的精华帖,所以与论坛的交流不算很多。加上本人做过电脑培训的工作,也非常愿意把自己的一点点收获告诉象我一样的菜鸟。于是前两天分析了“中华压缩V10.8”,写好了贴子,准备今天发上来,结果一搜索“中华压缩”,便看到baby2008于三月二十几号就已经把一篇完整的算法帖发在论坛上了。也就只好作罢。

但jney2的收获又好象不在写出了一篇几乎与baby2008一模一样的破文,因为我的收获是真正搞懂了一段复杂汇编代码的高级语言的含义,而这段代码也难倒了不少好汉,甚至包括早期的Fly大侠。baby2008应该是懂的,他的文章中点出了要害,但可惜也没有详细说明这段代码为什么是这样写,它的原理又是什么。于是我又有了共享的冲动,只不过它是免费的。算是一篇教程吧,这样大家只要看到这段代码就知道是实现的是什么功能了。

    因为程序中有多处调用该函数,且其它软件的破文中也找到类似的函数,我就断定,这不可能是作者写的,应该是编译系统的系统函数,最终印证了我的想法是正确的。

好了,引出我们的主角———:四字长的MOD函数:

先看一下入口参数:
0050151A  |.  52                 |push edx
0050151B  |.  50                 |push eax                          ;EDX(高双字)+EAX(低双字):这里是除数,压缩入堆栈保存。
0050151C  |.  8B46 68            |mov eax,dword ptr ds:[esi+68]     
0050151F  |.  8B56 6C            |mov edx,dword ptr ds:[esi+6C]     ;EDX(高双字)+EAX(低双字):这里是被除数。
00501522  |.  E8 0945F0FF        |call _ChinaZi.00405A30

     再看函数体:

00405A30  /$  55                 push ebp
00405A31  |.  53                 push ebx
00405A32  |.  56                 push esi
00405A33  |.  57                 push edi                           ;保护现场,因为下面要用到这几个寄存器。
00405A34  |.  31FF               xor edi,edi                        ; EDI清0
00405A36  |.  8B5C24 14          mov ebx,dword ptr ss:[esp+14]
00405A3A  |.  8B4C24 18          mov ecx,dword ptr ss:[esp+18]      ;ECX(高双字)+EBX(低双字):取除数到寄存器。
00405A3E  |.  09C9               or ecx,ecx
00405A40  |.  75 08              jnz short _ChinaZi.00405A4A        ;ECX不为0则跳走,也就说除数超过32位则跳走
00405A42  |.  09D2               or edx,edx
00405A44  |.  74 5D              je short _ChinaZi.00405AA3         ;如果EDX也为0,则跳到最简便的32位除法运算,一条指令搞定。
00405A46  |.  09DB               or ebx,ebx
00405A48  |.  74 59              je short _ChinaZi.00405AA3         ;如果EBX也为0,则跳到最简便的32位除法运算,一条指令搞定。如果从这里跳走的话,实际上就是除以0,应该会出现异常的啦!
00405A4A  |>  09D2               or edx,edx
00405A4C  |.  79 0A              jns short _ChinaZi.00405A58        ;符号为正则跳,即为被除数为正整数就跳
00405A4E  |.  F7DA               neg edx
00405A50  |.  F7D8               neg eax
00405A52  |.  83DA 00            sbb edx,0                          ;为负则取反
00405A55  |.  83CF 01            or edi,1                           ;设置为负的标志                       
00405A58  |>  09C9               or ecx,ecx
00405A5A  |.  79 07              jns short _ChinaZi.00405A63         ;符号为正则跳,除数为正整数就跳
00405A5C  |.  F7D9               neg ecx
00405A5E  |.  F7DB               neg ebx
00405A60  |.  83D9 00            sbb ecx,0                           ;为负则取反
00405A63  |>  89CD               mov ebp,ecx                         ;除数变为EBP+EBX,因为ECX要做计数器
00405A65  |.  B9 40000000        mov ecx,40                        ;设定循环次数为64次,为什么?
                                                                   ;也许很多人搞不懂的就是这里,我开始也就是搞不定这里,我参考了数篇破文,甚至搬出了我那古老的8086汇编教程,搞到深夜一两点,我终于开窍了。
                                                                   ;因为两个32位寄存加起来刚好是64位,也就刚好把EDX+EAX的值移到(当然是要用带进位的)EDI+ESI中(如果不作后面的减法运算的话)
00405A6A  |.  57                 push edi                          ;保存这个临时变量到堆栈
00405A6B  |.  31FF               xor edi,edi
00405A6D  |.  31F6               xor esi,esi                       ;EDI+ESI=0
00405A6F  |>  D1E0               /shl eax,1                        
00405A71  |.  D1D2               |rcl edx,1
00405A73  |.  D1D6               |rcl esi,1
00405A75  |.  D1D7               |rcl edi,1                        ;左移一位,实际上就是借一位,也等于乘2
00405A77  |.  39EF               |cmp edi,ebp                      ;比较除数的高双字
00405A79  |.  72 0B              |jb short _ChinaZi.00405A86       ;小于就直接进行下一次借位,也也就是说不够除。
00405A7B  |.  77 04              |ja short _ChinaZi.00405A81       ;大于就减去除数
00405A7D  |.  39DE               |cmp esi,ebx                      ;高双字相等的话,继续比较低双字
00405A7F  |.  72 05              |jb short _ChinaZi.00405A86       ;小于就直接进行下一次借位,也也就是说不够除。
00405A81  |>  29DE               |sub esi,ebx
00405A83  |.  19EF               |sbb edi,ebp                      ;减去除数
00405A85  |.  40                 |inc eax                          ;加1?NO。也许很多人更加搞不懂这里。为什么?答案:这是商!这是该段循环的巧妙之处,因为是二进制,所以:够除,商为1,不够除,又利用EAX左移在后面补0,即商为0,然后充分利用循环将得到一位位商换算成二进制保存在逐渐移出的EDX+EAX中。
00405A86  |>^ E2 E7              \loopd short _ChinaZi.00405A6F    ;循环直到除法做完
00405A88  |.  89F0               mov eax,esi
00405A8A  |.  89FA               mov edx,edi                       ;出参返回余数,如果没有这两句,则出参返回商。相信大家看到这里就应该豁然开朗了。还是不懂?反复看我的解说,反复琢磨。还是不懂?那说明你对二进制和移位指令的理解有问题
00405A8C  |.  5B                 pop ebx                           ;弹出堆栈中的标志位到EBX,EBX作为除数在这里已经不需要了。
00405A8D  |.  F7C3 01000000      test ebx,1
00405A93  |.  74 07              je short _ChinaZi.00405A9C        ;为正整数,则跳走
00405A95  |.  F7DA               neg edx
00405A97  |.  F7D8               neg eax
00405A99  |.  83DA 00            sbb edx,0                         ;为负则对出参也取反
00405A9C  |>  5F                 pop edi
00405A9D  |.  5E                 pop esi
00405A9E  |.  5B                 pop ebx
00405A9F  |.  5D                 pop ebp                            ;恢复现场
00405AA0  |.  C2 0800            retn 8                             ;返回
00405AA3  |>  F7F3               div ebx                            ;32位除法
00405AA5  |.  92                 xchg eax,edx                       ;交换商和余数,有这句则出参为余数,没有这句则出参为商。
00405AA6  |.  31D2               xor edx,edx                        ;高位清0,调整出参为64位
00405AA8  \.^ EB F2              jmp short _ChinaZi.00405A9C
00405AAA   .  C3                 retn


   真正一句一句读懂这个函数的汇编代码后,才觉得妙呀!不多一字,不少一字,有简有繁,有正有负,真是佩服写这段代码的程序员呀!

好了,这篇超详细的教程就先到这里吧,都凌晨03:59 2005-04-22了,明天还要上班呢。大家以后看见了这头痛的64次左移,就知道是在做除数运算了,只要注意一下出参就行了。要写注册机,也就好写得多了,不要再将这汇编代码贴到你的高级语言程序中去了。


    这也算逆向吧。jney2好久没这样熬夜了,还有两个小时可睡。本文完