<div id="js_content">
<p><img alt="" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-9ee48913c713c2c1398950a5f0e2cda1"></p>
</div>
<figcaption>
</figcaption>
<h2>缘起</h2>
<p>最近,接连在项目中遇到了两个界面无响应的问题。都只发生在客户特定机器上,不方便直接调试,只能抓取 <code>dump</code> 进行事后分析了。</p>
<h2>抓取 dump</h2>
<p>远程连上可以重现问题的机器,使用 <code>process explorer</code> 初步观察卡死的进程,发现 <code>CPU</code> 占用率很低,经过一段时间的观察,基本确定是一个死锁问题。在卡死的进程上右键,保存完整转储,压缩,发回本地进行分析。</p>
<h2>使用 windbg 进行分析</h2>
<p>双击抓取的 <code>dump</code> 文件,因为之前已经执行过 <code>windbg.exe -IA</code>,所以默认会通过 <code>windbg</code> 打开 <code>dump</code> 文件。先使用 <code>~*kvn</code> 粗略浏览一下每个线程的调用栈(因为比较长,这里就不截图了)。经过观察,很快锁定了两个值得进一步查看的线程:一个是主线程(界面线程),因为是界面无响应,肯定要关注界面线程。另外一个是 <code>7</code> 号工作线程。分别看一下这两个线程的调用栈。</p>
<p>主线程的调用栈如下图所示:</p>
<p><img alt="" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-2b8ce24b50af72016297f9a4cfab95da"></p>
<p>注意上图红色高亮部分,主线程通过 <code>SleepConditionVariableCS()</code> 进入等待。</p>
<p>看完主线程,再看 <code>7</code> 号工作线程的调用栈,如下图所示:</p>
<p><img alt="" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-70106bfb2e5f7d1b2ac6efed657457bb"></p>
<p><code>7</code> 号线程对 <code>SendMessage()</code> 的调用非常值得怀疑。</p>
<p>猜测整个流程是这样的:主线程不知由于什么原因进入等待状态,而工作线程由于各种各样的原因也进入了等待状态。其中 <code>7</code> 号线程最明显,因为它正在发消息,而主线程此时是无论如何也不会响应这个消息的。</p>
<p>于是,典型的死锁再一次发生了。</p>
<p><img alt="" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-44858e11826d06c4203911ebd12d060d"></p>
<p>加载 <code>AssemblyDesign_Tools.dll</code> 的符号后,查看对应的源码,消除对 <code>SendMessage()</code> 的调用,问题解决!<code>so easy!</code></p>
<blockquote>
<p><em>说明:工作线程中并没有直接调用 <code>SendMessage()</code>,而是调用了操作界面的相关 <code>API</code>,间接调用了 <code>SendMessage()</code> 给界面线程发消息。</em></p>
</blockquote>
<p>死锁的问题解决了,但是为什么向主线程发个消息就死锁了呢?秉着打破砂锅问到底的原则,我又开始折腾了。下面的内容适合喜欢调试逆向的极客阅读。</p>
<h2>深入调查</h2>
<p>最开始的思路是:查看主线程在等待的条件变量,然后再调查哪个工作线程会唤醒这个条件变量。奈何 <code>64</code> 位下,前四个参数通过寄存器 <code>RCX, RDX, r8, r9</code> 进行传递,如果这些寄存器没有在栈上存储一份的话,很难查看具体的值。折腾一番后,确实没找到有用的信息,而且就算找到了,也很难找出是哪个线程会执行唤醒操作。</p>
<p>这个死锁问题不像关键段死锁解决起来那么直接。不能直接通过命令(<code>!cs -l</code>),或者查看调用栈就能直接理出头绪。看来只能硬着头皮逆向分析相关代码了。</p>
<p><code>0</code> 号线程和 <code>7</code> 号线程最值得怀疑,其它线程基本可以排除。先看看主线程为什么会等待吧。</p>
<h2>主线程逻辑</h2>
<p>找到调用 <code>BentleyG!Bentley::BeConditionVariable::WaitOnCondition()</code> 的地方,也就是 <code>5</code> 号栈帧。</p>
<p><img alt="" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-37dc5b388133fdc41508296f5832244f"></p>
<p>在 <code>IDA</code> 中打开 <code>MobileDgn.dll</code>,并找到这个函数,然后按下神奇的 <code>F5</code>:</p>
<p><img alt="" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-d5c5792838110c43048f77ca0595652a"></p>
<figcaption>
可以看到,主线程在陷入等待之前,向工作线程发送了一个任务,也就是
<code>sub_7FEDAC749A0</code>,传递的参数是
<code>v5</code>。
<code>v5</code> 偏移
<code>88</code> 的位置保存了
<code>BeConditonVariable</code> 类型的变量,也就是
<code>WaitOnCondition()</code> 所等待的变量。猜测:
<code>sub_7FEDAC749A0</code> 内部会唤醒这个
<code>BeConditionVariable</code> |
|