也是最近被問的一個問題,全域變數在哪個階段初始化?
這個問題到沒被問倒,全域變數在mainCRTStartup之後main調用之前,在該階段應用會完成堆記憶體的申請(記得哪裡還看到如果改了EntryPoint需要自己進行堆記憶體的申請和管理).
而全域變數也正是在該階段完成的初始化.
然後又被問,那麼全域變數在哪裡被釋放?回答是在應用退出之後main函數退出之後,這個回答也沒問題.基本上算是正確的.
但是回頭自己仔細想想,那麼全域變數又是怎麼樣被初始化的呢?還真的有點不太清楚,所以出於好奇,今晚開始細細研究研究!
首先寫了一段代碼如下:
//標頭檔class ClassSizeRes{public:ClassSizeRes(void);~ClassSizeRes(void);};//cpp檔案ClassSizeRes::ClassSizeRes(void){}ClassSizeRes::~ClassSizeRes(void){}//main函數處理ClassSizeRes staticObj;int _tmain(int argc, _TCHAR* argv[]){ //...}
代碼大致如此,然後在建構函式處下斷點來調試,發現中斷之後的呼叫堆疊如下:
很顯然全域變數的初始化確實是在mainCRTStartup之後main調用之前,與之前所理解的確實沒有差別,但是編譯器又是如何處理的呢?
根據呼叫堆疊我們可以發現在函數_initterm的定義如下:
#ifdef CRTDLLvoid __cdecl _initterm (#else /* CRTDLL */static void __cdecl _initterm (#endif /* CRTDLL */ _PVFV * pfbegin, _PVFV * pfend ){ /* * walk the table of function pointers from the bottom up, until * the end is encountered. Do not skip the first entry. The initial * value of pfbegin points to the first valid entry. Do not try to * execute what pfend points to. Only entries before pfend are valid. */ while ( pfbegin < pfend ) { /* * if current table entry is non-NULL, call thru it. */ if ( *pfbegin != NULL ) (**pfbegin)();//這裡是關鍵,該函數就是遍曆調用無參的函數指標數組 ++pfbegin; }}
接下來這裡的函數指向的地址內容是關鍵:
這裡Pfbegin=0x00f5b30c
查看0x00f5b30c對應記憶體的內容
0x00F5B30C 00f57f60 00f57fc0 00f58020 (後面內容為00000000)
很顯然這裡是一個包含3個元素的數組,那麼關鍵就在於這3個元素指向的是什麼內容
00f57f60 地址的反組譯碼內容如下:
ClassSizeRes staticObj;
@0
00F57F60 push ebp
00F57F61 mov ebp,esp
00F57F63 sub esp,0C0h
00F57F69 push ebx
00F57F6A push esi
00F57F6B push edi
00F57F6C lea edi,[ebp-0C0h]
00F57F72 mov ecx,30h
00F57F77 mov eax,0CCCCCCCCh
00F57F7C rep stos dword ptr es:[edi]
00F57F7E mov ecx,offset staticObj (0F5E1E4h)
00F57F83 call ClassSizeRes::ClassSizeRes (0F51190h) //調用建構函式@1
00F57F88 push offset `dynamic atexit destructor for 'staticObj'' (0F590A0h)
00F57F8D call @ILT+190(_atexit) (0F510C3h)
00F57F92 add esp,4
00F57F95 pop edi
00F57F96 pop esi
00F57F97 pop ebx
00F57F98 add esp,0C0h
00F57F9E cmp ebp,esp
00F57FA0 call @ILT+625(__RTC_CheckEsp) (0F51276h)
00F57FA5 mov esp,ebp
00F57FA7 pop ebp
00F57FA8 ret
ClassSizeRes::ClassSizeRes:
@2
00F51190 jmp ClassSizeRes::ClassSizeRes (0F516E0h) @3
ClassSizeRes的建構函式
ClassSizeRes::ClassSizeRes(void)
{
@3
00F516E0 push ebp
00F516E1 mov ebp,esp
00F516E3 sub esp,0CCh
00F516E9 push ebx
00F516EA push esi
00F516EB push edi
00F516EC push ecx
00F516ED lea edi,[ebp-0CCh]
00F516F3 mov ecx,33h
00F516F8 mov eax,0CCCCCCCCh
00F516FD rep stos dword ptr es:[edi]
00F516FF pop ecx
00F51700 mov dword ptr [ebp-8],ecx
}
00F51703 mov eax,dword ptr [this]
00F51706 pop edi
00F51707 pop esi
00F51708 pop ebx
00F51709 mov esp,ebp
00F5170B pop ebp
00F5170C ret
如此看來全域變數的初始化過程如下
Step1:編譯器編譯之後會根據全域變數聲明來產生一些無參函數如上的@0
Step2:程式運行之後,__tmainCRTStartup會調用_initterm函數來調用編譯器產生的無參函數
(**pfbegin)();//函數指標,指向編譯器自動產生無參函數地址@
這裡的pfbegin-pfend都是指向的編譯器產生的全域變數初始化函數pfbegin
Step3:無參全域變數初始化函數pfbegin會調用各個類的建構函式完成對象初始化@1
Step3:@1會調用各類的建構函式存根地址(IAT存根地址)
Step4:@2 跳轉到建構函式實際實現地址完成對象的初始化
如此到了這一步基本上已經完成了一個全域變數的初始化.
那麼相應的釋放又是如何?呢?在解構函式中下斷點!發現呼叫堆疊如下:
很顯然實在doexit中調用了相應的解構函式來完成全域變數的析構
static void __cdecl doexit ( int code, int quick, int retcaller ){#ifdef _DEBUG static int fExit = 0;#endif /* _DEBUG */#ifdef CRTDLL if (!retcaller && check_managed_app()) { /* Only if the EXE is managed then we call CorExitProcess. Native cleanup is done in .cctor of the EXE If the Exe is Native then native clean up should be done before calling (Cor)ExitProcess. */ __crtCorExitProcess(code); }#endif /* CRTDLL */ _lockexit(); /* assure only 1 thread in exit path */ __TRY if (_C_Exit_Done != TRUE) { _C_Termination_Done = TRUE; /* save callable exit flag (for use by terminators) */ _exitflag = (char) retcaller; /* 0 = term, !0 = callable exit */ if (!quick) { /* * do _onexit/atexit() terminators * (if there are any) * * These terminators MUST be executed in reverse order (LIFO)! * * NOTE: * This code assumes that __onexitbegin points * to the first valid onexit() entry and that * __onexitend points past the last valid entry. * If __onexitbegin == __onexitend, the table * is empty and there are no routines to call. */ _PVFV * onexitbegin = (_PVFV *) DecodePointer(__onexitbegin); if (onexitbegin) { _PVFV * onexitend = (_PVFV *) DecodePointer(__onexitend); _PVFV function_to_call = NULL; /* save the start and end for later comparison */ _PVFV * onexitbegin_saved = onexitbegin; _PVFV * onexitend_saved = onexitend; while (1) { _PVFV * onexitbegin_new = NULL; _PVFV * onexitend_new = NULL; /* find the last valid function pointer to call. */ while (--onexitend >= onexitbegin && *onexitend == _encoded_null()) { /* keep going backwards. */ } if (onexitend < onexitbegin) { /* there are no more valid entries in the list, we are done. */ break; } /* cache the function to call. */ function_to_call = (_PVFV) DecodePointer(*onexitend);;//Decode之後指向編譯器產生的資源釋放處理代碼 /* mark the function pointer as visited. */ *onexitend = (_PVFV)_encoded_null(); /* call the function, which can eventually change __onexitbegin and __onexitend */ (*function_to_call)();//又是一個無參函數值得關注,調用編譯器產生資源釋放代碼,然後調用解構函式完成析構,與構造類似 onexitbegin_new = (_PVFV *) DecodePointer(__onexitbegin); onexitend_new = (_PVFV *) DecodePointer(__onexitend); if ( ( onexitbegin_saved != onexitbegin_new ) || ( onexitend_saved != onexitend_new ) ) { /* reset only if either start or end has changed */ onexitbegin = onexitbegin_saved = onexitbegin_new; onexitend = onexitend_saved = onexitend_new; } } }#ifndef CRTDLL /* * do pre-terminators */ _initterm(__xp_a, __xp_z);#endif /* CRTDLL */ }#ifndef CRTDLL /* * do terminators */ _initterm(__xt_a, __xt_z);#endif /* CRTDLL */#ifdef _DEBUG /* Dump all memory leaks */ if (!fExit && _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) & _CRTDBG_LEAK_CHECK_DF) { fExit = 1;#ifndef CRTDLL __freeCrtMemory(); _CrtDumpMemoryLeaks();#endif /* CRTDLL */ }#endif /* _DEBUG */ } /* return to OS or to caller */ __FINALLY if (retcaller) _unlockexit(); /* unlock the exit code path */ __END_TRY_FINALLY if (retcaller) return; _C_Exit_Done = TRUE; _unlockexit(); /* unlock the exit code path */ __crtExitProcess(code);}
詳細的內容就不多做重複,與構造類似,從
(*function_to_call)()->編譯器產生資源失敗處理代碼->調用到解構函式存根函數->跳轉到實際的解構函式地址執行資源釋放
如此基本上已經完成了全域變數資源的申請釋放.