10

记一次 .NET某医疗器械清洗系统 卡死分析

 1 year ago
source link: https://www.cnblogs.com/huangxincheng/p/17328225.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1. 讲故事

前段时间协助训练营里的一位朋友分析了一个程序卡死的问题,回过头来看这个案例比较经典,这篇稍微整理一下供后来者少踩坑吧。

二:WinDbg 分析

1. 为什么会卡死

因为是窗体程序,理所当然就是看主线程此时正在做什么? 可以用 ~0s ; k 看一下便知。


0:000> k
 # ChildEBP RetAddr      
00 00aff168 75e3bb0a     win32u!NtUserPeekMessage+0xc
01 00aff168 75e3ba7e     USER32!_PeekMessage+0x2a
02 00aff1a4 6a5d711c     USER32!PeekMessageW+0x16e
03 00aff1f0 6a5841a6     System_Windows_Forms_ni+0x23711c
...
17 00afffbc 00000000     ntdll!_RtlUserThreadStart+0x1b

从线程栈来看,当前的方法卡在 win32u!NtUserPeekMessage 上, 熟悉 Windows 窗体消息的朋友都知道这是提取 消息队列 的常规逻辑,这个方法的下一步就是通过 Wow64SystemServiceCall 进入到 Windows内核态,可以用 u 命令验证一下。


0:000> ub win32u!NtUserPeekMessage+0xc
761d1010 b801100000      mov     eax,1001h
761d1015 ba10631d76      mov     edx,offset win32u!Wow64SystemServiceCall (761d6310)
761d101a ffd2            call    edx

朋友也给我截了图,确实出现了卡死,那接下来的问题就是看下当前线程在 内核态 到底在做什么?

2. 真的卡在内核态吗

幸好朋友可以在卡死的机器上安装 windbg,让朋友在卡死的时候使用 Attch to kernel 的方式观察内核态,截图如下:

214741-20230418081643723-1386205738.png

附加成功后,可以用 !process 0 f xxxx.exe 看到主线程的线程栈。


lkd> !process 0 f xxxx.exe
PROCESS ffffab8ebea75080
    SessionId: 1  Cid: 0f78    Peb: 009f1000  ParentCid: 1134
        ...
        THREAD ffffab8ecad14540  Cid 0f78.38f8  Teb: 00000000009f3000 Win32Thread: ffffab8ecd5dabc0 WAIT: (WrUserRequest) UserMode Non-Alertable
            ffffab8ecb31bcc0  QueueObject
        IRP List:
            ffffab8ecad82b20: (0006,0478) Flags: 00060000  Mdl: 00000000
        Not impersonating
        DeviceMap                 ffffd400aa7eed50
        Owning Process            ffffab8ebea75080       Image:         xxxx.exe
        Attached Process          N/A            Image:         N/A
        Wait Start TickCount      1117311        Ticks: 9265 (0:00:02:24.765)
        Context Switch Count      60628          IdealProcessor: 2  NoStackSwap
        UserTime                  00:00:10.796
        KernelTime                00:00:06.593
        Win32 Start Address 0x00000000006e16aa
        Stack Init ffffa88b5b18fb90 Current ffffa88b5b18e780
        Base ffffa88b5b190000 Limit ffffa88b5b189000 Call 0000000000000000
        Priority 10 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
        Child-SP          RetAddr               Call Site
        ffffa88b`5b18e7c0 fffff806`6627e370     nt!KiSwapContext+0x76
        ffffa88b`5b18e900 fffff806`6627d89f     nt!KiSwapThread+0x500
        ffffa88b`5b18e9b0 fffff806`6627d143     nt!KiCommitThreadWait+0x14f
        ffffa88b`5b18ea50 fffff806`6628679b     nt!KeWaitForSingleObject+0x233
        ffffa88b`5b18eb40 ffffa9d4`bdd32b12     nt!KeWaitForMultipleObjects+0x45b
        ffffa88b`5b18ec50 ffffa9d4`bdd352d9     win32kfull!xxxRealSleepThread+0x362
        ffffa88b`5b18ed70 ffffa9d4`bdd33f8a     win32kfull!xxxInterSendMsgEx+0xdd9
        ffffa88b`5b18eee0 ffffa9d4`bdd37870     win32kfull!xxxSendTransformableMessageTimeout+0x3ea
        ffffa88b`5b18f030 ffffa9d4`bdf1e088     win32kfull!xxxSendMessage+0x2c
        ffffa88b`5b18f090 ffffa9d4`bdf1e0e9     win32kfull!xxxCompositedTraverse+0x40
        ffffa88b`5b18f0e0 ffffa9d4`bdf1e0e9     win32kfull!xxxCompositedTraverse+0xa1
        ffffa88b`5b18f130 ffffa9d4`bdf1e0e9     win32kfull!xxxCompositedTraverse+0xa1
        ffffa88b`5b18f180 ffffa9d4`bdf1e0e9     win32kfull!xxxCompositedTraverse+0xa1
        ffffa88b`5b18f1d0 ffffa9d4`bdf1e2a7     win32kfull!xxxCompositedTraverse+0xa1
        ffffa88b`5b18f220 ffffa9d4`bde5a013     win32kfull!xxxCompositedPaint+0x37
        ffffa88b`5b18f2b0 ffffa9d4`bdd2e438     win32kfull!xxxInternalDoPaint+0x12bce3
        ffffa88b`5b18f300 ffffa9d4`bdd2e03a     win32kfull!xxxInternalDoPaint+0x108
        ffffa88b`5b18f350 ffffa9d4`bdd30f1c     win32kfull!xxxDoPaint+0x52
        ffffa88b`5b18f3b0 ffffa9d4`bdd2ff08     win32kfull!xxxRealInternalGetMessage+0xfac
        ffffa88b`5b18f880 ffffa9d4`be1871ce     win32kfull!NtUserPeekMessage+0x158
        ffffa88b`5b18f940 fffff806`6640d8f5     win32k!NtUserPeekMessage+0x2a
        ffffa88b`5b18f990 00007ffe`1816ff74     nt!KiSystemServiceCopyEnd+0x25 (TrapFrame @ ffffa88b`5b18fa00)
        00000000`0077e558 00000000`00000000     0x00007ffe`1816ff74

如果线程信息很少的话,可以用 .process 将此进程作为当前上下文,然后加载用户符号,输出如下:


lkd> .process ffffab8ebea75080
Implicit process is now ffffab8e`bea75080
lkd> .reload
Connected to Windows 10 19041 x64 target at (Tue Mar 21 13:21:21.213 2023 (UTC + 8:00)), ptr64 TRUE
Loading Kernel Symbols
...............................................................
................................................................
................................................................
.................
Loading User Symbols
PEB is paged out (Peb.Ldr = 00000000`009f1018).  Type ".hh dbgerr001" for details
Loading unloaded module list

从刚才的线程栈上看,很明显有一个 win32kfull!xxxSendMessage+0x2c 方法,熟悉 SendMessage 的朋友都知道这个是用来向某个窗体发消息的,那到底是哪一个窗体呢?

3. 到底给哪个窗体发消息

要想获取发送窗体的句柄,需要提取 win32kfull!xxxSendMessage 方法的第一个参数,在 x64 的调用协定下,它是用 rcx 传递的,需要分析下汇编代码,如果 rcx 没有放到栈里,那就无法提取了。

为了少点麻烦,建议让朋友看下 32bit 的操作系统上是否也有这个问题?结果反馈说也存在,使用 !thread xxx 切到目标线程,使用 kb 提取第一个参数地址上的值,即:00010598,截图如下:

214741-20230418081643696-1329249374.png

丢了一个 sdbgext 插件让朋友看下窗体句柄信息,发现是个 64bit 的,其实除了它还可以用 Spy++ 观察窗体句柄,重点就是找到这个神秘窗体 是由哪个进程下的线程创建的,当把句柄号丢进去后还真给找到了,有点黑暗中寻找到了曙光。截图如下:

214741-20230418081643681-208225081.png

从 Spy++ 看当前窗体是由进程号:000016E0下的线程号0000109C 创建的,经过比对,这个线程就是本进程的某个线程号。

分析到这里其实就很明朗了,是因为这个线程 0000109C 创建了一个用户控件,导致内核态 在某种情况下给它发消息,接下来就是寻找到底是什么控件创建的。

4. 罪魁祸首

关于非主线程创建用户控件导致的卡死,我感觉都已经说破嘴皮了,还是有非常多的人犯这个毛病,无语哈,解决办法就是用 bp 去拦截 System.Windows.Forms.Application+MarshalingControl..ctor 方法,具体方案可参考我的文章:【一个超经典 WinForm 卡死问题的再反思】https://www.cnblogs.com/huangxincheng/p/16868486.html

接下来就是朋友的苦苦调试,终于给找到了,截图如下:

214741-20230418081643692-1364955207.png

对,就是这么一句 Intptr handle =this.Handle 代码,内核句柄的获取让它在这个线程上生根了。

就是这么一句代码,来来回回兜了好几圈,花费了朋友个把星期,终于给解决了,也算是一个好结果吧,这个案例需要实时观察程序的内核态用户态,看 dump 效果不大,造成了这么多时间的浪费。

相信这个案例也让公司老板对他 刮目相看


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK