------------------------------------------------------------------
大家好,我是Mike,微软拼音的开发工程师。在微软拼音的开发维护过程中,我们经常会遇到各种奇怪问题,很多都是客户机器那里出现,而在公司没有复现环境的,在这种情况下,如何调试解决问题是微软开发工程师必须具备的技能之一。下面就以一个崩溃问题做具体分析。
------------------------------------------------------------------
最近,我们的测试工程师从公司Dr. Watson (它是Windows下程序错误调试器,用于当程序发生错误时收集系统和程序信息,http://support.microsoft.com/kb/308538) 服务器上发现:非常频繁出现一种程序崩溃。输入法工作时,它只是一个动态链接库,运行在其他应用进程中,譬如,word,notepad,所以编程或设计时稍有地方考虑不周全,可能就会引起崩溃或其他错误。
感谢Windows的Error Report机制,让我们很及时了解到产品的问题。好,今天就分析一下这个崩溃的原因。Watson服务器上返回的具体错误报告如下:
错误应用程序名称: FATrayAlert.exe,版本: 2.4.7.1,时间戳: 0x4a42b696
错误模块名称: IMSCCORE.DLL,版本: 14.0.4734.1000,时间戳: 0x4b580fae
异常代码: 0xc00000fd
错误偏移量: 0x0005a7da
错误进程 ID: 0x90
错误应用程序启动时间: 0x01cae11f8ce3ff8c
错误应用程序路径: C:/Program Files (x86)/Sensible Vision/Fast Access/FATrayAlert.exe
错误模块路径: C:/PROGRA~2/COMMON~1/MICROS~1/IME14/IMESC/IMSCCORE.DLL
微软发布任何产品都有严格的流程,微软拼音输入法也一样,所以可以很快通过以下方法找出哪行代码出问题:
1. 从Watson 服务器上下载到crash时的dump文件
2. 用Windbg打开它,设置好这个版本的symbol路径。然后就可以看到call stack:
(Windbg是微软发布的一个超级强大的调试工具)
017efbb4 704a92fb IMxx!_chkstk+0x27 //crash
017efbc8 704ad697 IMSCCore!CIMExxx::OnConfigChange+0x2b
017efc54 704acb15 IMSCCore!CIMExxx::InitXXX+0x447
…
可以看到,程序崩溃在标注为红色函数中:_chkstk.
在MSDN查阅_chkstk函数,http://msdn.microsoft.com/en-us/library/ms648426(VS.85).aspx
如下:
_chkstk Routine
Called by the compiler when you have more than one page of local variables in your function.
Remarks
_chkstk Routine is a helper routine for the C compiler. For x86 compilers, _chkstk Routine is called when the local variables exceed 4K bytes; for x64 compilers it is 8K.
从上面说明可以看到,_chkstk这个函数只有在local变量超过4K(x86 OS),才会被编译器加入代码中。但是这个也应该没什么问题呀,因为在几乎所有微软拼音输入法的宿主程序中,这个函数都会被频繁调用到,而且都工作得好好的。什么原因呢?
虽然还不知道具体原因,但至少从上面的callstack看到,问题基本上是跟stack溢出有关。还是先看看OnConfigChange的代码吧,有些代码年代久了,说不定就有问题。
BOOL CIMExxx::OnConfigChanged(CIMEXXXConfig* pConfig)
{
...
SXXXConfig sCfg = {0};
...
}
计算了一下SStncConfig, sCfg是一个大约98K的对象. Oops,这么大的家伙呀,居然还在栈中分配的。是有些不合理,但是在正常wordpad和notepad用微软拼音,他们都没有问题呀。突然想起,VC连接器默认的栈是1M。默认?如果这个程序不是默认栈大小,是不是就有问题?
检查这个很容易,在windbg command窗口输入!teb, 这个命令显示thread environment block(TEB), 即线程环境信息。结果如下:
0:001> !teb
TEB at 7ffdd000
TEB的定义如下:
typedef struct _TEB {
NT_TIB NtTib;
PVOID EnvironmentPointer;
CLIENT_ID ClientId;
PVOID ActiveRpcHandle;
PVOID ThreadLocalStoragePointer;
PPEB ProcessEnvironmentBlock;
ULONG LastErrorValue;
.....
PVOID DeallocationStack;
.....
} TEB;
typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
.....
} NT_TIB;
我们关心的是StackBase和StackLimit, 通过他们可以知道剩余的栈空间。从上面看出,他们的偏移分别是+4, +8 字节。运行
0:001> dd 7ffdd000 L4
7ffdd000 017efc48 017f0000 017e1000 00000000
可见stackBase是017f0000, StackLimit是017e1000, 剩余空间是:
0:001> ? 17f0000-17e1000
Evaluate expression: 61440 = 0000f000
大概61K,不够分配上面98K的大对象L 原因知道了。但为求更安全(毕竟环境在实际客户那,我们分析的是一个瞬时镜像),我们可以进一步的验证:
0:001> dd 7ffdd000+E0C L1
7ffdde0c 017e0000
0:001> ? 17f0000-17e0000
Evaluate expression: 65536 = 00010000
E0C是存放最大栈空间的偏移。可以看到这个程序最大栈空间是64K。
当然可以更进一步验证,从本文开首的错误报告看到程序名称是FATrayAlert.exe。从网上搜索到它,这是一款识别应用程序,下载安装后,运行:
Dumpbin /headers FATrayAlert.exe
2800 size of stack reserve (hex)
1000 size of stack commit
可以看到10k左右,但从windbg分析栈空间应该是64K?原来操作系统会将做64K的向上圆整,所以小于64K的程序,运行时,操作系统都会给64K的栈空间。
原因找到了,修这个bug就简单了,将这个大对象放在heap中分配就可以了。