"卡死"本文指的是程序永久无响应。出现这种状况一般有3个原因:
遇到这样的问题该如何排查?往往让人无从下手。常规的办法就是不断增加日志,然后逼近现场,但是如果现场偶发,则这个过程会比较漫长。而且有的时候卡死往往是系统级的,日志可能都不知道写哪里。碰到此类问题往往让人崩溃。
有没有一招制敌的秘籍呢?看了下面死锁案例破解过程,你可能就会觉得这类问题也是小菜一碟了。
案例:测试现场在执行日初始化时界面卡死。
开发已经定位到某个线程在准备调用 A.dll 的一个导出函数 B 时,加载 A.dll 卡死。虽然现场是必现的,但是由于是在加载 dll 时卡死,开发连加日志的地方都找不到。现在就可以参考一下案例中的破解过程。
【准备工具】
1、-- 抓现场 dump 的工具。
2、-- 分析 dump 的工具。 如何使用这里不作详细介绍。
3、-- 由于 的程序没有符号文件,所以需借助 将调用栈地址转换成源代码。
【获取现场】
1、用 工具抓取卡死进程的 dump。
2、DOS 界面执行命令, -ma 进程 ID 保存路径。
【打开dump分析】
一、首先确定主线程的进程
输入 kv 命令
最后的调用堆栈:
ntdll!ion 主线程在尝试进入临界区。
ntdll!t 然后等待内核对象。
可以看出主线程应该是发生死锁了。
往上回溯调用栈 rtl60!$qqrv+0xc
这个是 的 bpl 导出函数,bpl 导出函数可以直接通过名称就对应到源代码。
继续往前回溯
通过导出函数名字可以直接定位到函数 .
但是偏移 0x41 代表哪一行呢?
提取返回地址:
即
实际地址是
然后 建一个空工程,带包 vclwindbg,任意找一行代码下断点。
运行到断点windbg,菜单 view → Debug → CPU 打开 CPU 窗口。
Goto ... → 输入地址 $
对应源代码是
定位到函数
里面 调用了锁,由此看来,主线程在消息循环时,卡死在 锁上。
二、谁占用锁
ion 第一个参数 是临界对象地址
输入指令!cs 可以获得该临界的详细信息
其中 指是该临界被哪个线程所持有
31b0 是持有临界的线程 ID,对应 53 号线程
三、53号线程的进程排查
输入命令 ~53 kv,显示 53 号线程调用栈
调用栈 vcl60!teWnd$qqrv+0x12d,可以直接定位到代码
源代码 和调用栈 !+0x15 吻合
这说明 53 号线程正在创建一个 类型的窗口
四、谁是 类型的窗口
由于 vcl 和 rtl 代码无法确定创建了什么窗口,因此堆栈往上回溯,进入业务函数
因为 没有符号文件,不像 bpl 一样有详细的导出函数,因此调用栈只是定位到模块名称和一个很大的地址偏移,显而易见,这两个地址是无法直接定位到原代码。很多人往往会在这里卡住而无法继续深入分析。
五、如何通过地址手工定位原代码
很多情况下我们可以得到程序运行时的某个程序地址,比如:
这种情况下,如果我们可以知道这个地址对应的源代码,相信问题应该很容易解决。
方法一:偏移地址计算法
这个需要源代码和 dump 的版本完全一致;
打开 调试状态,手工计算;
!+ 地址;
然后在 CPU 界面定位到该地址,然后和 dump 比较汇编代码;
如果相同,则定位的源代码是正确的,否则这个办法走不通。
方法二:特征代码搜索法
所谓特征代码搜索法,是基于这样一个事实,DLL 在进程中,无论基地址发生什么变化,有些二进制代码是不会发生变化的,这些不变的二进制代码就可以作为特征码,从而用来搜索定位源代码。
那么哪些汇编代码是不会发生变化的呢?只要看汇编代码不包含具体地址,就不会在DLL重定向中发生变化
特征代码搜索法的优点:源代码和 dump 版本不一致问题也不大,只要出问题的那小段代码没有改变,是可以定位到的。另外如果 C++ 的 DLL 符号文件缺失,或者不一致的情况下也可以启用这种办法定位代码。
下面在这个案例中演示下这种方法。
查看 dump 中 !+ 的汇编代码。
类似下面红框中的代码都是没有地址信息可以作为特征码。
类似下面的红框中的包含地址的则不可以作特征码。
另外选择特征码需具备唯一性,即在进程中,这段代码需要不容易重复,一般来说长度越长越不容易重复。
上面的汇编代码中,!+ 对应的地址是 ,由于这个地址是调用返回地址,因此函数目标地址是前一个地址 。现在的目标是要定位到这个地址对应的源代码。
我们选择这个目标地址前面的 64 ff 30 64 89 20 83 2d 作为特征码。
打开 开启 新进程(进程只要启动起来就可以),PID=2668
启动新 , 到进程 2668,选择非侵入式。
执行命令如下:
s -b 64 ff 30 64 89 20 83 2d
找到四个结果,经比对地址 前后的汇编代码和 dump 的汇编前面是一致的。
下面截图左边是新进程,右边是 dump,可以看到两边的汇编代码是完全一致的,由于目前找的地址不是特征码的地址,而是目标地址,因此定位到新进程的目标地址是 。
CPU 界面定位地址 $。
找到 .pas 1561 源代码。
可以看到单元的初始化在创建一个窗体。这个初始化是 dll 加载时自动执行的,和现场吻合。经检查该窗体也的确是 类型的。
至此卡死原因已豁然开朗,找到了原因接下来的工作就简单了。
六、程序为什么会卡死
53号日初始化线程的执行轨迹如下:
加载,执行单元初始化 →
创建窗口 →
先锁定 →
→ . →
主线程发生交互
主线程的执行轨迹如下:
消息循环 →
→
等待 锁
死锁由此发生。而直接原因是因为线程去操作界面。
之所以标题称为破解,是因为从上面的排查过程来看,用这种方法,不用熟悉模块代码,不用了解业务场景,反向行之,只要捕捉到一次现场,就可以由现场而还原到代码环境,一击而中,无所遁形。
限时特惠:本站持续每日更新海量各大内部创业课程,一年会员仅需要98元,全站资源免费下载
点击查看详情
站长微信:Jiucxh