VC編譯選項“基本運行時檢查”的作用

從VS新建一個C++工程,Debug的配置中,“基本運行時檢查”這個選項默認值爲“兩者”,也就是同時包含“堆棧幀”和“未初始化的變量”檢查。

一、堆棧幀檢查

先將選項設置爲“默認值”,默認值意味着不檢查,寫一個這樣的函數

void test()
{
	int val[3];
	val[0] = 0x11111111;
	val[1] = 0x22222222;
	val[2] = 0x33333333;
	val[3] = 0;  // 越界
}

其彙編代碼大概如下

push ebp
mov ebp,esp
sub esp,4C
push ebx
push esi
push edi
mov eax,4
imul ecx,eax,0
mov dword ptr ss:[ebp+ecx-C],11111111
mov eax,4
shl eax,0
mov dword ptr ss:[ebp+eax-C],22222222
mov eax,4
shl eax,1
mov dword ptr ss:[ebp+eax-C],33333333
mov eax,4
imul ecx,eax,3
mov dword ptr ss:[ebp+ecx-C],0   // 越界寫入
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 

越界寫入前,棧空間內容是這樣的

00EFF760  11111111  
00EFF764  22222222  
00EFF768  33333333  
00EFF76C  00EFF7C0  // 在這段彙編中,這裏是保存的ebp值

越界寫入後

00EFF760  11111111  
00EFF764  22222222  
00EFF768  33333333  
00EFF76C  00000000  

因爲違規寫入了不屬於自己的棧空間,會導致內存數據異常,程序其他地方訪問了該地址就會引發內存錯誤

而出現這種內存錯誤的時候可能已經離BUG事發地很遠很遠了,排查BUG會耗費你大量的時間。

因此微軟在編譯器里加入了棧檢查的功能,這樣就能當場抓住BUG。讓我們把選項改爲“堆棧幀”後彙編如下

push ebp
mov ebp,esp
sub esp,D4
push ebx
push esi
push edi
lea edi,dword ptr ss:[ebp-D4]
mov ecx,35
mov eax,CCCCCCCC
rep stosd // 將棧空間初始化爲0xCCCCCCCC
mov eax,4
imul ecx,eax,0
mov dword ptr ss:[ebp+ecx-10],11111111
mov eax,4
shl eax,0
mov dword ptr ss:[ebp+eax-10],22222222
mov eax,4
shl eax,1
mov dword ptr ss:[ebp+eax-10],33333333
mov eax,4
imul ecx,eax,3
mov dword ptr ss:[ebp+ecx-10],0
push edx
mov ecx,ebp
push eax
lea edx,dword ptr ds:[<>]
call <dddd.@_RTC_CheckStackVars@8> // 檢查棧末尾的值是否爲0xCCCCCCCC
pop eax
pop edx
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 

再看這次開啓了檢查後的越界寫入前的棧空間

010FFCB0  11111111  
010FFCB4  22222222  
010FFCB8  33333333  
010FFCBC  CCCCCCCC  
010FFCC0  010FFD94  

越界寫入後

010FFCB0  11111111  
010FFCB4  22222222  
010FFCB8  33333333  
010FFCBC  00000000  
010FFCC0  010FFD94  

原理很簡單,加入了棧檢查後,初始化棧空間時會多4字節,並把所有內容填充爲0xCC,然後通過嵌入的RTC_CheckStackVars函數來檢查棧底的4字節是否爲0xCCCCCCCC,如果不是則會當場引發異常,省去了你排查出事點的時間。

二、未初始化變量檢查

寫測試代碼

void test()
{
	int val1;
	int val2 = val1;
}

彙編代碼

push ebp
mov ebp,esp
sub esp,48
push ebx
push esi
push edi
mov eax,dword ptr ss:[ebp-4]
mov dword ptr ss:[ebp-8],eax
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 

而打開檢查後的彙編如下

push ebp
mov ebp,esp
sub esp,4C
push ebx
push esi
push edi
mov byte ptr ss:[ebp-49],0
cmp byte ptr ss:[ebp-49],0
jne dddd.F1040
push <dddd.>
call <dddd.__RTC_UninitUse>
add esp,4
mov eax,dword ptr ss:[ebp-4]
mov dword ptr ss:[ebp-8],eax
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 

看上去也很簡單,對於未初始化的變量設爲0,並且判斷其等於0就通過嵌入的一個函數__RTC_UninitUse來引發異常

對於Release配置來說,這個選項默認就是”默認值“的,不用任何檢查。

所以“基本運行時檢查”功能應該只在開發階段開啓,來輔助我們避免寫出糟糕的代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章