使用 Process Explorer 和 Windbg 排查软件线程堵塞问题

目录

1、问题说明

2、线程堵塞的可能原因分析

[3、使用Windbg和Process Explorer确定线程中发生了死循环](#3、使用Windbg和Process Explorer确定线程中发生了死循环)

4、根据Windbg中显示的函数调用堆栈去查看源码,找到问题

4.1、在Windbg定位发生死循环的函数的方法

4.2、在Windbg中查看变量的值去辅助分析

4.3、是循环计数值没有累加导致的

5、可以从动态调试的Windbg中导出dump文件

6、最后


C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html 软件运行过程中有线程发生堵塞,是时常发生的事,最近就遇到一个典型的案例,虽然分析过程不是很复杂,但很有代表性,在这里给大家分享一下这个问题的详细排查过程,希望能给大家提供一个借鉴或参考。使用Process Explorer和Windbg排查软件线程堵塞问题

1、问题说明

某天,客户在使用我们软件的过程中遇到了问题,在其Windows10系统中运行我们的客户端软件,加入了一个会议,一直在开会,没有做其他的操作,中间某个时间点用鼠标去点击软件窗口时发现点击没反应,好像是软件UI界面卡死了!但会议窗口中还能看到正在动的远端视频,能听到远端的声音,应该是UI线程卡死了,其他线程还在正常运行。这个问题比较严重,无法操作软件界面了,如果领导那边出现这样的问题,就比较麻烦了。于是联系到我们,希望我们尽快协调研发人员排查定位一下。

我们研发这边接到任务后,和客户取得了联系,通过远程软件远程连到他的电脑上。我们初步怀疑是软件的UI界面所在的主线程卡死或堵塞了,于是将SPY++、Process Explorer和Windbg等工具拷贝到客户的电脑上准备详细分析一下。


在这里,给大家重点推荐一下我的几个热门畅销专栏:

专栏1:(该专栏订阅量接近350个,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!)

C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931

本专栏根据近几年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的实战问题分析实例,带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
专栏中的文章均是通过项目实战总结出来的(通过项目实战积累了大量的异常排查素材和案例),有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!

专栏2:

C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html

以多年的开发实战为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域的多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!

专栏3:

开源组件及数据库技术https://blog.csdn.net/chenlycly/category_12458859.html

以多年的开发实战为基础,分享一些开源组件及数据库技术!


2、线程堵塞的可能原因分析

软件为了并发处理事务一般都使用了多线程,如果代码处理不当,可能就会出现线程堵塞或卡死问题。一般是个别线程的堵塞,其他线程还是正常执行的,发生堵塞的线程中处理的业务出现异常。

对于客户端软件,一旦有线程发生异常,我们可以通过软件界面的异常表现感知到,然后通过日志去大概地确定发生异常的业务线程。在本例中,**UI界面不能操作了,判断应该是UI界面所在的主线程堵塞了。**会议中音视频的解码播放是在底层模块的其他线程中进行的,这些线程是正常运行的,所以还能看到视频画面、听到会议中的声音。

导致线程发生堵塞或卡死的原因主要有两种

1)死锁 :软件中多线程发生了死锁,某个线程需要获取某个锁,但因为死锁导致该锁一直没释放,导致线程一直卡在WaitForSingleObject等等待函数接口上没返回,所以线程卡住了;
2)死循环:当前线程中某个函数中发生了死循环,接口调用一直没返回,导致线程卡住了。如果是死循环,还会导致一个现象,死循环会占用大量的CPU时间片,导致程序进程占用较高的CPU,用Process Explorer工具则可以看到某个线程的CPU占用的特别高,这个线程就是发生死循环的线程。

在本案例中,出问题的是UI客户端软件,客户端界面没法操作了,可能是界面所在的UI线程(UI程序的主线程)发生了堵塞,可能是死锁或死循环导致的。 UI界面没法操作,还有另外一个原因,可能是窗口被disable了 ,窗口之前在执行某个操作时被disable了(比如弹出了一个模态框),在执行完操作后出现异常,没有将窗口恢复到enable状态,这个问题场景我们以前在项目中遇到过几次。我们可以用SPY++工具查看一下当前不能操作的窗口属性,如果是当前的窗口被disable导致窗口不能操作的,那么使用SPY++查看的窗口属性中应该能看到WS_DISABLED窗口风格,如下所示:

本例中界面不能操作不是窗口被disable导致的。

对于腾讯会议、企业微信、字节飞书、阿里钉钉等这类Windows桌面客户端软件,UI界面所在的线程称为UI线程,也是软件的主线程。如果UI界面或窗口不能操作了,可能就是UI线程出问题,发生堵塞了。

在Windbg中,被分析的软件有多个线程,线程除了有线程id,还有个线程编号,编号从0开始,UI主线程的编号就是0,Windbg切换线程的命令~ns中的n就是线程编号。

3、使用Windbg和Process Explorer确定线程中发生了死循环

当时软件的UI窗口不能操作了,使用SPY++查看窗口属性,窗口并没有被disable掉,所以基本可以断定,UI界面所在的UI线程发生堵塞了。

那这个线程堵塞是死锁引起的,还是死循环引起的呢?其实要确定这个问题,很简单,当前程序进程还在的,只需要将Windbg附加到问题进程上,切换到0号UI主线程(上面已经讲了,UI程序的UI界面就在UI主线程中),然后查看该线程的函数调用堆栈,如果堆栈中调用了WaitForSingleObject或NtWaitForAlertByThreadId(进入临界区时会调用该函数)等等待函数时,就能确定是线程发生死锁了。

这个地方需要提一下,线程堵塞和程序崩溃是两个完全不同的概念和场景,要注意区分一下

1)对于堵塞 ,堵塞只是发生在个别线程中,其他线程还是正常的,程序进程没有退出,进程还在的,此时可以将Windbg附加到进程上调试的。
2)对于程序崩溃 ,如果没弹出崩溃提示框,程序直接闪退,程序进程就不存在了,就没有机会将Windbg附加到进程上分析了。如果程序没有生成dump文件,只能在下次运行程序时将Windbg附加到进程上动态调试(Windbg和目标程序一起跑),然后去复现崩溃,一旦程序发生崩溃,Windbg就会感知到并中断下来,就可以分析了。此外,如果程序崩溃时弹出了系统报错提示框,只要不将该提示框点掉,则进程还在的,此时还有机会将Windbg附加到进程上进行分析的,我们在项目中遇到过这样的场景。

将Windbg附加到当前出问题的程序进程上,使用~0s命令切换到UI线程中,然后输入kn命令查看UI线程此刻的函数调用堆栈,如下所示:

因为没有加载pdb符号文件,所以堆栈中看不到具体的函数名。于是使用lm命令查看堆栈中dll模块的时间戳,到我们的文件服务器上找到对应时间点的pdb文件(我们已经将各个版本的安装包、二进制文件和pdb文件保存到文件服务器上,维护起来了),然后将pdb文件路径设置到Windbg中,重新执行~0s和kn命令查看UI线程的详细函数调用堆栈信息,如下所示:

加载pdb文件后,调用堆栈中就可以看到具体的函数名了,但没有看到WaitForSingleObject或NtWaitForAlertByThreadId(进入临界区时会调用该函数)等等待函数的调用,所以基本可以断定UI线程的堵塞和死锁没有关系。

基本只有一种可能,UI线程中的某个函数发生死循环了。死循环就是一直在执行代码,会占用大量的CPU时间片,死循环所在的线程会占用较高的CPU比例,直接导致程序进程占用较高的CPU。这个我们可以使用Process Explorer工具核验一下,打开Process Explorer,在进程列表中找到当前出问题的进程,然后双击该进程条目,查看进程的属性,在弹出的属性页面中点击Threads标签页,在该页面中可以查看到当前进程的各个线程信息,包括线程占用的CPU比例以及线程此刻的函数调用堆栈。

Process Explorer查看线程的函数调用堆栈可能不准确,可以使用另一个类似的工具Process Hacker,这个工具看线程的函数调用堆栈比较准确!

查看到如下的线程列表:

果然看到了一个线程占用了20%的CPU,但只能看到线程号,我们如何确定这个线程就是当前出问题的UI主线程呢?

其实很简单,我们可以回到Windbg中,使用~命令打印出当前问题进程的所有线程,如下所示:

我们上面讲了,UI应用程序的UI主线程在Windbg中的线程序号就是0 ,看着上述线程列表,第一个条目就是0号线程,就是UI主线程,找到其对应的线程id为0x2e20(16进制),转换成10进制数据为:11808,即UI线程的线程id为11808,然后跳回到Process Explorer的线程列表页面,占用CPU高的线程就是UI主线程。所以,可以确定UI线程中有函数发生死循环了。

4、根据Windbg中显示的函数调用堆栈去查看源码,找到问题

确定UI线程发生了死循环,下面只要根据Windbg中显示的UI线程的函数调用堆栈,去查看C++源码去分析为什么出现死循环就可以了。

回到Windbg中,使用~0s命令切换到UI线程中,然后输入kn命令查看UI线程的函数调用堆栈,如下所示:

之前已经设置pdb到windbg中了,所以堆栈中显示了详细的函数名和代码行号,看到最后调用的一个函数是xxxlib!LoginManager::OnSrvAddrsChangedNtf。

4.1、在Windbg定位发生死循环的函数的方法

这个地方说几个关于使用Windbg排查死循环的技巧。

我们当前的问题相对较简单,没用到此处提的方法。大家后面可能会用到,所以在此说明一下。

如果当前要确定当前线程是否发生死循环,可以尝试多次输入g命令,让程序继续跑,然后再break中断下来,如果每次堆栈都是一样的话,可能发生死循环了。但这个需要我们根据调用堆栈辨别一下,不是堆栈一样就一定是死循环,比如我们的某个业务线程就在循环执行业务,每次查看的堆栈肯定是一样的,这不是程序中发生了死循环。业务线程中虽然是个循环,但中间会人为sleep一下,不让线程跑满。

此外,我们在Windbg中可以使用bp命令设置断点,比如可以在函数调用堆栈中的多个函数中设置断点,然后输入g命令让程序继续跑,看都命中了哪些函数中的断点,这样就能确定死循环发生在哪个函数中了。

Windbg中支持设置断点,相关的命令如下:

1)bp:添加断点;

2)bc:清除断点;

3)bl:列出断点;

4)ba:设置数据断点。

这几个命令的详细说明及使用方法,可以查看Windbg帮助文档,文档中有相关的示例可以参考。

4.2、在Windbg中查看变量的值去辅助分析

以前我们多次讲过,可以尝试在Windbg中查看函数中局部变量或者函数所在类的成员变量的值,某些变量的值可能是分析问题的关键线索。在Windbg中查看变量的值有几个场景,此处说明一下:

1)当前用Windbg分析的是小dump文件 (程序中安装的异常捕获模块捕获到的,文件比较小,可能就几MB大小),dump文件中只保存了少部分变量的值,能不能看到自己想看的变量的值,要看运气的。

2)当前用Windbg分析的是全dump文件(从Windows任务管理器中导出的或者从正在动态调试的Windbg中使用.dump命令导出的),全dump保存了所有内存信息,其大小接近当前进程占用的用户态虚拟内存的大小,可以看到所有变量的值。

如果要查看程序中全局变量的值,可以使用x命令去搜索,前提是要有pdb符号文件,因为pdb文件中有变量的符号信息。

当前函数调用堆栈显示的最后一个函数是LoginManager::OnSrvAddrsChangedNtf,代码如下:

估计是函数中的for循环发生了死循环,for循环的条件中访问了TServerAddrs_Api结构体变量的dwCount成员的值,**是不是这个dwCount值有问题?是一个异常大的值,导致循环一直跳不出来?**于是想查看该指针变量的值,于是点击函数调用堆栈前面的序号:

将所在函数的栈展开:

只能看到函数所在类对象指针this值,但我们想看TServerAddrs_Api* ptServerAddrs指针变量展开的结构体对象中的值。并没有显示,但该结构体指针对象的值就是wParam值,wParam值是能看到的,所以我们是有办法看到TServerAddrs_Api* ptServerAddrs指针变量展开的结构体对象中的值的。

我们把wParam局部变量中的值0x336200a0当成指针变量TServerAddrs_Api* ptServerAddrs的值去解析,需要使用Windbg的一个较复杂的命令,但我们并没有记这个命令,该怎么办呢?我们点击this指针的超链接:

这个超链接自动生成查看这个指针中变量的值的Windbg命令并执行该命令,我们直接借鉴这个自动生成的命令:

dx -r1 ((xxxlib!LoginManager *)0x55a8cc0)

将之改成:

dx -r1 ((xxxlib!TServerAddrs_Api *)0x336200a0)

然后执行该命令:

可以看到dwCount值为1,所以底层传上来的数据不是异常值,不是上面假定的问题。

4.3、是循环计数值没有累加导致的

我们继续查看函数pcmt_mtclib!LoginManager::OnSrvAddrsChangedNtf:

这个接口是处理底层模块投递上来的消息的(消息的处理函数),乍一看函数没有明显问题,难道是底层一直在抛这个消息,导致我们的代码一直在执行,导致CPU占用高?

我甚至找来了维护底层模块的同事,让他们看日志,看看是不是底层一直在持续抛同一个消息。同事并没有找到问题,后来我无意中瞄了一下代码,**居然是for循环中索引值没有累加导致的,**如下所示:

没有对索引值进行自加操作,导致了死循环。这么简单的问题,居然如此动干戈!属实不应该啊!

5、可以从动态调试的Windbg中导出dump文件

当前我们到客户的电脑上将Windbg附加到目标进程上调试时,如果一时半会没查出问题,我们不能一直占用别人的电脑,他们还有自己的事情,我们可以使用.dump命令将进程的上下文信息导出到dump文件中:

.dump /ma D:\1214.dmp

然后将dump文件发回去,我们事后再去详细分析。

6、最后

这个案例给我们展示了如何使用工具高效地排查软件运行过程中遇到的问题,虽然不复杂,但讲到了使用Windbg等工具的多个细节,有很大的参考价值!我们在研学技能时,要多关注细节,多关注分析问题的思路和办法。

相关推荐
蓑衣夜行2 天前
捉虫笔记(五)-WinDbg调试3个时机
windbg·捉虫笔记
zhuqiyua3 天前
深入解析Kernel32.dll与Msvcrt.dll
汇编·microsoft·windbg·二进制·dll
可均可可2 个月前
Advanced .Net Debugging 11:完结篇
windbg·sos·net 高级调试·ntsd
鬼才2 个月前
记一次 公司.NET项目部署在Linux环境压测时 内存暴涨分析
windbg·dump
可均可可5 个月前
Advanced .Net Debugging 10:事后调试
windbg·任务管理器·dump·ntsd·事后调试