背景:在项目 debug 过程中遇到 failwithmessage 函数,但是程序正常运行,判断这个函数应该是类似 exception 之类的功能。查看调用栈,发现是 _RTC_CheckStackVars(...) 引起 failwithmessage 的调用。网上查到这个函数是检查栈变量完整性,很可能程序当中已经发生了访问越界。项目稳定性的要求比较高,需要捉一下虫子。
通过本文可以了解:
1. RTC API (run time check) 的基本原理。
2. 实际分析调用栈的形式。
3. 初步探索windbg调试的一些方法。
代码:猜测是访问越界导致,所以单写了代码越界的代码调试用
- int main()
- {
- int a[3] = { 0 };
- int b[3] = { 0 };
- a[3] = 3;
- return 0;
- }
编译,并利用windbg调试,果然出现同样的函数调用栈。第一行是我们可以通过windbg查看的关于调用栈的所有信息。这里选了Addrs和Source args。第一列是每一个函数调用栈栈顶地址。函数名紧接着传进函数的参数的值。我们的调试函数是wmain,单步跟踪可以发现,在wmain完全退出之前(位于renturn之后)调用了_RTC_CheckStackVars。这是一个编译器行为。
图中高亮的函数是我们需要仔细分析的函数。通过搜索visual studio安装文件夹,找到了对应的定义文件。
C:\Program Files\Microsoft Visual Studio 12.0\VC\include\rtcapi.h
- /* These unsupported functions are deprecated in native mode and not supported at all in /clr mode */
- _RTCINTERNAL_DEPRECATED void __fastcall _RTC_CheckStackVars(void *_Esp, _RTC_framedesc *_Fd);
- /* Compiler generated calls (unlikely to be used, even by power users) */
- /* Types */
- typedef struct _RTC_vardesc {
- int addr;
- int size;
- char *name;
- } _RTC_vardesc;
- typedef struct _RTC_framedesc {
- int varCount;
- _RTC_vardesc *variables;
- } _RTC_framedesc
首先查看_RTC_framedesc。0x0002代表检查的栈中变量的个数。0x00281434代表变量信息存储的地址。
接下来查看地址0x00281434以获得变量的信息。
第一个变量的偏移地址是0xffffffec,大小是0x0c,名称存储在0x28144e,可以看到是a(0x61)。第二个变量偏移地址是0xffffffd8,大小是0x0c,名字存储在0x28144c,b(0x62)。最后要找到出错的内存地址:栈顶地址+偏移量,得到是0x16faf8
高地址对应是a的内存,低地址是b。可以看出,对于debug版本,VS不止会为变量分配空间,而且给变量周围包围4个字节的boudary,均初始化成0xcccccccc。但读写越界的时候,这四个字节中的数据被改写,_RTC_CheckStackVars就会发现异常,并调用failwithmessage函数。而且查看该函数的栈空间,会发现Stack arround the variable 'a' corrupted字样。对应图中0x16fb04位置的内存已经从0xcccccccc改成0x03。
最后可以利用上述信息定位错误。重头开始运行程序,在进入main函数入口添加断点。查看变量a的地址是0x163f24,所以a[3]的地址是0x163f30。添加断点
ba w4 0x163f30
表示从0x163f30开始四个字节内的内存被写入时,进入断点。运行程序发现断点停在a[3] = 3。由此我们定位到了访问越界的根源。
相关的知识:
1. windbg的几种断点:bu - defferred breakpoints 每次会根据符号去计算断点地址。因此即使相应的模块还没有被加载,也可以通过它来设置断点。
bp - original breakpoints 通过地址设置断点。
ba - data breakpoints 可以针对指定内存的读写操作(另外一种是对指定代码)设置断点。
bm - set breakpoints using symbol pattern 在设置符号断点的时候可以利用通配符。比如在一个类的成员函数入口都设置断点,bm module!class::*。
2. 为了验证猜想,修该代码为a[3] = 0xcccccccc。这个时候_RTC_CheckStackVars不再报错。
3. 在VS中关闭RTC(runtime check)。
/RTC1 /RTCu /RTCc /RTCs - 编译器开关。
#pragma runtime_checks( "[runtime_checks]", {restore | off} ) - 宏编译开关。