从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 // 检查栈末尾的值是否为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
call
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配置来说,这个选项默认就是”默认值“的,不用任何检查。
所以“基本运行时检查”功能应该只在开发阶段开启,来辅助我们避免写出糟糕的代码。