1. 项目场景
本故事纯属虚构。
初入职场的小木,负责维护一个博客系统,后端采用C++编写,部署在Windows服务器上。刚刚熟悉完产品的小木,接到了后台服务的报警,服务器后端偶尔会程序崩溃。刚开始小木还有点慌张,脑子里面浮现出各种问题,这个是程序的bug吗?茫茫的代码如何寻找问题?log能看到线索吗?当冷静下来后,小木忽然想起前几天看的两篇文章<<Windbg调试----Windbg入门>>和<<Windows程序Dump收集>>,还没动手过呢,正好练习练习。
2. 收集Dump
首先小木想到了要定位到问题,得收集Dump。blogserver程序是64位程序,小木决定采用procdump64去收集dump。于是在产品服务器上运行了如下的命令, 将程序产生的dump生成到C:\dumps
目录下。
procdump.exe -ma blogserver.exe -t -e -o C:\dumps
接下小木就是一边review以前的代码,一边等待着Crash的出现。终于有一天出现啦,procdump64输出了如下的信息:
[15:34:17] Exception: C0000005.ACCESS_VIOLATION
[15:34:17] Unhandled: C0000005.ACCESS_VIOLATION
[15:34:17] Dump 1 initiated: C:\dumps\blogserver.exe_201115_153417.dmp
[15:34:17] Dump 1 writing: Estimated dump file size is 42 MB.
[15:34:17] Dump 1 complete: 42 MB written in 0.2 seconds
[15:34:17] Dump count reached.
ACCESS_VIOLATION
看来是访问了不可访问的内存,估计做过C++代码编写的程序员都碰到过这种内存访问问题。 小木将程序dump拷贝到了自己的办公机器上,准备用预先安装好的Windbg64位进行分析。
3. Windbg分析
小木根据之前学习的内容,先用Windbg 加载dump。用k
查看crash的堆栈,因为没有加载产品的符号信息,函数调用栈,没有显示出哪个函数调用导致程序crash了。这里补充一句,默认的产品发布采用Visual Studio Release模式发布,这个模式产品的符号信息将采用.pdb
文件单独保存,保证自己的符号信息不被泄露。
3.1 符号信息加载
- 小木先将之前产品的符号信息
blogserver.pdb
拷贝到调试机器的C:\blogserversymbols
目录下 - 创建一个微软的symbols的缓存目录
C:\windowssymbols
, 一般windows程序会加载很多微软的dll,而在分析crash的时候,也需要加载微软的symbols - 使用命令添加产品的symbols目录:
.sympath c:\blogserversymbols
- 使用命令添加了微软的symbols,并保存到指定的目录:
.sympath+ srv*C:\windowssymbols*http://msdl.microsoft.com/download/symbols
- 运行重新加载symbols:
.reload
以上的配置也可以保存到workspace中,以便下次继续使用。
3.2 寻找程序崩溃的代码
加载完symbols后,我们来看下程序调用栈:
0:000> k
# Child-SP RetAddr Call Site
00 00000001`08b2f4e8 00007ffb`62c05237 ucrtbase!strnlen+0x3c
01 00000001`08b2f4f0 00007ffb`62bebf65 ucrtbase!cgetws_s+0x3b37
02 00000001`08b2f520 00007ffb`62beaa7c ucrtbase!_stdio_common_vfwscanf+0x3b15
03 00000001`08b2f570 00007ffb`62be9e7a ucrtbase!_stdio_common_vfwscanf+0x262c
04 00000001`08b2f5a0 00007ffb`62be863b ucrtbase!_stdio_common_vfwscanf+0x1a2a
05 00000001`08b2fac0 00007ffb`62beead1 ucrtbase!_stdio_common_vfwscanf+0x1eb
06 00000001`08b2faf0 00007ff6`78aa1c9f ucrtbase!_stdio_common_vfprintf+0x81
07 00000001`08b2fb60 00007ff6`78aa1cf8 blogserver!_vfprintf_l+0x3f [c:\program files (x86)\windows kits\10\include\10.0.18362.0\ucrt\stdio.h @ 644]
08 00000001`08b2fba0 00007ff6`78aa15b3 blogserver!fprintf+0x48 [c:\program files (x86)\windows kits\10\include\10.0.18362.0\ucrt\stdio.h @ 839]
09 00000001`08b2fbf0 00007ff6`78aa1d59 blogserver!LogStr+0x33 [c:\personal\test\blogserver\blogserver.cpp @ 10]
0a 00000001`08b2fc30 00007ff6`78aa2054 blogserver!main+0x39 [c:\personal\test\blogserver\blogserver.cpp @ 16]
0b (Inline Function) --------`-------- blogserver!invoke_main+0x22 [d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]
0c 00000001`08b2fc90 00007ffb`653f4034 blogserver!__scrt_common_main_seh+0x10c [d:\agent\_work\2\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
0d 00000001`08b2fcd0 00007ffb`664a3691 kernel32!BaseThreadInitThunk+0x14
0e 00000001`08b2fd00 00000000`00000000 ntdll!RtlUserThreadStart+0x21
小木松了一口气,终于有点线索了,程序崩溃在函数LogStr
,根据里面的行数提示,找到那段代码:
void LogStr(std::string strContent)
{
fprintf(stdout, strContent.c_str());
}
刚松了一口气,小木又疑惑起来,这个函数是用来打印博客标题的log的,一直都用,也测试过,怎么会偶尔导致程序崩溃呢? 小木睁大眼睛,(也许在读文章的你已经知道到什么问题了),作为新手,就这么一行代码,还是没有找到原因。
3.3 真相大白
小木接着看,是不是这个log内容比较奇特呢,决定先看一看strContent的内容。
小木切到fprintf那个frame:
0:000> .frame 08
08 00000001`08b2fba0 00007ff6`78aa15b3 blogserver!fprintf+0x48 [c:\program files (x86)\windows kits\10\include\10.0.18362.0\ucrt\stdio.h @ 839]
然后再查看当前的参数内容,知道了strContent
是Hello %sWindbg!
0:000> dv /t
struct _iobuf * _Stream = 0x00007ffb`62c584b8
char * _Format = 0x00000001`08b2fc60 "Hello %sWindbg!"
char * _ArgList = 0x00000001`08b2fc00 "Hello %sbg"
int _Result = 0n145947744
当看到%s
的时候,小木的编程直觉,瞬间反应过来了,这个%s
是格化式字符串。而这句话fprintf(stdout, strContent.c_str());
却将需要打印的strContent内容直接放在fprintf
的_Format
参数中, 这样%s
这个格式化串,将认为存在一个字符串参数,其实并没有,这样读取到的地址,将可能会出现ACCESS_VIOLATION
.
真相大白了,小木身心舒适,程序员的成就感涌上心头,对着空气微微一笑。小木知道以后还会碰到棘手的问题去处理,所以决定继续努力学习,所谓“台上一分钟,台下十年功”。
最后是个人微信公众号,文章CSDN和微信公众号都会发,欢迎一起讨论。