2.2.1 需要用匯編來排錯的常見情況
彙編是CPU執行指令的最小單元。下面一些情況下,彙編級別的分析通常是必要的:
1. 閱讀代碼看不出問題,但是跑出來的結果就是不對,懷疑編譯器甚至CPU有毛病。
2. 沒有源代碼可以閱讀。比如,調用某一個API的時候出問題,沒有Windows的源代碼,那就看彙編。
3. 當程序崩潰,訪問違例的時候,調試器裏看到的直接信息就是彙編。
調試中涉及的彙編知識分爲兩部分:
1. 寄存器的運算,對內存地址的尋址和讀寫。這部分是跟CPU本身相關的。
2. 函數調用時候堆棧的變化,局部變量全局變量的定位,虛函數的調用。這部分是跟編譯器相關的。
彙編的知識可以在大學計算機教程裏面找到。建議先熟悉簡單的8086/80286的彙編,再結合IA32芯片結構和32位Windows彙編知識深入。建議的資源:
AoGo彙編小站
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] = '/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.com/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,這個函數是無法內聯的。
關於性能的另外一些討論: