目录
1、概述
2、老版本的输入法导致软件CPU频繁跳高(导致软件出现卡顿)的问题
3、QQ拼音输入法注入到安装包进程中,导致安装包主线程卡死问题
3.1、多线程死锁分析
3.2、进一步研究
4、安全软件注入到软件中,注入模块发生了崩溃,直接导致软件发生崩溃
5、安全软件注入到软件中,注入模块发生了内存泄露,直接导致软件内存耗尽后发生闪退
6、最后
VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html 有些软件为了实现某些功能需要远程注入到其他软件进程中的,比如输入法和安全软件等,注入到其他进程后,注入模块就运行在被注入的进程空间中了,一旦注入模块发生内存泄漏、崩溃等问题,会直接影响到被注入的进程,引发被注入进程发生异常。本文结合项目中遇到的若干问题,给大家详细介绍一下因为远程注入引发软件异常的几个典型项目实例,以供大家借鉴或参考。
1、概述
在日常工作中接触比较多的需要注入到其他软件进程的软件主要有输入法和安全软件:
1)输入法:输入法在注入到其他软件进程之后,其他软件进程才能使用输入法输入文字。
2)安全软件:安全软件为了实时监控其他软件的操作与行为,为了实时监控其他软件网络数据的收发,也需要远程注入到其他软件进程中。
用于远程注入的模块,注入到目标进程中后,就驻留在目标进程中了,即运行在目标进程的进程空间中了。一旦注入模块发生异常,会直接影响到被注入的进程,会直接引发被注入的进程发生异常。
根据之前项目中遇到的多个问题,注入模块对被注入进程的影响,主要有以下几类:
1)输入法注入到软件进程后,可能会导致软件发生明显的卡顿,特别是在输入文字时,这个问题在客户使用低版本的搜狗输入法时遇到过。
2)输入法注入到软件进程后,可能会引发软件发生死锁,这个问题在运行软件安装包程序时遇到过。
3)注入模块在运行过程中遇到异常,发生崩溃,直接导致被注入的软件发生崩溃。因为注入模块就运行在被注入的软件进程中的。
4)注入模块发生内存泄漏,内存泄漏发生在被注入的软件进程中,导致被注入软件发生Out of memory内存耗尽发生闪退。
下面讲几个在项目中遇到的问题实例,来看看注入模块是怎样影响到我们软件的。
2、老版本的输入法导致软件CPU频繁跳高(导致软件出现卡顿)的问题
有客户反馈,在其使用我们软件的过程中会时不时出现卡顿问题。让客户打开Windows任务管理器,让其帮忙观察一下软件使用过程中该软件进程的CPU占用情况。
经观察发现,软件在使用过程中CPU占用比例会时不时地跳高,跳高之后又自动回落。特别是在聊天框中输入时,CPU会跳高。CPU占用变高,一般可能是因为软件中在持续地执行代码导致的,比如程序中发生死循环,一直在不停歇地执行代码。但本例中,CPU会很快自动回落,好像不是死循环导致的,如果是死循环,CPU占用会一直比较高。难道代码中会出现短时间内的死循环?
于是使用Process Explorer工具查看CPU占用高的线程,然后查看该线程的函数调用堆栈,多次刷新多次查看函数调用堆栈,发现线程中一直有搜狗输入法(IME - Input Method Editor,输入法)相关函数的调用(函数所在模块名中有Ime和Sougou字样,所以判断是搜狗输入法相关模块),所以怀疑这个问题可能和搜索输入法有关。
软件界面卡顿,应该是软件UI界面卡顿,所以应该是UI主线程有问题。Process Explorer中看到的CPU占用高的线程,应该就是UI主线程。可以用Windbg确认一下,将Windbg附加到软件进程上,使用~命令将进程中的所有线程信息都打印出来,如下所示:
在Windbg中,UI主线层是0号线程,将该0号线程的线程id(进程id为16进制数)与Process Explorer中显示的线程id(进程id为十进制数)比较一下就知道了。至于怎么比较,也可以参看我之前写的文章:
使用Process Explorer/Process Hacker和Windbg高效排查软件高CPU占用问题https://blog.csdn.net/chenlycly/article/details/134180480 后来让客户查看了一下其安装的搜狗输入法的版本,是一个较老的版本,让他安装最新版本的搜狗输入法,安装后好像就没问题了。所以该问题应该是和输入法有关的
3、QQ拼音输入法注入到安装包进程中,导致安装包主线程卡死问题
在某客户的电脑上,会时不时出现启动我们的软件安装包后没反应,启动起来后应该显示安装包界面的,但一直看不到安装包界面,到任务管理器中可以看到软件安装包进程,说明程序已经启动了。
估计是安装包UI界面所在的UI主线程发生卡死了,查看安装包进程的CPU占用很低,所以能排除程序发生死循环的可能。很可能是UI线程与其他线程发生死锁了。
3.1、多线程死锁分析
导致线程发生卡死,一般有两种原因:
1)代码中发生死循环,导致函数一直没返回,线程卡死;
2)代码一直卡在WaitForSingleObject等待锁的状态,导致函数一直没返回,线程卡死。这是多个线程之间使用锁,发生死锁引发的。
于是将Windbg启动起来,附加到出问题的安装包进程上,然后使用~0s命令切换到UI主线程(UI主线程是0号线程),然后输入kn命令查看函数调用堆栈,如下所示:
从堆栈中可以看出,当前线程卡在等待锁的WaitForSingleObject函数上,一直没返回。可以多次go,多次查看0号线程的函数调用堆栈,每次都卡在WaitForSingleObject函数上。
此外,沿着函数调用堆栈向上看,是调用EnterCriticalSection接口去获取临界区对象,触发了WaitForSingleObject函数的调用。说明当前发生死锁的是临界区锁。
当前确定是UI主线程发生死锁了,那与之关联的死锁线程是哪个呢?安装包程序比较简单,进程中没几个线程,使用~*kn命令将所有线程的函数调用堆栈都打印出来,看到1号线程也卡在获取临界区锁的函数调用上:
那基本可以确定是0号线程和1号线程发生死锁了:
至于是不是这两个线程形成了死锁,需要分析锁的信息,具体分析方法,我在此就不赘述了,可以查看我之前写的文章:
使用Process Explorer/Process Hacker和Windbg高效排查软件高CPU占用问题https://blog.csdn.net/chenlycly/article/details/134180480 此处也可以用《Windows核心编程》第9章源码中提供了一个叫LockCop的死锁检测工具,当时使用该工具监测结果如下:
该死锁检测工具是调用Windows API函数去检测的,只能检测到部分对象的死锁,无法监测到所有类型的死锁,关于这个工具的详细说明,可以查看《Windows核心编程》第9章(使用等待链遍历API来检测死锁)的内容。
关于多线程及多线程死锁相关内容,可以参考我的文章:
从C++软件调试实战的角度去看多线程编程中的若干细节问题https://blog.csdn.net/chenlycly/article/details/134358655 从发生死锁的1号线程的调用堆栈来看,看到了QQPinYin的模块名,那说明这个线程是与QQ拼音输入法相关的。这就是说,这个死锁是与QQ拼音相关的,当时建议尝试两个办法,一个是升级QQ拼音输入法的版本,另一个是将QQ拼音输入法换成搜狗输入法。客户采用了后面这个方法,将输入法换成搜狗输入法后就不再出现了。
3.2、进一步研究
对于0号线程,是调用了API函数SHGetSpecialFolderLocation触发的死锁;对于1号线程,是调用API函数SHGetSpecialFolderPathW触发的死锁。这说明死锁发生在底层的Windows系统库中,不在上层库中。这种情况很少遇到,一般死锁都发生在上层的业务代码中。
这两个函数在两个线程中调用会发生死锁?于是尝试到微软MSDN上查看这两个函数的说明。看到这两个函数都已经被微软废弃了,如下所示:
建议不要使用这两个函数了,应该使用对应的替代函数。
为了保证代码的健壮性,我们应该遵从微软官方的说明,不再使用已经废弃的API函数。如果继续使用,可能会产生一些无法预料、未知的结果。
4、安全软件注入到软件中,注入模块发生了崩溃,直接导致软件发生崩溃
几年前遇到的一个客户问题,他们的Windows系统中安装了VPN软件,注入到我们的进程中,hook了网络通信的相关接口,以监控软件的网络数据包的收发,其中hook的recvfrom接口实现有bug,我们代码中有处调用recvfrom接口的地方传入了两个NULL参数(对于系统API函数recvfrom,传入NULL值是允许的),结果直接导致该注入模块产生了崩溃,进而导致了我们软件的崩溃。
到MSDN上查看套接字API函数recvfrom的说明,函数的最后两个参数是可选的,可以不传入,直接设置NULL就可以了,如下所示:
但客户VPN软件注入模块,将系统的recvfrom函数hook成了他们实现的recvfrom函数,在实现他们自己的recvfrom函数时,直接访问了recvfrom最后的两个参数,而我们的代码直接传入了NULL值:
这样在他们的recvfrom内部访问了NULL指针,触发了内存访问违例,导致VPN软件的注入模块发生崩溃,从而导致了我们整个程序的崩溃。
事实上,这个问题的排查难度远比此处文字描述的复杂,崩溃时的函数调用堆栈不完整(看不到套接字函数recvfrom的调用):
崩溃的注入库MinLSP.dll是第三方安全厂商的,我们拿不到该库的pdb文件,可能厂商也没有保存!
当时是使用IDA反汇编工具查看汇编代码,以及使用Windbg的dds命令,找出recvfrom函数调用的!限于篇幅,这个地方就没有完全展开了!
像这类出在第三方安全软件中的问题,必须要拿出足够的证据,证明问题是出在安全软件上,客户才会认可排查的结论,客户才会找第三方安全软件开发商反馈问题。对于本例中的问题,我们有个临时的规避办法,我们只要传入两个有效的参数即可,当然在对应的代码中,我们并不关心这两个参数在函数调用完成之后的返回值,不再传入两个NULL参数。
5、安全软件注入到软件中,注入模块发生了内存泄露,直接导致软件内存耗尽后发生闪退
有个客户在某台机器上运行的我们的软件,每次大概运行半个多小时后软件就会出现闪退崩溃,问题基本是必现的。软件运行一段时间后发生闪退崩溃,可能是内存泄漏引起的。有内存泄露的代码在频繁地执行,泄露的内存越来越多,接近或达到用户态虚拟内存的上限(32位程序默认的用户态虚拟内存位2GB),就会导致Out of memory内存耗尽的异常,程序就会发生闪退崩溃。
于是让客户重新运行软件,按照之前的操作步骤操作,然后在Windows任务管理器中持续地观察软件进程占用的内存情况,看看内存是否在持续地升高。之前我们讲过,Windows任务管理器中看不到进程占用的用户态虚拟内存,需要使用Process Explorer去查看。不过任务管理器中看内存的变化趋势是可以的,也能看到内存在持续增长的。我们推荐使用Process Explorer工具去查看虚拟内存占用。
经观察,软件中确实存在内存泄露,大概半个小时后内存就耗完了。然后就是使用工具去分析内存泄露的模块及位置了。当时选择Windbg去检测内存泄露,将Windbg附加到目标进程上去监测。至于如何使用Windbg去检测内存泄露,此处就不再赘述了,可以查看我的文章:
使用Windbg定位Windows C++程序中的内存泄漏https://blog.csdn.net/chenlycly/article/details/121295720 经分析,内存泄露发生在某个dll模块中,然后查看该dll模块的路径,发现是客户安装的某个安全软件的路径,即该dll模块是某安全软件中的。安全软件中的模块,怎么会跑到我们的软件进程空间中来的呢?答案只有一个,这个dll库是远程注入到我们的软件进程中的,安全软件正是通过这个注入模块对我们的软件进行监控的。
这个问题应该和客户机器的系统环境有关,如果是软件本身模块有内存泄漏,应该在公司内部测试环境中就发现了,因为半个小时左右就能复现,在公司环境中不用专门长时间拷机就能复现。
我们给客户的结论是,安全软件的注入模块有内存泄露,需要客户联系安全软件的开发厂商去核实排查一下。但客户有些不认可我们的结论,他们给出的理由是,运行其他软件都没问题,为啥运行我们的软件就有内存泄露呢?后来将发生泄漏模块所属的安全软件卸载掉后,我们的软件运行就没有泄露了,所以基本确定泄露和这个安全软件有关,客户才愿意承认可能是安全软件引起的。然后联系了安全软件厂商,协调了他们的相关开发人员,然后创建了讨论组。
经排查得知,安全软件在拦截UDP数据包进行分析时有内存泄露,而我们的软件在加入会议后,源源不断的音音视频码流都是使用UDP传输的,所以导致内存一直在持续的泄露,然后内存很快就耗尽,程序发生闪退了。
至于软件是如何实现远程注入的,可以查看《Windows核心编程》一书中的第22章(DLL注入与API拦截)的内容。
6、最后
本文详细讲述了几个项目中遇到的远程注入对软件产生影响的问题实例,有很强的实战参考价值,希望能给大家提供一定的借鉴或参考。