猿创征文|C++软件开发值得推荐的十大高效软件分析工具
Posted dvlinker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了猿创征文|C++软件开发值得推荐的十大高效软件分析工具相关的知识,希望对你有一定的参考价值。
目录
作为C++软件开发人员,有没有思考过这些问题:如何提升日常工作中的开发效率?如何才能快速地分析并解决开发调试过程中遇到的各类问题?有哪些值得推荐的高效软件工具可以使用?作为从业多年的C++开发老兵,根据多年来的开发经历及取得的实战经验,今天给大家推荐十大高效C++软件分析工具。我们需要熟练使用这些工具,借助这些工具,可以高效快速地分析和排查软件开发调试过程中遇到的各种问题,从而有效地提升软件开发和维护的效率。
1、概述
本文将以Windows系统中C++开发的应用软件为主线,对本文主体内容进行展开。运行在Windows系统中的应用软件主要是用C++、C#等语言开发的,其中的大部分都是用C++实现的。比如大家平时常用的Google Chrome浏览器、腾讯会议、PC版微信和QQ、企业微信、阿里钉钉、字节飞书、华为WeLink、百度网盘客户端、有道云笔记、PC版QQ音乐、PC版腾讯视频、酷狗音乐等软件都是用C++开发实现的。
在这些C++软件开发和维护的过程中,会用到一些高效的软件分析工具去辅助分析和排查软件运行中遇到的各式各样的问题,今天就把我在开发调试过程中用的一些列工具分享给大家。我日常使用的分析工具有SPY++、Dependency Walker、剪切板查看工具Clipbrd、 GDI对象查看工具GDIView、Process Explorer、Process Monitor、API Monitor、调试分析工具Windbg、交互式反汇编工具IDA、抓包工具Wireshark等。本文将结合具体的案例及使用场景,对这些工具一一进行详细的介绍。
通过公司内部培训交流以及网上技术沟通得知,很多C++软件开发人员都没有用过GDIView、Process Explorer、Process Monitor、API Monitor、Windbg、IDA等高效分析工具,甚至都没听说过,他们排查和分析问题的方式还停留在用IDE调试和查看打印日志的老方法上,效率相对较低,所以很有必要给大家普及一下这些高效的分析工具,大家都值得拥有的!希望大家在今后的开发工作中能将这些工具用起来,去有效地提升软件开发调试的效率。
有人可能会问,你怎么了解到这么多工具的?其实这些技能都是通过项目实践不断积累得来的,有的是大家都用的,有的是同事或朋友推荐的,有的是网上搜索问题时别人建议的。我已经熟练掌握这些工具的使用,已经将这些工具全面地应用到商业项目的问题分析中,解决了很多开发调试中遇到的多个问题,极大地提升了分析问题和解决问题的效率。
2、高效软件工具介绍
2.1、窗口查看工具SPY++
SPY++是微软IDE软件Visual Studio自带的一个小工具(spyxx.exe),该工具可以查看当前系统中打开的所有窗口的信息(包含显示和掩藏的窗口),在做UI界面编程分析窗口问题时该工具比较有用。
SPY++的主界面如下所示:
使用该工具,我们可以来查看目标窗口的位置、大小、窗口样式、父窗口等信息,如下所示:
对于可见的窗口,可以直接将探测按钮拖动到目标窗口上:
直接查看目标窗口的信息。
对于掩藏的、不可见的窗口,也可以直接在SPY++主窗口中的窗口列表中以窗口标题、类名、窗口句柄等关键字去搜索目标窗口,搜索到查看窗口属性即可。
在模仿其他软件实现一些特效窗口时,通过这个工具查看特效窗口的一些属性,然后去参考相关属性的设置。我们也可以将SPY++列表中所有的窗口(当前系统中创建的所有窗口)信息保存到文件中,操作入口如下所示:
这在分析窗口异常崩溃的问题会很有用。
2.2、Dependency Walker
Depenency Walker是微软提供的一个库依赖查看工具(depends.exe),该工具可以查看二进制文件exe和dll动态库的导出接口信息,也可以查看二进制文件的库依赖关系。这个工具虽然很小,却很实用,是分析软件因为库依赖问题导致的启动报错的最直接工具。该工具是早期Visual Studio自带的工具,现在的Visual Studio不再打包该工具了,可以到Depenency Walker官网上去下载。
Dependency Walker的主界面如下所示:
2.2.1、排查启动报找不到dll库的问题
一个exe程序的启动过程大概是这样的:
用户启动exe程序时,系统会分配对应的进程空间,首先去读取exe文件中的PE头信息,读出exe主程序依赖的所有dll库信息,然后系统将这些依赖的dll库加载到当前进程空间中(加载到进程空间的dll库包括exe开发者开发的dll库,也包括依赖的一些操作系统的dll库),待依赖的库完成加载之后,再去加载exe文件,然后进入exe文件中的main函数,这样exe程序就启动起来了。
如果加载依赖库时找不到依赖库文件,或者找到的依赖库的版本不对,会直接弹窗报错,提示程序启动失败。Depenency Walker工具正是用于排查这类问题的,比如程序启动时缺少库报错、在某个dll库中找不到某个接口报错等。
以以前遇到的一个问题为例,程序启动时报如下的错误:
提示找不到MSVCP100D.dll运行时库(D对应debdebug版本的运行时库),程序是release版本的,怎么会依赖debug版本的运行时库呢?估计是底层依赖的dll库(底层的库包括底层的业务库、网络库、协议库、组件库、音视频编解码库等)发布的有问题,错误地把debug版本的库发布到我们release版本的exe产品流中了。
于是用Dependency Walker打开exe程序,查看其依赖的库,看看哪个库依赖了的debug版本的MSVCP100D.dll,可以查到:
是截图中的dll库依赖了debug版本的MSVCP100D.dll,找到该问题库的维护组,让他们重新发布release版本的库即可。
2.2.2、排查动态启动的dll库启动失败的问题
有的库可能是主程序动态启动的,如果这些动态启动的库因为其依赖的库有问题导致库动态加载失败,是不会弹出上面那种明确的提示框的。比如a.dll库是动态启动的,如果该库启动失败,会导致与之相关业务无法进行,此时可以先使用Process Explorer查看主程序的库加载情况(Process Explorer工具下面也会介绍,该工具会将目标进程加载的所有的dll库显示出来),确定a.dll有没有启动起来:
注意打开Process Explorer后,要点击上方工具栏中的View DLLs图标按钮才能查看目标进程加载的dll列表,然后在该列表中查看目标库有没有启动起来。
假设a.dll没有启动起来,有可能其依赖的库找不到,也可能该库中调用的接口在其依赖的库中找不到。我们再用Dependency Walker直接打开a.dll查看其库依赖的情况即可找到原因。如果库有问题,在其前面会显示红色或黄色警告图标:
黄色图标表示系统中找不到该库,红色图标表示某接口在某个库中找不到。
这里需要注意一下,Dependency Walker打开目标文件后,默认会展开目标文件依赖的所有的库,其中包括很多Windows系统库,我们要通过经验加以甄别,将Windows系统库都折叠起来跳过去(要熟悉一些常见的Windows系统库,比如ntdll.dll、user32.dll、kernel32.dll等。),一般问题都出在我们应用程序的业务库中的。
此外,还有一些api-ms开头的库,在Dependency Walker中显示黄色图标,一般是没问题的。这些api-ms开头的库,一般是应用程序开发者需要打包到产品的安装包中的,这些库在安装时会自动释放到程序的安装路径中的,以腾讯会议的安装目录为例可以看到这些库:
最后有一点需要说明一下,Dependency Walker在win10系统中打开文件时特别慢,有可能是Dependency Walker版本比较老(好几年前出的版本),对win10系统的兼容性不好导致的,打开文件时可能要等上好几分钟才能打开。在这种时候,需要耐心等待一会。
关于使用Dependency Walker排查项目问题实例,可以参见我之前写的文章:
使用Dependency Walker和dumpbin工具定位C++软件启动时找不到接口的报错问题https://blog.csdn.net/chenlycly/article/details/125665650
2.3、剪切板查看工具Clipbrd
该工具主要用来查看剪切板中包含了哪些剪切板格式的数据,在处理数据的复制与粘贴时很有用!在XP系统中,该工具是系统自带的,但自win7系统开始就不再带该工具了,需要的话可以到XP系统中拷贝出来,也可以到网上去下载。
有人可能会说,这都什么年代了,谁还用XP系统啊!确实,现在确实很难找到还使用XP系统的电脑了。不过我们在2009年刚毕业那些年,一直用的还是XP系统。然后出现了Win7系统,并逐步普及,到最近几年已经完全普及了Win10系统,新版本的Win11都出来了。几年前,微软甚至还说Win10将是Windows系统的终极版本,结果微软还是食言了,近年又搞出了Win11。
我几年前参与了一款IM即时通讯软件的开发,要实现聊天窗口中聊天内容的复制和粘贴功能,要支持多种剪切板的格式,要支持与其他常用IM软件比如QQ、PC版微信、企业微信等间的复制粘贴互通,即在IM软件的聊天框中对图片与文字的混合内容进行复制,然后将内容粘贴到QQ等的聊天输入框中,QQ的输入框中可以正常显示;从QQ中复制图片和文字的混合内容,也要能够粘贴到我们的IM软件中并正确显示出来。
那如何知道QQ等软件都支持哪些剪切板格式呢?使用Clipbrd就可以看到。具体的做法是,我们先在QQ的聊天框中复制图片和文字的混合内容,然后打开Clipbrd剪切板查看工具,在Clipbrd的菜单栏中点击查看,在弹出的菜单中就可以看到QQ在复制图片和文字的混合内容时都生成了哪些剪切板格式的数据,如下图所示:
从图片中可以看出,QQ生成了多种剪切板格式的数据,其中QQ_Unicode_RichEdit_Format格式是QQ专有的剪切板数据格式, html Format格式则是通用的格式,是很多IM软件和邮箱都支持HTML Format格式。
系统支持的预定义剪切板格式有CF_TEXT(文字)、CF_UNICODETEXT(Unicode文字)、CF_BITMAP(位图图片)和CF_HDROP(可拖拽数据)等,这些基本格式可以直接使用。对于QQ_Unicode_RichEdit_Format、HTML Format等这些用户自定义格式,则需要使用API函数RegisterClipboardFormat向系统注册才能获得对应CF值,这样自定义的剪切板格式是以“QQ_Unicode_RichEdit_Format”、“HTML Format”字符串作为标识的。
如果我们要知道某种自定义剪切板格式对应的数据内容与格式,可以编写类似如下的测试代码,去获取指定剪切板格式的数据内容:
HANDLE hClip = NULL;
char *pBuf = NULL;
WCHAR* pwchBuf = NULL;
char* pTemp = NULL;
// 1、通过名称去注册剪切板格式
m_dwClipFormatQQ = ::RegisterClipboardFormat( _T("QQ_Unicode_RichEdit_Format") );
// 2、打开剪切板
BOOL bRet = ::OpenClipboard( NULL );
if ( !bRet )
return;
// 3、获取指定剪切板格式的数据句柄
hClip = GetClipboardData( m_dwClipFormatQQ );
if ( NULL == hClip )
CloseClipboard();
return;
// 4、调用GlobalLock函数获取数据buffer地址
pBuf = (char*)GlobalLock( hClip );
if ( NULL == pBuf )
GlobalUnlock( hClip );
CloseClipboard();
return;
// QQ_Unicode_RichEdit_Format格式的数据(字符串),是以utf-8编码的,所以先将
// utf-8编码的字符串转成Unicode编码的
int nNum = MultiByteToWideChar( CP_UTF8, 0, pBuf, -1, NULL, 0 );
pwchBuf = new WCHAR[nNum];
MultiByteToWideChar( CP_UTF8, 0, pBuf, -1, pwchBuf, nNum );
// 将转码后的字符串显示到UI界面上
CString strContent;
CopyUtf8ToCStringT( strContent, pBuf );
m_editContent.SetWindowText( strContent );
GlobalUnlock( hClip );
可以将指定格式的剪切板数据读出来,看看里面的数据格式是什么样子的。比如获取到的QQ_Unicode_RichEdit_Format剪切板格式的数据内容如下:
<QQRichEditFormat>
<Info version="1001">
</Info>
<EditElement type="1" filepath="C:\\Users\\Administrator\\AppData\\Roaming\\Tencent\\Users\\7100****\\QQ\\WinTemp\\RichOle\\MJRPJKHS077`IF_@$Y3.png" shortcut="">
</EditElement>
<EditElement type="0"><![CDATA[123]]>
</EditElement>
<EditElement type="1" filepath="C:\\Users\\Administrator\\AppData\\Roaming\\Tencent\\Users\\7100****\\QQ\\WinTemp\\RichOle\\L2_QM_EYG3)R`76P9HYLPJ.png" shortcut="">
</EditElement>
</QQRichEditFormat>
获取到的HTML Format剪切板格式数据内容如下:
Version:0.9
StartHTML:00000112
EndHTML:00000461
StartFragment:00000126
EndFragment:00000425
<!doctype html><html><body>
<!--StartFragment --><DIV>
<IMG src="file:///C:\\Users\\Administrator\\AppData\\Roaming\\Tencent\\Users\\710027094\\QQ\\WinTemp\\RichOle\\LVF%BQDXAUX@0MP2(%$LLK.png" >123<IMG src="file:///C:\\Users\\Administrator\\AppData\\Roaming\\Tencent\\Users\\710027094\\QQ\\WinTemp\\RichOle\\V8E90K]C]N%1G]8_Q(B488.png" >
</DIV><!--EndFragment-->
</body>
</html>
我们可以参考这些剪切板格式的数据样式,在我们的软件中复制数据时构造这些格式的剪切板数据设置到剪切板中,然后就能和其他IM软件互通了。经实践,对于文字与图片的混合内容或者多张图片一起的复制和粘贴,我们得出了如下的结论:
1)要支持从其他软件中复制内容粘贴到我们的软件中,我们只需要解析通用的HTML Format剪切板格式数据就可以了;
2)要支持从我们软件复制粘贴到其他软件中,我们需要构建通用的HTML Format剪切板格式数据,还要构建粘贴到目标软件时目标软件支持的格式(剪切板中支持同时设置不同剪切板数据类型的数据),比如要将复制的内容粘贴到QQ中生效,则在复制内容时要构造QQ专用的QQ_Unicode_RichEdit_Format剪切板格式,只构造HTML Format剪切板格式,是没法粘贴到QQ中的(粘贴进去没法正常显示的)。飞秋、邮箱等软件都是支持通用的HTML Format剪切板格式的。
而对于纯文字的内容复制,我们只需要设置基础类型CF_TEXT和CF_UNICODETEXT就可以了;对于单张图片,我们设置基础的CF_BITMAP即可;对于多张图片,则要设置HTML Format和QQ_Unicode_RichEdit_Format格式,构造对应格式的数据了。
2.4、GDI对象查看工具GDIView
GDIView是nirsoft公司开发的一个用来显示每个进程使用的GDI对象句柄数的工具,它显示了每种GDI对象的数量以及整个进程占用的GDI对象总数,主要用来帮助开发人员追踪软件中的GDI句柄资源泄漏问题。可以自行到GDIView的官网上下载该工具。
GDIView工具的主界面如下所示:
GDI对象是什么:
GDI是Graphics Device Interface(图形设备接口)的缩写,UI界面应用程序在绘制图片、绘制直线等图形、输出文字时都会用到GDI对象。常见的GDI对象有Pen(笔)、Font(字体)、Bitmap(位图)、Brush(画刷)、Region(区域)、DC(设备上下文)等。
GDIView工具在排查GDI对象泄露问题时,非常好用。
2.4.1、GDI对象泄漏
所谓GDI对象泄露是指,调用系统API函数创建(Create)了GDI对象,在使用完成后,没有去删除(Delete)GDI对象。比如某段执行UI绘制代码有GDI对象泄露的问题,并且这段有问题的代码会被频繁地执行,随着软件的长时间运行,占用的GDI对象可能越来越多(泄露的GDI对象一直没释放,越积越多),当进程的GDI对象总数接近或到达10000个,绘图会出现异常,紧接着程序就会出现卡死或崩溃。这个GDI对象泄漏可以通过自动化工具拷机测试复现出来(比如使用按键精灵录制操作去自动执行拷机测试)。
在Windows系统中,一个进程占用的GDI绘图对象不允许超过10000个,当接近或超过10000时程序绘图就会出现异常,甚至出现崩溃闪退的问题。
当程序绘图出现异常时,可以先在Windows系统自带的任务管理器中查看目标进程的GDI对象总个数:
如果GDI对象数接近或者达到10000个,则说明程序中肯定有GDI对象泄露了。然后再使用GDIView工具去查看哪些GDI对象在持续增长,没有释放,这样在排查代码时就比较有针对性了,就能很快定位问题了。
2.4.2、GDI对象泄漏示例
举一个简单的内存泄露的例子,如下所示:
HPEN hPen2 = ::CreatePen( PS_SOLID, 5, RGB(255,0,0));
HBRUSH hBr = ::CreateSolidBrush( RGB(0, 255, 0) );
::SelectObject( hMemDC, hBr );
HPEN hOldPen = (HPEN)::SelectObject( hMemDC, hPen2 );
::Rectangle( hMemDC, 0, 0, 20, 20 );
::SelectObject(hMemDC, hOldPen);
代码中创建了一个pen对象和一个brush对象,结果在这两个对象使用完毕后没有将这两对象给delete掉,这样就导致了GDI对象的泄露。如果上述代码在程序运行的过程中被频繁的执行,则pen对象和brush对象会持续的增长,使用GDIView工具就能看到这两个GDI对象数在持续的增长。
2.4.3、GDI对象泄漏项目实例
以前帮公司webrtc开发维护小组排查过一个DC对象内存泄露的例子,在他们的代码中调用GetDC去获取窗口DC对象,结果在使用完成后没有调用ReleaseDC将DC对象释放掉,问题代码如下:
#if defined (WEBRTC_WIN)
//修正程序开启DWM导致的鼠标位置问题
int desktop_horzers = GetDeviceCaps( GetDC(nullptr) DESKTOPHORZRES);
int horzers = GetDeviceCaps(GetDC(nullptr),HORZRES);
float scale_rate=(float)desktop_horzers/(float)horzers;
relative_position.set( relative_ position.x()*scale_rate,
relative_ position.y()*scale_rate );
#endif
就导致了DC对象的泄露,导致他们的程序在运行一段时候后程序的窗口显示异常(窗口绘制异常),紧接着程序就发生了崩溃。修改后的代码如下:
#if defined (WEBRTC_WIN)
//修正程序开启DWM导致的鼠标位置问题
HDC hDC = ::GetDC(nullptr);
int desktop_horzers = GetDeviceCaps( hDC, DESKTOPHORZRES);
int horzers = GetDeviceCaps(hDC,HORZRES);
float scale_rate=(float)desktop_horzers/(float)horzers;
relative_position.set( relative_ position.x()*scale_rate,
relative_ position.y()*scale_rate );
::ReleaseDC(nullptr, hDC); // 增加了这一句,解决GDI对象泄漏问题
#endif
我们查到有GDI对象泄漏问题,因为之前的版本是没有GDI泄漏的,并且查看svn代码提交记录确定上层最近没修改过与GDI有关的代码,而底层的webrtc库最近修改过几个问题,所以怀疑是webrtc库修改的代码导致的。但维护webrtc的同事说他们新增的代码并没有操作GDI对象去绘图,应该不是他们的问题。后来不得已去他们那边查看webrtc开源库的修改记录,看到了上述有问题的代码,然后他们才承认是他们新增的代码引起的。这可能也是和他们很少搞UI编程有关系吧,对GDI对象泄漏问题不太敏感。
这是大家的思维定势,软件出问题了,最开始总是说这肯定不是我们模块的问题,应该是其他组维护的模块引起的。
关于使用GDIView排查项目中遇到的问题实例,可以参见我之前写的文章:
使用GDIView工具排查GDI对象泄漏问题https://blog.csdn.net/chenlycly/article/details/125399896
2.5、Process Explorer
Process Explorer是Winternals公司(该公司已经被微软收购)开发的增强版任务管理器软件工具(procexp.exe),功能要比Windows系统自带的任务管理器的功能多很多。使用该工具可以查看服务、进程、线程、模块、句柄、磁盘使用状态、网络使用状态等多类信息。Process Explorer和下面要讲到的Process Monitor都是Winternals公司开发的免费工具,被放置在Sysinternals Suite工具包中。
Sysinternals Suite包含了一系列免费的系统工具,其中有大名鼎鼎的Process Explorer、Process Monitor、FileMon、RegMon等,如果把系统管理员比喻成战士的话,那么Sysinternals Suite就是战士手中的良兵利器。熟悉和掌握这些工具,并且对Windows的体系有一定的了解,将大幅度的提高日常的诊断和排错能力。
Process Explorer的主界面如下所示:
微软在2006年7月收购了Winternals公司,更重要的是,微软通过此并购网罗了该公司的两位重量级创始人Mark Russinovich及Bryce Cogswell。其中,Mark Russinovich目前担任微软Azure云计算CTO首席技术官。Mark Russinovich 在微软开发了非常多的系统工具,比如 winobj、sysmon、diskmon 和进程监视器,同时他还著有著名Windows内核书籍《Windows Internals》(中文名为:深入解析Windows操作系统),慢慢地Mark Russinovich已经成为微软的象征。此外,Mark Russinovich 还精通逆向工程,震惊世界的索尼BMG光盘复制保护丑闻就是他发现的。
对于Sysinternals Suite工具包的获取,可以到微软的https://live.sysinternals.com/ 页面去下载,需要哪个工具就下载哪个工具:
注意,在Sysinternals Suite工具包中,Process Explorer缩写成procexp.exe,Process Monitor缩写为procmon.exe。
2.5.1、使用Process Explorer查看进程的详细信息
在日常工作中,我们主要用该工具来查看进程的相关信息,以排查程序运行过程中出现的问题。平时用到的主要功能有:
1)查看进程启动时传入的命令行启动参数;
2)查看进程加载了哪些dll库(通过该功能确定目标dll有没有启动起来),以及这些库的详细信息;
3)查看进程的磁盘和网络使用统计信息;
4)查看进程的各个线程的信息,可以看到各线程的CPU占用比例和线程的函数调用堆栈(在排查进程CPU占用高时会用到,找到占用CPU高的那个线程);
5)查看进程的GPU占用情况(判断程序是否使用GPU硬件编解码);
6)查看进程的虚拟内存的占用情况。
7)查看进程占用的句柄信息,比如文件句柄、线程句柄、注册表句柄等。
它是我们在日常工作中使用很频繁的一个工具!下面紧接着讲几个使用Process Explorer的常用场景,这些场景在我们的项目都有频繁的使用。
2.5.2、查看目标进程的dll库加载情况
如果要查看进程的库占用情况,需要在第一次启动该工具时点击工具栏中的“View DLLs”图标按钮,才会显示目标进程加载的库列表。默认情况下显示的是句柄占用信息。
在进程列表中,点击选择目标进程,下方就会显示加载了哪些库,然后取查看这些库的信息,比如占用库的路径,占用库的版本。通过库的路径或版本,确定是否加载了正确版本的库。
此外,代码中有些库可能是动态加载的,可以在库列表中看目标dll库有没有启动起来(加载到进程空间中),如果没启动成功,一般是库依赖的库有问题,就需要使用Dependency Walker去打开启动失败的库,查看该目标库依赖的库是否有问题。
2.5.3、查看目标进程中各线程的CPU占用情况,排查CPU占用高的问题
除了查看依赖的dll库信息,还可以查看目标进程中的所有线程信息。双击进程列表中的目标进程,在弹出的窗口中点击Threads标签页,就能看到进程中的所有线程的信息。比如我们在排查目标进程的高CPU占用问题时,按照上述操作去查看进程中的线程列表,列表中看到一个线程占用了很高的CPU,如下所示:
应该就是这个线程导致进程的高CPU占用了。于是双击该线程,就看到线程当前的函数调用堆栈了,如下:
根据线程的函数调用堆栈中调用的函数,对照着C++源码,就能确定当前线程是哪个业务线程了。这个地方需要注意一下,要看线程函数调用堆栈中详细的函数信息,需要将相关模块的pdb符号库文件提前放到模块的同级目录中。
2.5.4、查看目标进程的GPU占用情况
双击进程列表中的目标进程,在弹出的窗口中,点击GPU Graph标签页,就看可以看到目标进程对GPU占用的情况:
主要用于判断目标进程是否使用了CPU的GPU硬件单元。在很多视频会议、视频监控的软件中,可能会使用CPU上的GPU单元,进行视频的硬编硬解,从而节省对CPU的消耗。直接使用CPU进行视频的编解码操作,我们称之为软编软解,会消耗大量的CPU资源。如果能使用GPU进行硬编硬解,则能有效地较少对CPU的占用。
2.5.5、推荐一款与Process Explorer类似的工具Process Hacker
Process Hacker是一款类似于Process Explorer功能丰富的进程查看管理工具(ProcessHacker.exe),可以查看服务、进程、线程、模块、句柄、磁盘使用状态、网络使用状态等。Process Hacker主界面如下:
Process Hacker界面要更酷炫一些,功能上基本和Process Explorer差不多的。此外,Process Hacker是完全开源的,是一位华人软件开发者开发并开源的,可以到Process Hacker官网上下载开源代码。
Process Hacker功能很完备,可以到开源代码中查看诸多功能是如何实现的。
关于使用Process Explorer排查项目中遇到的问题实例,可以参见我之前写的文章:
使用Process Explorer和Dependency Walker定位dll库动态启动失败的问题https://blog.csdn.net/chenlycly/article/details/125216591
2.6、Prcoess Monitor
Process Monitor一款系统进程监视工具(Procmon.exe),主要用来监测目标进程的注册表和文件操作活动。该工具也是微软Sysinternals Suite工具包中的一个工具,程序名缩写为procmon.exe,可以到微软的https://live.sysinternals.com/ 页面去下载(上面已经讲过)。
该工具的具体使用方法是,先点击工具栏的漏斗按钮,添加要监测的目标进程:
然后在工具栏将不需要监测的活动类型取消掉:(一般我们只监测文件活动和注册表活动)
2.6.1、监测注册表活动
比如想知道目标进程在执行某个操作时将相关信息写到哪个注册表路径中,都写入了哪些内容。可以使用该工具对注册表活动进行监测。目标进程会调用系统库,系统库会频繁操作注册表,所以监测出来的注册表读写记录会比较多,需要使用关键字对监测到的结果搜索。
2.6.2、监测文件活动
比如测试同事在测试时发现软件在桌面或其他路径中生成了一些莫名的日志文件,他们需要定位到日志是哪个模块生成的,需要将生成日志文件的代码清理掉(在桌面上生成一些莫名的日志文件,很奇怪的!),此时可以使用该工具对文件活动进行监测。
同样,目标进程对文件的操作很频繁,会监测到大量的记录,可以在监测列表中以文件名进行搜索,然后找到生成目标文件的操作。找到生成目标文件的记录,双击打开该记录,就会弹出与该操作相关的函数调用堆栈上下文:
通过堆栈中函数调用的上下文,就可以看出日志文件是哪个模块、哪个函数生成的,这样就可以到函数中修改代码,将生成日志的操作关闭掉。
关于使用Prcoess Monitor排查项目中遇到的问题实例,可以参见我之前写的文章:
使用Process Monitor工具监测进程对注册表和文件的操作https://blog.csdn.net/chenlycly/article/details/125436195
2.7、API Monitor
API Monitor是rohitab出品的一个用来探测进程对API函数调用的工具,使用该工具可以窥探软件在实现一些功能时都调用了哪些Windows系统API函数,通过这些探测出来的API函数去估计出目标软件某些功能的实现方法。不仅可以探测出调用了哪些API函数,还能探测出调用API函数时都传递了什么样的参数,然后可以参照这些API函数及参数去实现类似的功能或者去尝试解决在开发软件过程中遇到的一些问题。
API Monitor工具的主界面如下所示:
2.7.1、API Monitor帮我们解决问题的实例
该工具非常的有用,帮我们解决了项目中的多个问题。比如在处理我们软件setup安装包的管理员权限问题、64位系统中创建桌面快捷方式时的图标问题,都是通过该工具对某主流软件的安装包的监测,找到了解决问题的办法。
再比如在实现文件传输的打开文件所在文件夹功能时,使用了该工具对某主流软件和迅雷进行监测,找到了解决问题的办法,甚至还发现了迅雷的一个掩藏很深的bug。
还有一个实例,我们软件在打开chm文件时遇到了一些问题,使用该工具监测了Beyond compare文件内容比较软件,该软件有打开.chm帮助文档的操作,获得了Beyond compare软件在调用对应的API函数时传递的参数。参照这个参数,解决了我们软件中的问题。
此外,该工具除了可以监测对系统API的调用,还可以监测对第三方库的导出函数的调用。
2.7.2、API Monitor的具体使用方法
该工具的具体使用方法以监控某安装包程序为例,先将目标监控程序启动起来,在左侧的函数列表中搜索要监控的API,比如ShellExecute和CreateProcess,将搜索到目标函数都勾选上:
然后在左下侧的进程列表中,选择要监控的进程,点击右键菜单中的“Start Monitoring”即可开启监控。然后让目标监控程序继续跑下去,这样就能监控到目标API函数的调用情况了:
关于使用API Monitor排查项目中遇到的问题实例,可以参见我之前写的文章:
使用API Monitor监测到目标程序对系统API函数的调用https://blog.csdn.net/chenlycly/article/details/125362394
2.8、调试器Windbg
Windbg是微软出品的Windows平台下强大的用户态和内核态调试工具,与微软的IDE工具Visual Studio相比,它是一个轻量级的调试工具。所谓轻量级指的是安装快速轻便,但其调试功能,却比Visual Studio更为强大。
Windbg的主界面如下所示:
2.8.1、使用Windbg分析C++软件异常
Windbg是分析与排查C++软件异常的利器,在排查软件异常与崩溃时非常有用,是我们日常工作中用的最多的工具之一!
通过在公司内部及网上调研发现,很多C++程序员普遍在软件调试与异常分析方面比较欠缺,所以很多人这方面的技能都有待加强。他们分析软件异常问题还停留在很原始的状态,要么通过IDE调试,要么通过分析日志去分析。对于很难复现的问题,则很难去用IDE调试排查,而使用日志去排查效率非常低,很难定位问题。而使用Windbg调试器去分析,可以极大地提升排查问题的效率,有些比较典型的异常崩溃,分分钟就能定位出原因,当然这其中可能需要一些经验。
2.8.2、IDE看不到有效的函数调用堆栈,Windbg动态调试却可以看到
在排查软件异常时,一般是通过发生异常时的函数调用堆栈去分析的,但有时即使能用IDE调试源码复现问题,但却看不到有效的函数调用堆栈,也没法做进一步分析。这类场景我们遇到过很多次了。比如一个最典型的场景,当程序中发生线程栈溢出时,Visual Studio会直接退出调试状态,这样也就无法看到异常发生的函数调用堆栈了。再比如,我们在调试状态发生崩溃,但切换到函数调用堆栈页面,却看不到有效的函数调用堆栈。遇到这些问题时,可以尝试使用Windbg启动目标程序调试运行,问题复现后可能就能看到完整的函数调用堆栈了,问题可能很快就能迎刃而解了。
以上个星期帮新人搭建项目的Debug运行调试环境时遇到的问题为例,环境搭建好后启动Debug调试,结果刚启动程序就发生了崩溃,弹出如下的提示框:
Visual Studio中断下来,将提示窗口关闭,切换到call stack函数调用堆栈窗口中查看函数调用堆栈:
但看不到完整的函数调用堆栈。这个崩溃是必现的,既然Visual Studio看不到有效信息,于是想到使用Windbg启动Debug版本exe程序。
因为是exe启动时的崩溃,所以Windbg一启动exe就产生崩溃,Widnbg感知到了异常并中断下来,输入kn命令查看函数调用堆栈,看到了如下的详细函数调用堆栈:
这个函数调用堆栈时有效的,通过堆栈得知崩溃发生在initglobe函数中,并且看到了代码的行号,于是去查看C++源码最终找到出了问题。
在这个问题中,IDE开发工具Visual Studio在崩溃时看不到有效的函数调用堆栈,但使用Windbg动态调试目标程序复现崩溃时是可以看到的。
2.8.3、Windbg静态分析dump文件与Windbg动态调试
使用Windbg分析软件异常有两种方式,一种是静态分析dump文件,一种是使用Windbg动态调试目标程序(可以直接使用Windbg启动目标程序,也可以将Windbg附加到已经运行的目标进程上)。
对于静态分析dump文件的方式,首先需要在软件发生异常时软件自己自动捕获到,并自动生成dump文件。一般我们需要在软件中嵌入异常捕获模块,可以使用Google开源的CrashRpt库(该开源库是有缺陷的,没法做到对所有模块的异常捕获,可以使用微软的detours开源代码中的技术进行改进),在软件发生异常时异常捕获模块感知到,自动生成包含异常上下文的dump文件。这样我们在事后将dump文件取过来分析就可以了。我们在项目上基本使用这种方式去事后分析异常。
但在实际场景下,有少数异常崩溃,异常捕获模块可能捕捉不到,或者感知到后,在生成dump文件时产生了二次崩溃,这样就没有dump文件可供分析了。遇到这类问题,则需要使用Windbg将程序启动起来或者将Windbg附加到已经启动的程序进程上,即将Windbg挂到目标进程上和目标进程一起跑,如果目标进程发生异常,Windbg会第一时间感知到并中断下来,这样就可以拿到函数调用堆栈进行分析了。
2.8.4、Windbg上手与汇编代码
很多人可能觉得Windbg上手比较难,其实Windbg的入门并不难,只需要掌握常用的Windbg命令,了解函数调用的栈分布图,知晓函数调用的栈回溯原理,基本就能用起来了。但如果要深入分析,就需要有较深的软件开发经验和汇编语言基础了。但要熟练掌握软件异常排查的技能,还是需要不断实践,C++异常崩溃的原因是五花八门的,需要在排查问题中不断的积累、不断的总结!排查的问题越多,积累就越多,就越有心得!在问题中增长经验,在总结中升华技能!这是近几年来做这类工作内容的最大心得!
搞C/C++开发,了解汇编语言,掌握一些基础的汇编知识,是很有必要的。这不仅有利于理解C/C++是如何运行的(有些问题需要从汇编代码的角度去看,从汇编代码的角度才能将问题看清楚),而且有利于从较深层次上分析一些复杂的软件异常。有时在分析一些异常崩溃时,需要查看C++源码对应的汇编代码才能最终搞清楚的。程序最终是崩溃在某套汇编指令上,汇编指令才能最本真、最直接地反映问题的所在。
此外,有时我们为了提高代码的执行效率,会直接在C++代码中嵌入一段汇编代码(可能是嵌入一小段代码,也可能是整个函数都是汇编代码实现的)。之前我在协助音视频编解码组的同事排查异常崩溃时,看到了音视频编解码的部门源码,他们直接将一个频繁调用的函数直接用汇编代码去实现,以提升音视频编解码的执行效率。
有人可能会问,经过IDE编译出来的二进制文件中也都是汇编指令,你人为的添加一段汇编代码,都是汇编代码,为啥会有执行速度上的差别呢?因为源代码经过编译器的处理生成的汇编代码在实现上可能不是最优的,这要依赖编译器,而我们人为地添加汇编,可以直接控制汇编代码,保证汇编代码是最优的,不再依赖编译器。
关于如何使用Windbg以及使用Windbg排查问题的实例,可以参看我的专栏,里面有多篇文章进行了详细的阐述:
C++软件异常排查从入门到精通系列汇总https://blog.csdn.net/chenlycly/article/details/125529931
2.9、反汇编工具IDA
IDA(The Interactive Disassembler)是比利时Hex-Rays公司开发的一款强大的交互式反汇编工具,可以直接打开exe或者dll等二进制文件,查看文件中的汇编代码。
有时在分析一些C++软件异常崩溃时,需要查看C++源码对应的汇编代码才能搞清楚,我们一般使用IDA去查看目标模块的汇编代码。程序最终是崩溃在某套汇编指令上,汇编指令才能最本真最直接地反映问题的所在的。有时要搞清楚代码奔溃的本质,还是要看汇编代码的。
IDA是一位俄罗斯天才程序员Ilfak Guilfanov(伊尔法克·吉尔法诺夫)开发的,他是比利时Hex-Rays公司的CEO。俄罗斯盛产天才程序员,Java程序熟知的IDE工具IDEA(IntelliJ IDEA)就是3个俄罗斯天才程序员开发的,他们在捷克成立了伟大的JetBrains公司,该公司除了开发出IDEA,还研发出了Python开发环境PyCharm、Web前端开发环境WebStrom、Go语言开发环境GoLand、php语言开发环境PhpStorm等多个IDE开发工具。被IT界广泛使用的Web及反向代理服nginx,也是俄罗斯人Igor Sysoev(伊戈尔·赛索耶夫)开发的。在大数据领域广泛应用的高性能数据库ClickHouse是俄罗斯的Yandex 公司开发并开源的,字节跳动公司推出的具有更高性能数据库ByteHouse就是在开源ClickHouse的基础上改进而来的。除了盛产程序员,还擅长产出顶级的数学家和化学家,华为俄罗斯研究所就招募了不少俄罗斯数学家去做基础研究。
2.9.1、IDA程序图标的由来
IDA的程序图标是世界首位计算机程序员:
对,你没看错,她是世界第一位程序员,女程序员,她叫阿达·洛芙莱斯(Ada Lovelace)。IDA 用她的头像作为图标是为了纪念这位伟大的女程序员。
Ada Lovelace设计了巴贝奇分析机上解伯努利方程的一个程序,并证明当时的19世纪计算机狂人巴贝奇的分析器可以用于许多问题的求解。她甚至还建立了循环和子程序的概念。由于她在程序设计上的开创性工作,Ada Lovelace被称为世界上第一位程序员。
2.9.2、使用IDA协助排查C++软件异常
在我的日常工作中,IDA主要是来辅助Windbg去分析和排查软件异常问题的。分析问题时,我们用IDA打开二进制文件查看汇编代码,一般是对照着C++源码去阅读汇编代码上下文的。
Windbg中也能查看到汇编代码的上下文,但Windbg中显示的汇编代码没有任何注释,很难将汇编代码和C++源代码对应起来的。一般撇开C++源代码直接去读汇编代码上下文是很难的,除非你有非常强大的汇编功底与反汇编能力。
此外,在release下编译代码时,编译器会对代码进行大量的优化,所以有时汇编代码很难和C++源代码一一对应起来。而IDA在解析出二进制文件的汇编代码时,会添加很多注释,在二进制文件有pdb符号库的情况下,IDA会添加更多的注解(有pdb时,可以看到每个函数名称以及函数中的变量信息),所以使用IDA查看汇编代码上下文要方便多了,能很好地将汇编代码和C++源代码对应起来。
2.9.3、使用IDA查看二进制文件的汇编代码,排查第三方安全软件bug
有次客户那边出现崩溃问题,正是公司的技术大牛直接使用IDA查看第三方安全软件的汇编代码定位出来的。我们的软件部署到某个客户的系统中,会出现频繁的崩溃。客户的安全等级比较高,系统中安装了多个第三方安全软件,这些安全软件一般为了实现对目标程度的监控,都会注入到系统的进程中进行实时监测,我们初步怀疑是安全软件注入引起的。但客户说系统中的其他软件运行都没问题,为啥就你们的软件有问题呢?
如果怀疑是第三方安全有问题,必须拿出证据,否则就是你们的软件有问题。这个问题比较棘手,于是找来了公司的顶级大牛,他直接使用IDA去查看注入模块的汇编代码,最终找到了原因。
第三方安全软件为了监控所有进程在网络上收发的数据,直接将系统协议栈的socket套接字函数给hook了,hook成安全软件模块的接口,其中有个用于UDP下接收数据的recvfrom API接口,该接口的函数声明如下:
其中第5个参数和第6个参数都是指针,微软的说明如下:
第五个参数:[out] from
An optional pointer to a buffer in a sockaddr structure that will hold the source address upon return.
第六个参数:[in, out, optional] fromlen
An optional pointer to the size, in bytes, of the buffer pointed to by the from parameter.
这两个参数都是指针,都是可选参数,我们在某块代码不用这两个参数,于是都传入NULL,正是这两个NULL参数,导致了第三方安全软件的注入模块产生了崩溃。
通过使用IDA查看第三方安全软件的注入模块中recvfrom函数的汇编代码,函数中没有对该函数的第5个指针参数和第6个指针参数做是否为空的判断(这点太不严谨太不专业了!!!),然后就直接给传进来的参数变量赋值了,这样就操作NULL内存地址,产生了内存访问违例,导致了崩溃。拿出证据后,让客户向第三方安全软件提供商反馈,他们也承认是他们软件有上述缺陷的,并进行了相应的修改。
关于IDA反汇编工具的详细介绍,可以参见我之前写的文章:
IDA反汇编工具使用详解https://blog.csdn.net/chenlycly/article/details/120635120