"卡死"本文指的是程序永久无响应。出现这种状况一般有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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注