windos调试工具系列
03-windows分析工具(depends定位动态库加载失败问题)
概述
之前上线的项目,在服务器的运行过程中发现会有缓慢的内存增长,程序本身没有主动去调用分配内存的API函数,所以初步猜测可能由于使用了第三方库导致了内存泄露,本文简单描述下如何使用windbg来定位windows程序中的内存泄露问题。
一、使用windbg排查内存泄露的步骤
对于一般的内存泄露问题,个人偏向于先在本地模拟测试,看能否重现内存泄露,如果本地总是能复现问题,可以采用代码注释等手段来进行测试,但是对于服务器上缓慢的内存泄露问题,可以根据dump文件或者gflags和umdh程序生成的内存快照进行对比分析,
1.使用dump文件分析
到服务器上创建一个dump文件,使用windbg设置pdb符号文件路径(参考:05-windbg工具定位内存问题),注意:程序的pdb文件一定要与程序对应。我平时在使用jenkins自动编译的时候,都会给对应版本的应用程序打包一个对应时间的pdb文件,
1.使用命令heap -s 显示内存使用情况
通过显示我们可以观察到,000002a054a20000处堆块占用了大量的内存,
2.!heap -stat -h 000002a054a20000 统计该地址堆内存块使用情况
使用该命令查看,该堆块的内存使用情况,在top 20的内存块中发现,
最多的一个是大小为0x10 字节,分配了0x892f4a次,0x10 * 0x892f4a/1024/1024 大约137M内存,与程序内存泄露的总量差不多,大约猜测在此处进行了内存泄露,
3.在符号文件中查找对应size的结构体
之前为了测试内存泄露,特意写了一个简单的程序,去pdb文件中查找对应大小的结构体名,0x10 刚好为16个字节,所以去pdb文件中查找大小为16字节的结构体或者类。
如下图所示:服务里面使用了ACE的定时器和另一个第三方的读取点的库(此库是由另一个厂家提供的,之前听他们开发人员提过,该库在高版本的编译器上可能有问题),所以对这两个先表示怀疑,
难道是ACE的定时器使用的不对?为了验证自己的猜测,加快定时器的频率,在本地进行代码测试,测试并未发现内存泄露,
因为没有读取点库的源代码,所以无法直接进行定位,所以通过此方法暂时无法确认程序大概的内存泄露的地方,这时候需要利用windbgs的gflags和umdh的程序来辅助判断
二.使用windbg的辅助工具定位内存泄露
经过上面简单的分析,暂时无法得出结论,所以需要使用windbgs的安装目录下的gflags.exe和umdh.exe完成从内存检测,以管理员权限运行cmd命令行窗口程序,切换到windbg目录,依次执行下面的命令,
1.设置pdb符号路径
umdh分析出使用堆内存的函数调用堆栈,为方便查看看函数调用堆栈中的具体函数,可以先设置符号的pdb文件路径,设置的变量名称必须为: _NT_SYMBOL_PATH.
#设置符号路径
set _NT_SYMBOL_PATH="c:\Symbols\;SRV*c:\Symbols\*http://msdl.microsoft.com/download/symbols"
2.调用gflags设置启动umdh的堆栈跟踪
gflags /i xxx.exe +ust
/i 用于指定进程的名称,参数+ust 给目标进程创建用户堆栈跟踪数据库,用于追踪内存的使用情况
2.第一次使用umdh抓取堆内存快照
umdh.exe -pn:xxx.exe -f:d:\mem1.log
参数-pn用来指定目标进程的进程名,-f参数用来指定存放抓取的堆内存使用快照的数据信息,运行完此命令后,第一次的堆内存快照就保存到d:\mem1.log中
3.等程序运行一段时间,发生内存泄露
在进行第一次的堆内存的快照抓取后,等程序运行一段时间,使程序发生内存泄露,然后再进行第二次抓取堆内存快照。
4.第二次使用umdh抓取堆内存快照
umdh.exe -pn:xxx.exe -f:d:\mem2.log
抓取第二次堆内存使用快照,保存到d:\mem2.log
5.比较两次堆内存快照
umdh.exe d:\mem1.log d:\mem2.log -f:d:\res.log
比较mem1.log和mem2.log两个文件中堆内存的使用变化量,得出统计数据保存到res.log中,即可查看到堆内存的变化情况,按堆内存的数量从高到低排列,而且有详细的函数的调用堆栈,分析使用量较高的几项即可,
我是在抓取第一次堆内存快照后,间隔了比较长的时间,抓取了第二次。
- 23fbeb0 ( 2409090 - d1e0) 240909 allocs BackTrace6A
- 23fbeb ( 240909 - d1e) BackTrace6A allocations
格式说明:- 23fbeb0 : 表示两次内存快照之间,增加的内存,
- 2409090:第二次使用内存快照时该堆占用内存,
- d1e0:第一次使用内存快照时该堆占用的内存
- 240909:总的分配次数
- 23fbeb:两次之间,多分配的次数
- 240909:第二次内存快照到第一次之间分配次数
- d1e:第一次的内存快照分配的次数
0x23fbeb0/0x23fbeb = 0x10 刚好16个字节,符合我们前面分析的内存泄露的字节大小,
从调用堆栈上看,内存泄露的地方像是在使用标准的std::string 中发生的,根据调用堆栈提示的代码行数,找到对应的代码部分。
代码的655行是对一个string类型的值进行赋值,查找该类型所属的结构体,发现了问题,以前在进行结构体数据清0的时候,使用了memset()函数对整个结构体清0,但是结构体中后来增加了std::string 类型,memset会把std::string 内存维护的指针清空,所以等在给std::string赋值的时候,又需要重新分配内存,所以该处导致了内存泄露,所以即使程序里面没有动态申请内存,这个地方也是导致了内存泄露,
总结
1.注意事项
对于含有string类型的结构体对象,不能使用了memcpy、memmove、memset函数操作其内存,否则会导致一些程序的异常和内存泄露问题,本程序中的std::string 使用的字符串都比较小,小于std::string 的预设的16个字节,所以每声明一次该对象,就发生一次内存泄露。
2.为了简化生成堆内存的快照步骤,使用脚本
如果所示意:为了简化操作步骤,将windbg程序打包,写了脚本命令,按照顺序使用管理员权限运行1,2,3,4脚本命令,在set_NT_SYMBOL_PATH.bat中修改保存路径和要检测的程序名称和pdb符号路径
set_NT_SYMBOL_PATH.bat
rem 设置pdb文件路径和要捕捉的进程的名称 并设置输出文件
set currsymbolpath=%~dp0
rem 设置符号路径 根据实际需求设置
set _NT_SYMBOL_PATH="c:\Symbols\"
set _NT_SYMBOL_PATH=%_NT_SYMBOL_PATH%;%currsymbolpath%
rem 设置windb到环境变量
set PATH=%PATH%;%currsymbolpath%\windbg_x64
rem 设置要捕捉的进程的名称
set PROCESS=mem.exe
rem 设置输出的文件
set OUT1=%currsymbolpath%\mem1.log
set OUT2=%currsymbolpath%\men2.log
set OUTRES=%currsymbolpath%\res.log
echo %_NT_SYMBOL_PATH%
1.gflags.bat
set CURRDIR=%~dp0
call %CURRDIR%\set_NT_SYMBOL_PATH.bat
gflags -i %PROCESS% +ust
pause
2.umdh.bat
set CURRDIR=%~dp0
call %CURRDIR%\set_NT_SYMBOL_PATH.bat
umdh.exe -pn:%PROCESS% -f:%OUT1%
pause
3.umdh.bat
set CURRDIR=%~dp0
call %CURRDIR%\set_NT_SYMBOL_PATH.bat
umdh.exe -pn:%PROCESS% -f:%OUT2%
pause
4.res.bat
set CURRDIR=%~dp0
call %CURRDIR%\set_NT_SYMBOL_PATH.bat
umdh.exe %OUT1% %OUT2% -f:%OUTRES%
pause