變量類型以及作用域和生命周期
變量的作用域
變量的作用域就該變量可以被訪問的區間,變量的作用域可以分為以下四種:
- 進程作用域(全局):在當前進程的任何一個位置都可以訪問
- 函數作用域:當流程轉移到函數后,在其開始和結束的花括號內可訪問
- 塊作用域:最常見的就是if(...){...},while(..){...},類似這種,
塊內部可以訪問 - 文件作用域:在當前源碼文件內可以被訪問
變量的生命周期
變量的生命周期就是從創建該變量開始到該變量被銷毀的這一段時間,
各種變量的生命周期:
- 全局變量:進程開始時創建,進程結束時銷毀,在代碼編譯鏈接后,直接將
其初始值寫入到可執行文件中,創建時按照定義時的初始值進
行賦值 - 局部變量和參數變量:進入函數時創建,退出函數時銷毀
- 全局靜態變量:定義一個全局變量並使用static關鍵字修飾時,這個變量
就成了全局靜態變量,它的生命周期和全局變量一樣,但是
作用域被限制在定義文件內,無法使用extern來讓其他源
文件中使用它 - 靜態局部變量:在函數內使用static關鍵字修飾一個變量時,這個變量就
是靜態局部變量,它的生命周期同全局變量一樣,作用域被
限制在函數內 - 寄存器變量:在VC++的Debug版本中,寄存器變量和普通變量沒區別,在
Release版本中VC++編譯器會自動優化,即使一個變量不是
寄存器變量也有可能放到寄存器中,所以register關鍵字對
於VC++編譯器來說只是個建議
各種變量和常量的小實驗
- 全局常量
編寫對全局常量賦值的代碼會導致編譯時報錯,現在我用指針指向它的地址,
然后在向它賦值,看看這種猥瑣的方式是否能成功:
可以看出編譯時能混過去,但是運行時報錯,這是因為全局常量保存在數據區
的常量區中,常量區的內存屬性為只讀,如果向只讀內存寫入數據則會引發錯誤 - 局部常量和參數常量
可以看出局部常量和參數常量都在棧上,只是在編譯時檢查是否被賦值,運行時
還是可以猥瑣修改 - 全局常量,局部常量,參數常量,全局變量,全局靜態變量,靜態局部變量的生命周期:
在程序入口點mainCRTStartup下函數點,程序停在這里,此時程序剛剛int g_Test1 = 3; const int g_Test2 = 4; static int g_Test3 = 5; void TestConstVar(const int nTest1) { static int nTest4 = 8; const int nTest = 1; int* pTest = (int*)&nTest; *pTest = 2; pTest = (int*)&nTest1; *pTest = 9; } int main() { TestConstVar(3); return 0; }
建立,main函數還沒有被執行:
可以看出g_Test1,g_Test2,g_Test3都可以在"監視"窗口中查看
main函數退出后g_Test1,g_Test2,g_Test3依舊存在
局部常量和參數常量在保存在棧上,但靜態局部變量因為只做一次初始化的
原因所以它也被保存在數據區,在實驗的過程中發現了之前的VS2013以及之前
的版本的編譯器在初始化靜態局部變量是線程不安全的,對比如下:-
VS2013:
從源碼對應的匯編語言可以看出,VC++編譯器為了做到靜態局部變量
只被初始化一次,所以使用了標記變量,只要發現標記變量沒有被置位,
那么會先進行置位,然后在進行初始化,但是這在多線程環境中是不安全的,
當兩個線程同時調用靜態局部變量所在的函數時,會出現兩個線程在沒有同
步機制的情況下操作同一個變量,在我這個簡單代碼中,靜態局部變量的類型
是整型,所以看起來沒啥太大危害,但是如果靜態局部變量的類型是一個類,
那么構造函數極有可能發生一個線程,剛剛置標記位還沒構造完成,接着另一個
線程也調用了該函數,這個線程發現標記位被置位了,然而此時對象的構造還未
完成,如果該線程就執行剩下的代碼,那么極有可能發生錯誤,而且極難排查 -
VS2015:
我在測試程序中創建另一個線程,以便觀察:int g_Test1 = 3; const int g_Test2 = 4; static int g_Test3 = 5; void TestConstVar(const int nTest1) { static int nTest4 = nTest1; nTest4 += 1; const int nTest = 1; int* pTest = (int*)&nTest; *pTest = 2; pTest = (int*)&nTest1; *pTest = 9; } unsigned __stdcall startaddress(void *) { TestConstVar(3); printf("333"); return 0; } int main() { TestConstVar(3); uintptr_t ret = _beginthreadex(NULL, 0, startaddress, NULL, 0, NULL); system("pause"); return 0; }
TestConstVar函數完整的反匯編代碼:
void TestConstVar(const int nTest1) { 011F1760 push ebp 011F1761 mov ebp,esp 011F1763 sub esp,0DCh 011F1769 push ebx 011F176A push esi 011F176B push edi 011F176C lea edi,[ebp-0DCh] 011F1772 mov ecx,37h 011F1777 mov eax,0CCCCCCCCh 011F177C rep stos dword ptr es:[edi] 011F177E mov eax,dword ptr [__security_cookie (011FA014h)] 011F1783 xor eax,ebp 011F1785 mov dword ptr [ebp-4],eax static int nTest4 = nTest1; 011F1788 mov eax,dword ptr [_tls_index (011FA194h)] 011F178D mov ecx,dword ptr fs:[2Ch] 011F1794 mov edx,dword ptr [ecx+eax*4] 011F1797 mov eax,dword ptr ds:[011FA154h] 011F179C cmp eax,dword ptr [edx+104h] 011F17A2 jle TestConstVar+6Fh (011F17CFh) 011F17A4 push 11FA154h 011F17A9 call __Init_thread_header (011F104Bh) 011F17AE add esp,4 011F17B1 cmp dword ptr ds:[11FA154h],0FFFFFFFFh 011F17B8 jne TestConstVar+6Fh (011F17CFh) 011F17BA mov eax,dword ptr [nTest1] 011F17BD mov dword ptr [nTest4 (011FA150h)],eax 011F17C2 push 11FA154h 011F17C7 call __Init_thread_footer (011F10E1h) 011F17CC add esp,4 nTest4 += 1; 011F17CF mov eax,dword ptr [nTest4 (011FA150h)] 011F17D4 add eax,1 011F17D7 mov dword ptr [nTest4 (011FA150h)],eax const int nTest = 1; 011F17DC mov dword ptr [nTest],1 int* pTest = (int*)&nTest; 011F17E3 lea eax,[nTest] 011F17E6 mov dword ptr [pTest],eax *pTest = 2; 011F17E9 mov eax,dword ptr [pTest] 011F17EC mov dword ptr [eax],2 pTest = (int*)&nTest1; 011F17F2 lea eax,[nTest1] 011F17F5 mov dword ptr [pTest],eax *pTest = 9; 011F17F8 mov eax,dword ptr [pTest] 011F17FB mov dword ptr [eax],9 } 011F1801 push edx 011F1802 mov ecx,ebp 011F1804 push eax 011F1805 lea edx,ds:[11F1830h] 011F180B call @_RTC_CheckStackVars@8 (011F128Fh) 011F1810 pop eax 011F1811 pop edx 011F1812 pop edi }
從上述反匯編代碼中可以看出VS2015對靜態變量的初始化與VS2013完全不一樣,
編譯器插入了這兩個函數:__Init_thread_header,__Init_thread_footer,
從VS2015的安裝目錄下:VS2015\VC\crt\src\vcruntime的thread_safe_statics.cpp,
源文件中找到了這兩個函數的源碼和這兩個函數中引用到的變量:int const Uninitialized = 0; int const BeingInitialized = -1; int const EpochStart = INT_MIN; extern "C" { int _Init_global_epoch = EpochStart; __declspec(thread) int _Init_thread_epoch = EpochStart; } extern "C" void __cdecl _Init_thread_header(int* const pOnce) { _Init_thread_lock(); if (*pOnce == Uninitialized) { *pOnce = BeingInitialized; } else { while (*pOnce == BeingInitialized) { // Timeout can be replaced with an infinite wait when XP support is // removed or the XP-based condition variable is sophisticated enough // to guarantee all waiting threads will be woken when the variable is // signalled. _Init_thread_wait(XpTimeout); if (*pOnce == Uninitialized) { *pOnce = BeingInitialized; _Init_thread_unlock(); return; } } _Init_thread_epoch = _Init_global_epoch; } _Init_thread_unlock(); } // Called by the thread that completes initialization of a variable. // Increment the global and per thread counters, mark the variable as // initialized, and release waiting threads. extern "C" void __cdecl _Init_thread_footer(int* const pOnce) { _Init_thread_lock(); ++_Init_global_epoch; *pOnce = _Init_global_epoch; _Init_thread_epoch = _Init_global_epoch; _Init_thread_unlock(); _Init_thread_notify(); } extern "C" void __cdecl _Init_thread_lock() { EnterCriticalSection(&_Tss_mutex); }
從反匯編代碼中可以看出調用_Init_thread_footer,和_Init_thread_header時,前面都會有
011F17C2 push 11FA154h,這行代碼是將與靜態變量關聯的標記變量的地址作為參數
傳遞,在_Init_thread_footer中先調用_Init_thread_lock函數進入臨界區,確保在當前線程
獨占此標記變量,進入臨界區后判斷此標記變量的值是否為Uninitialized(值為0,表示靜態局部
變量未被初始化),如果標記變量為0,那么則將標記變量置為BeingInitialized(值為-1,表示該
變量正在被初始化),然后當前線程調用_Init_thread_unlock函數釋放臨界區,退出_Init_thread_footer
函數,流程轉移到TestConstVar函數中進行靜態局部變量的初始化,如果在此時緊接着又有好幾個線程同
時調用TestConstVar函數,假設此時靜態局部變量還咩有初始化完成,那么后來的線程就會進入
_Init_thread_header中,然后發現與該靜態變量關聯的標記變量已經被置為BeingInitialized
那么這些線程則會進入到_Init_thread_header的else分支中,然后在else分支的while循環中
等待當前正在初始化靜態局部變量的線程完成初始化,那么現在來看看這些線程是如何等待的:static decltype(SleepConditionVariableCS)* encoded_sleep_condition_variable_cs; extern "C" bool __cdecl _Init_thread_wait(DWORD const timeout) { if (_Tss_event == nullptr) { return __crt_fast_decode_pointer(encoded_sleep_condition_variable_cs)(&_Tss_cv, &_Tss_mutex, timeout) != FALSE; } else { _ASSERT(timeout != INFINITE); _Init_thread_unlock(); HRESULT res = WaitForSingleObjectEx(_Tss_event, timeout, FALSE); _Init_thread_lock(); return (res == WAIT_OBJECT_0); } }
_Tss_event只有在XP系統下才不為空,因為XP系統不支持條件變量,所以只能用WaitForSingleObjectEx
來模擬條件變量,這里的encoded_sleep_condition_variable_cs是函數指針,這行代碼:
__crt_fast_decode_pointer(encoded_sleep_condition_variable_cs)(&_Tss_cv, &_Tss_mutex, timeout),就是在調用SleepConditionVariableCS,然后睡眠timeout(100ms),在睡眠的期間會釋放
_Tss_mutex,超時或者醒來時在重新進入臨界區_Tss_mutex。當前線程初始化完成后會調用_Init_thread_footer:
_Init_thread_lock(); ++_Init_global_epoch; *pOnce = _Init_global_epoch; _Init_thread_epoch = _Init_global_epoch; _Init_thread_unlock(); _Init_thread_notify();
正是因為那些后來等待的線程調用SleepConditionVariableCS時會釋放臨界區,所以_Init_thread_footer
中調用_Init_thread_lock()不會卡在這里,當前線程進入臨界區后,那些在_Init_thread_wait
中調用SleepConditionVariableCS函的線程將會卡在這個函數中,因為_Tss_mutex臨界區被當前線程
所占有;++_Init_global_epoch則是累加全局計數器,然后將全局計數器的值賦值給標記變量,而每個線程
都有一個計數器(_Init_thread_epoch),全局計數器的值也被賦值給當前線程的計數器,至此標記變量和
計數器都已賦值完成,此時在調用_Init_thread_unlock釋放臨界區,然后在調用_Init_thread_notify:static decltype(WakeAllConditionVariable)* encoded_wake_all_condition_variable; extern "C" void __cdecl _Init_thread_notify() { if (_Tss_event == nullptr) { __crt_fast_decode_pointer(encoded_wake_all_condition_variable)(&_Tss_cv); } else { SetEvent(_Tss_event); ResetEvent(_Tss_event); } }
從上面代碼可以看出在非XP系統下,調用WakeAllConditionVariable喚醒所有陷入睡眠的線程,
在XP系統下使用SetEvent和ResetEvent喚醒等待線程,醒來的線程發現while循環中的條件
*pOnce == BeingInitialized不成立,則退出_Init_thread_header函數,返回到TestConstVar
函數,然后進行如下判斷:011F17B1 cmp dword ptr ds:[11FA154h],0FFFFFFFFh 011F17B8 jne TestConstVar+6Fh (011F17CFh)
發現與靜態局部變量關聯的標記變量已經不是BeingInitialized,則說明該靜態局部變量已經被
其他線程初始化了,則跳過靜態局部變量的初始化代碼。現在回過頭解釋下反匯編中的第一個判斷語句:
011F1788 mov eax,dword ptr [_tls_index (011FA194h)] 011F178D mov ecx,dword ptr fs:[2Ch] 011F1794 mov edx,dword ptr [ecx+eax*4] 011F1797 mov eax,dword ptr ds:[011FA154h] 011F179C cmp eax,dword ptr [edx+104h] 011F17A2 jle TestConstVar+6Fh (011F17CFh)
這里前三行代碼從局部線程存儲中取出的一個值與靜態局部變量對應的標記變量進行比較,
根據_Init_thread_epoch變量的聲明可以判斷出取出的值就是_Init_thread_epoch,
_Init_thread_epoch初值被置為EpochStart(一個負數),而標記變量未完成初始化時的
值是0,比_Init_thread_epoch大,所以jle指令不滿足跳轉條件,后續的靜態變量初始化
代碼得以執行;靜態變量初始化完成后標記變量被置為_Init_thread_epoch(++_Init_global_epoch),
所以標記變量時小於或者等於_Init_thread_epoch,jle指令跳轉條件成立,靜態局部的
初始化代碼全部跳過.At Last: 這個靜態局部變量初始化bug經歷了將近20年才被修復,我也是偶然間觀察VS2013和
VS2015生成的二進制代碼的反匯編代碼才發現這事,同時也順帶學會了條件變量的使用。
-