admin 管理员组文章数量: 887018
感谢博主 http://book.51cto/art/200711/59874.htm
2.2 读懂机器的语言:汇编,CPU执行指令的最小单元
2.2.1 需要用汇编来排错的常见情况
汇编是CPU执行指令的最小单元。下面一些情况下,汇编级别的分析通常是必要的:
1. 阅读代码看不出问题,但是跑出来的结果就是不对,怀疑编译器甚至CPU有毛病。
2. 没有源代码可以阅读。比如,调用某一个API的时候出问题,没有Windows的源代码,那就看汇编。
3. 当程序崩溃,访问违例的时候,调试器里看到的直接信息就是汇编。
调试中涉及的汇编知识分为两部分:
1. 寄存器的运算,对内存地址的寻址和读写。这部分是跟CPU本身相关的。
2. 函数调用时候堆栈的变化,局部变量全局变量的定位,虚函数的调用。这部分是跟编译器相关的。
汇编的知识可以在大学计算机教程里面找到。建议先熟悉简单的8086/80286的汇编,再结合IA32芯片结构和32位Windows汇编知识深入。建议的资源:
AoGo汇编小站
http://www.aogosoft/
Intel Architecture Manual volume 1,2,3
http://www.intel/design/pentium4/manuals/index_new.htm
案例分析:用汇编读懂VC编译器的优化
问题描述
客户在开发一个性能敏感的程序,想知道VC编译器对下面这段代码的优化做得怎么样:
int hgt=4;int wid=7;for (i=0; i<hgt; i++)for (j=0; j<wid; j++)A[i*wid+j] = exp(-(i*i+j*j)); |
最直接的方法就是查看编译器生成的汇编代码分析。有兴趣的话先自己调试一下,看看跟我的分析是否一样。
我的分析
我分析的平台是,VC6,release mode下编译:(因为当时做这个case的时候,客户用的VC6。现在VC6已经退出历史舞台,微软不再提供支持)。
int hgt=4;int wid=7;24: for (i=0; i<hgt; i++)0040107A xor ebp,ebp0040107C lea edi,[esp+10h]25: for (j=0; j<wid; j++)26: A[i*wid+j] = exp(-(i*i+j*j));00401080 mov ebx,ebp00401082 xor esi,esi// The result of i*i is saved in ebx00401084 imul ebx,ebp00401087 mov eax,esi// Only one imul occurs in every inner loop (j*j)00401089 imul eax,esi // Use the saved i*i in ebx directly. !!Optimized!!0040108C add eax,ebx 0040108E neg eax 00401090 push eax00401091 call @ILT+0(exp) (00401005)00401096 add esp,4 // Save the result back to A[]. The addr of current offset in A[] is saved in edi00401099 mov dword ptr [edi],eax 0040109B inc esi// Simply add edi by 4. Does not calculate with i*wid. Imul is never used. !!Optimized!!0040109C add edi,40040109F cmp esi,7004010A2 jl main+17h (00401087)004010A4 inc ebp004010A5 cmp ebp,4004010A8 jl main+10h (00401080) |
这段代码涉及到的优化有:
1. i*i在每次内循环中是不变化的,所以只需要在外循环里面重新计算。编译器把外循环计算好的i*i放到ebx寄存器中,内循环直接使用。
2. 对A[i*wid+j]寻址的时候,在内循环里面,变化的只有j,而且每次j都是增加1,由于A是整型数组,所以每次寻址的变化就是增加1*sizeof(int),就是4。编译器把i*wid+j的结果放到了EDI中,在内循环中每次add edi,4来实现了这个优化。
3. 对于中间变量,编译器都是保存在寄存器中,并没有读写内存。
如果这段汇编让你手动来写,你能做得比编译器更好一点吗?
案例分析:VC2003 编译器的bug、debug模式正常,release模式会崩溃
不要迷信编译器没有bug。如果你在VS2003中测试下面的代码,会发现在release mode下面,程序会崩溃或者异常,但是在debug mode下工作正常。
例子程序
// The following code crashes/abnormal in release build when "whole program optimizations /GL"// is set. The bug is fixed in VS2005 #include <string>#pragma warning( push )#pragma warning( disable : 4702 ) // unreachable code in <vector>#include <vector>#pragma warning( pop )#include <algorithm>#include <iostream> //vcsig// T = float, U = std::cstringtemplate <typename T, typename U> T func_template( const U & u ){std::cout<<u<<std::endl;const char* str=u.c_str();printf(str);return static_cast<T>(0);} void crash_in_release(){std::vector<std::string> vStr; vStr.push_back("1.0");vStr.push_back("0.0");vStr.push_back("4.4"); std::vector<float> vDest( vStr.size(), 0.0 ); std::vector<std::string>::iterator _First=vStr.begin();std::vector<std::string>::iterator _Last=vStr.end();std::vector<float>::iterator _Dest=vDest.begin(); std::transform( _First,_Last,_Dest, func_template<float,std::string> ); _First=vStr.begin();_Last=vStr.end();_Dest=vDest.begin(); for (; _First != _Last; ++_First, ++_Dest)*_Dest = func_template<float,std::string>(*_First); } int main(int, char*){getchar();crash_in_release();return 0;} |
编译设定如下:
1. 取消precompiled header。2. 编译选项是: /O2 /GL /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_MBCS" /FD /EHsc /ML /GS /Fo"Release/" /Fd"Release/vc70.pdb" /W4 /nologo /c /Wp64 /Zi /TP。 |
跟踪汇编指令来分析
拿到这个问题后,首先在本地重现。根据下面一些测试和分析,认为很有可能是编译器的bug:
1. 程序中除了cout和printf外,没有牵涉到系统相关的API,所有的操作都是寄存器和内存上的操作。所以不会是环境或者系统因素导致的,可能性是代码错误(比如边界问题)或者编译器有问题。
2. 检查代码后没有发现异常。同时,如果调整一下std::transform的位置,在for loop后面调用的话,问题就不会发生。
3. 问题发生的情况跟编译模式相关。
代码中的std::transform和for loop的作用都是对整个vector调用func_template作转换。可以比较transform和for loop的执行情况进行比较分析,看看func_template的执行过程有什么区别。在VS2003里面利用main函数设定断点,停下来后用ctrl+alt+D进入汇编模式单步跟踪。下面的分析证明了这是编译器的bug:
在VisualStudio附带的STL源代码中,发现 std::transform的实现中用这样的代码来调用传入的转换函数:
*_Dest = _Func(*_First); |
编译器对于该代码的处理是:
EAX = 0012FEA8 EBX = 0037138C ECX = 003712BC EDX = 00371338 ESI = 00371338 EDI = 003712B0 EIP = 00402228 ESP = 0012FE70 EBP = 0012FEA8 EFL = 00000297 388: *_Dest = _Func(*_First);00402228 push esi00402229 call dword ptr [esp+28h] 0040222D fstp dword ptr [edi] |
ESI寄存器中保存的是需要传入_Func的参数*_First。可以看到,std::transform把这个参数通过push指令传入stack给_Func调用。
对于for loop中的*_Dest = func_templatefloatstd::string>(*_First);编译器是这样处理的:
EAX = 003712B0 EBX = 00371338 ECX = 003712BC EDX = 00000000 ESI = 00371338 EDI = 0037138C EIP = 00401242 ESP = 0012FE98 EBP = 003712B0 EFL = 0000029737: *_Dest = func_template<float,std::string>(*_First);00401240 mov ebx,esi 00401242 call func_template <float,std::basic_string<char,std::char_traits<char>,std::allocator<char> > > (4021A0h) 00401247 fstp dword ptr [ebp] |
可以看到,使用for loop的时候,参数通过mov指令保存到ebx寄存器中传入func_template调用。
最后,看一下func_template函数是如何来获取传入的参数的。
004021A0 push esi004021A1 push edi 16: std::cout<<u<<std::endl;004021A2 push ebx004021A3 push offset std::cout (414170h) 004021A8 call std::operator<<<char,std::char_traits<char>,std::allocator<char> > (402280h) |
这里直接把ebx推入stack,然后调用std::cout,并没有读取stack中的资料,说明func_template(callee)认为参数应该是从寄存器中传入的。然而transform函数(caller)却把参数通过stack传递。于是使用transform调用func_template的时候,func_template无法拿到正确的参数,因而导致崩溃。通过for loop调用的时候,由于参数通过寄存器传递,所以func_template就可以正常工作。
结论是编译器对参数的传入、读取、处理不统一,导致了这个问题。
为何问题在debug模式下不发生,或者调换函数次序后也不发生,留作你的练习吧 :-P
案例分析:臭名昭著的DLL Hell如何导致ASP.NET出现Server Unavailable
客户的ASP.NET程序,访问任何页面都报告Server Unavailable。观察发现,ASP.NET的宿主w3wp.exe进程每次刚启动就崩溃。通过调试器观察,崩溃的原因是访问了一个空指针。但是从call stack看,这里所有的代码都是w3wp.exe和 framework的代码,还没有开始执行客户的页面,所以跟客户的代码无关。通过代码检查,发现该空指针是作为函数参数从调用者(caller)传到被调用者(callee)的,当callee使用这个指针的时候问题发生。接下来应该检查caller为什么没有把正确的指针传入callee。
奇怪的时候,caller中这个指针已经正常初始化了,是一个合法的指针,调用call语句执行callee的以前,这个指针已经被正确地push到stack上了。为什么caller从stack上拿的时候,却拿到一个空指针呢?再次单步跟踪,发现问题在于caller把参数放到了callee的[ebp+8],但是callee在使用这个参数的时候,却访问[ebp+c]。是不是跟前一个案例很像?但是这次的凶手不是编译器,而是文件版本。Caller和callee的代码位于两个不同的DLL,其中caller是.NET Framework 1.1带的,而callee是.NET Framework 1.1 SP1带的。在.NET Framework 1.1中,callee函数接受4个参数,但是新版本SP1对callee这个函数作了修改,增加了1个参数。由于caller还使用SP1以前的版本,所以caller还是按照4个参数在传递,而callee按照5个参数在访问,所以拿到了错误的参数,典型的DLL Hell问题。在重新安装.NET Framework 1.1 SP1让两个DLL保持版本一致,重新启动后,问题解决。
导致DLL Hell的原因有很多。根据经验猜测版本不一致的原因可能是:
1. 安装了.NET Framework 1.1 SP1后没有重新启动,导致某些正在使用的DLL必须要等到重新启动后才能够完成更新。
2. 由于使用了Application Center做Load Balance,集群中的服务器没有做好正确的设置,导致系统自动把老版本的文件还原回去了:
PRB: Application Center Cluster Members Are Automatically Synchronized After Rebootinghttp://support.microsoft/kb/282278/en-us |
2.2.2 题外话和相关讨论
Release比 Debug快吗
分别在debug/release模式下运行下面的代码比较效率,会发现debug比release更快。你能找到原因吗?
long nSize = 200;char* pSource = (char *)malloc(nSize+1);char* pDest = (char *)malloc(nSize+1);memset(pSource, 'a', nSize);pSource[nSize] = '\0'; DWORD dwStart = GetTickCount();for(int i=0; i<5000000; i++){strcpy(pDest, pSource);}DWORD dwEnd = GetTickCount();printf("%d", dwEnd-dwStart); |
如果让你自己实现一个strcpy函数,应该考虑什么?你能做到比系统的strcpy函数快吗?
一些讨论可以参考:
http://eparg.spaces.live/blog/cns!59BFC22C0E7E1A76!1498.entry
从效率上说,起决定性作用的至少有下面两点:
1. 在32位芯片上,应该尽量每次mov一个DWORD,而不是4个byte来提高效率。注意到mov DWORD的时候要4字节对齐。
2. 这里对strcpy的调用高达5000000次。由于call指令的开销,使用内联 (inline) 版本的strcpy函数可以极大提高效率。
所以,汇编、CPU习性、操作系统和编译器,是分析细节的最直接武器。
上面例子中的strcpy是否内联,取决于编译设定。由于strcpy是CRT(CRuntime C运行库)函数,函数的实现位于MSVCRT.DLL或者MSVCRTD.DLL。如果编译设定使得函数调用要跨越DLL,这个函数是无法内联的。
关于性能的另外一些讨论:
http://eparg.spaces.msn/blog/cns!59BFC22C0E7E1A76!875.entry
2.3 理解操作系统对程序的反馈:异常(Exception)和通知(Debug Event)
本小结首先介绍异常的原理和相关资料,再举例说明异常跟崩溃和调试是如何紧密联系在一起的。最后说明如何利用工具来监视异常,获取准确的信息。
2.3.1 异常(Exception)的方方面面和一篇字字珠玑的文章
异常是CPU,操作系统和应用程序控制代码流程的一种机制。正常情况下,代码是顺序执行的,比如下面两行:
*p=11;printf(“%d”,*p); |
这里应该会打印出11。 但若p指向的地址是无效地址呢?那么这里对*p赋值的时候,也就是CPU向对应地址做写操作的时候,CPU就会触发无效地址访问的异常,接下来的printf很可能就不会执行了。
从这个简单的例子可以看到,当程序行为跟预期相左的时候,很可能就是异常的发生改变了程序的执行逻辑。在很多案例中,抓准异常的原因,其实就解决了问题。
异常发生的时候,由于操作系统在内核挂接了对应的CPU异常处理函数,CPU就会跳转去执行操作系统提供的处理函数,所以printf就不一定会被执行了。在操作系统的处理函数里面,如果检测到发生在用户态的程序的异常,操作系统会再把异常信息发送给用户态进程对应的处理函数,让用户态程序有处理异常的机会。
用户态程序处理完了异常,代码会继续执行,不过执行的次序可以是紧接着的下一个指令,比如printf,也可以跳到另外的地址开始执行
版权声明:本文标题:第二章排错的工具 调试器Windbg(下) 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726379298h948656.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论