概述
感谢博主 http://book.51cto.com/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.com/
Intel Architecture Manual volume 1,2,3
http://www.intel.com/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,ebp 0040107C lea edi,[esp+10h] 25: for (j=0; j<wid; j++) 26: A[i*wid+j] = exp(-(i*i+j*j)); 00401080 mov ebx,ebp 00401082 xor esi,esi // The result of i*i is saved in ebx 00401084 imul ebx,ebp 00401087 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 eax 00401091 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 edi 00401099 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,4 0040109F cmp esi,7 004010A2 jl main+17h (00401087) 004010A4 inc ebp 004010A5 cmp ebp,4 004010A8 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::cstring template <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 esi 00402229 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 = 00000297 37: *_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 esi 004021A1 push edi 16: std::cout<<u<<std::endl; 004021A2 push ebx 004021A3 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和.net 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 Rebooting http://support.microsoft.com/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] = ' |