上次我分析了一下,debug模式下反汇编后的算法部分代码,天才的您可能觉得不算糟,想再搞点花样,那么本文就能满足你的需求。天书夜读上其实还贴出来了release模式下的代码,它经过vc编译器O2的优化,我初次看到反汇编代码时,还真汗了一把。不过定下心来细细品位还是可以看懂的,尽管正如原书所说,连语句的对应顺序也已经不见了。。。
废话不多说,先贴出代码大伙“饱饱眼福”。。。
00401000 mov eax,dword ptr [esp+4]
00401004 mov edx,dword ptr [esp+0Ch]
00401008 mov ecx,dword ptr [esp+8]
0040100C push ebx
0040100D push esi
0040100E add eax,4
00401011 push edi
00401012 add edx,8
00401015 mov esi,3
0040101A lea ebx,[ebx]
00401020 mov ebx,dword ptr [eax]
00401022 imul ebx,dword ptr [ecx+0Ch]
00401026 mov edi,dword ptr [ecx+18h]
00401029 imul edi,dword ptr [eax+4]
0040102D add edi,ebx
0040102F mov ebx,dword ptr [eax-4]
00401032 imul ebx,dword ptr [ecx]
00401035 add edi,ebx
00401037 mov dword ptr [edx-8],edi
0040103A mov ebx,dword ptr [eax]
0040103C imul ebx,dword ptr [ecx+10h]
00401040 mov edi,dword ptr [ecx+1Ch]
00401043 imul edi,dword ptr [eax+4]
00401047 add edi,ebx
00401049 mov ebx,dword ptr [eax-4]
0040104C imul ebx,dword ptr [ecx+4]
00401050 add edi,ebx
00401052 mov dword ptr [edx-4],edi
00401055 mov ebx,dword ptr [eax+4]
00401058 imul ebx,dword ptr [ecx+20h]
0040105C mov edi,dword ptr [ecx+14h]
0040105F imul edi,dword ptr [eax]
00401062 add edi,ebx
00401064 mov ebx,dword ptr [eax-4]
00401067 imul ebx,dword ptr [ecx+8]
0040106B add edi,ebx
0040106D mov dword ptr [edx],edi
0040106F add eax,0Ch
00401072 add edx,0Ch
00401075 dec esi
00401076 jne myfunction+20h (401020h)
00401078 pop edi
00401079 pop esi
0040107A xor eax,eax
0040107C pop ebx
代码确实精简了不少,也精简地一点对应关系也找不到了。。。这份汇编代码,比(二)中其实要完整,因为它把整个函数调用到返回的代码都列出来了,尽管也早精简得不是标准的函数汇编代码了,由此可见VC编译器的优化能力之强令人叹为观止。。。
老规矩,对上述汇编代码的部分内容先作一简析。
第一. 细心的读者一定已经发现,(二)中的[](间接寻址符)包含的都是活生生的变量名,而在release中已经变为寄存器的表达式,当然我们可以根据参数入栈的顺序来推导出哪个是哪个,这会在后续有所说明。
第二. 正常的函数调用过程开始都会先将ebp入栈,而后将ebp用作esp的暂存寄存器,而在这里,从头至尾就没有出现过ebp,当然主要是因为esp并没有被函数用做特殊用途,再追溯根源一点,是因为循环变量i被更高速的寄存器esi所替代,而j这个循环。。。很神奇的由于只有3次,居然被编译器展开成三个等效计算过程给代替了。
第三. 最后一点也不是很重要,就是寄存器压栈的指令原本是放在最前的,现在又很神奇地被嵌在了数据计算语句之间。这点更加体现了经过优化的代码比较不符合人的常识,但更契合高性能的需要。
现在我们从头开始,一段段看:
; 随着c, b, a顺序入栈,堆栈向地址减小的方向发展,故而可以容易推断出
00401000 mov eax,dword ptr [esp+4] ; 对应a
00401004 mov edx,dword ptr [esp+0Ch] ; 对应c
00401008 mov ecx,dword ptr [esp+8] ; 对应b
; 40100C, 40100D, 401011 三个指令是常规的寄存器保存工作
0040100C push ebx
0040100D push esi
; 这里给eax与edx分别加上一个值的目的,后面会得知
0040100E add eax,4
00401011 push edi
00401012 add edx,8
; 使用esi寄存器作为循环变量,提高执行速度
00401015 mov esi,3
; 下面一行指令尽管看不出什么用处,但我倒不是很赞同原书上的说法,
; 如果有哪位大侠知道,请告知下大家,谢谢
0040101A lea ebx,[ebx]
{ esi循环体
; 原先j = 0时要执行的代码片段
; 最初eax保存了a数组的首地址,而对eax+4,则等于跳到0行1列这个元素
; 所以这时ebx = a[0][1]
00401020 mov ebx,dword ptr [eax]
; ecx对应b, 加了12,即跳跃过3个int,指向b[1][0]
; ebx = a[0][1] * b[1][0]
00401022 imul ebx,dword ptr [ecx+0Ch]
; 同理edi = b[2][0]
00401026 mov edi,dword ptr [ecx+18h]
; edi = b[2][0] * a[0][2],注意这里的eax+4,实际上是a往后8个字节
00401029 imul edi,dword ptr [eax+4]
; edi = b[2][0] * a[0][2] + a[0][1] * b[1][0]
0040102D add edi,ebx
; ebx = a[0][0], eax-4就是a首地址,即0行0列这个元素
0040102F mov ebx,dword ptr [eax-4]
; ebx = a[0][0] * b[0][0]
00401032 imul ebx,dword ptr [ecx]
; edi = b[2][0] * a[0][2] + a[0][1] * b[1][0] + a[0][0] * b[0][0]
; 把上式中各项换下位置,稍微整理下,很容易就看出它就是当j = 0时,c[i][0]的表达式
; 大家可以对照C的源程序,一看就会明白
00401035 add edi,ebx
; 接着下面一句,由于前面edx被加过一次8,故而这里edx-8就指向c首地址,
; 即c[0][0] = edi = b[2][0] * a[0][2] + a[0][1] * b[1][0] + a[0][0] * b[0][0]
00401037 mov dword ptr [edx-8],edi
; 下面j = 1, 2的情况与j = 0的算法完全一样,大家完全可以自己看懂了
; 原先j = 1时要执行的代码片段
0040103A mov ebx,dword ptr [eax]
0040103C imul ebx,dword ptr [ecx+10h]
00401040 mov edi,dword ptr [ecx+1Ch]
00401043 imul edi,dword ptr [eax+4]
00401047 add edi,ebx
00401049 mov ebx,dword ptr [eax-4]
0040104C imul ebx,dword ptr [ecx+4]
00401050 add edi,ebx
00401052 mov dword ptr [edx-4],edi
; c[0][1] = edi = b[2][1] * a[0][2] + a[0][1] * b[1][1] + a[0][0] * b[0][1]
; 原先j = 2时要执行的代码片段
00401055 mov ebx,dword ptr [eax+4]
00401058 imul ebx,dword ptr [ecx+20h]
0040105C mov edi,dword ptr [ecx+14h]
0040105F imul edi,dword ptr [eax]
00401062 add edi,ebx
00401064 mov ebx,dword ptr [eax-4]
00401067 imul ebx,dword ptr [ecx+8]
0040106B add edi,ebx
0040106D mov dword ptr [edx],edi
; c[0][2] = edi = b[2][2] * a[0][2] + a[0][1] * b[1][2] + a[0][0] * b[0][2]
} esi 循环体
; 可以看到,上面这个循环体每次处理的是a与c对应的一行数据,如上面的注解是针对第0
; 行的情况,而下面两个指令就是把a与c分别下移一行,即下次将处理a与c的第1行,只要
; 把上面注解中c[0][x]改为c[1][x],a[0][y]改为a[1][y],依次类推……
0040106F add eax,0Ch
00401072 add edx,0Ch
; 循环变量esi减1,若还不为0,则循环未结束,跳转后接着循环
00401075 dec esi
00401076 jne myfunction+20h (401020h)
; 函数结束时常规的寄存器恢复工作,依次恢复edi, esi, ebx,和保存顺序相反
00401078 pop edi
00401079 pop esi
0040107A xor eax,eax ; 返回值在eax里,异或运算,把eax清零,即返回值是0
0040107C pop ebx
其实对C反汇编的技术还有很多,这里只是举个例子给大家起一个抛砖引玉的作用,要完全领悟还得靠自己去学习去多多实践。
本系列三篇文章就结束了,首先感谢《天书夜读》的作者,另外如果由于我语文或技术水平有限导致理解有些困难的话,那我先说声sorry哈,也欢迎大家在回复中提出各种问题共同探讨,共同进步!