Yet Another Crash Analysis
手头有这么一个 bug,当启用 application verifier 之后,用键盘 navigate 菜单时会 crash。我在自己的机器上没能重现。 今天终于找到机会登陆到 qa 的机器上查这个问题。不错,在她的机器上能够很稳定的重现。
先把 pdb 下载下来添加到本地 symbol server, 在 qa 的机器上设置好 _NT_SYMBOL_PATH
环境变量。直接把 windbg 打开,开始调试。
这是 crash 的时候的调用栈等信息:
apMenu!CSkinMenu::GetCurSel+0x4:
22ec57df ff761c push dword ptr [esi+1Ch] ds:0023:28b7afc4=????????
0:000> kL
ChildEBP RetAddr
0012f224 22ec6772 apMenu!CSkinMenu::GetCurSel+0x4
0012f2d4 22ec9154 apMenu!CSkinMenu::WindowProc+0x282
0012f304 76e11a10 apMenu!CSubclassWnd::HookWndProc+0x90
0012f330 76e11ae8 USER32!InternalCallWinProc+0x23
0012f3a8 76e11c03 USER32!UserCallWinProcCheckWow+0x14b</pre>
查看一下GetCurSel 函数的代码:
<pre>0:000> uf apMenu!CSkinMenu::GetCurSel
apMenu!CSkinMenu::GetCurSel:
22ec57db 56 push esi
22ec57dc 57 push edi
22ec57dd 8bf1 mov esi,ecx
22ec57df ff761c push dword ptr [esi+1Ch] // crash 在这里
22ec57e2 ff158cb2ec22 call dword ptr [apMenu!_imp__GetMenuItemCount (22ecb28c)]
22ec57e8 8bf8 mov edi,eax
22ec57ea eb14 jmp apMenu!CSkinMenu::GetCurSel+0x25 (22ec5800)
apMenu!CSkinMenu::GetCurSel+0x11:
22ec57ec 6800040000 push 400h
22ec57f1 4f dec edi
22ec57f2 57 push edi
...
this 指针有问题. 既然只有加载 appverifier 才能重现,十有八九是 这个对象被删除了. 看看这个地址的属性:
0:000> !address @esi
ProcessParametrs 0015a808 in range 0015a000 0015b000
Environment 26f3c448 in range 26f3c000 26f3d000
28aa0000 : 28b79000 - 00003000
Type 00020000 MEM_PRIVATE
State 00002000 MEM_RESERVE // 的确不是 committed 的状态. 应该是被删除了.
Usage RegionUsagePageHeap
Handle 04721000
通过 review 代码应该能找出来是在哪里被删除的,但我对这块逻辑不熟悉,还是让 windbg 来找找吧. 这里我们需要使用 gflags 启用这个进程的 stack trace.
C:\Program Files\Debugging Tools for Windows (x86)>gflags /i app.exe +ust
Current Registry Settings for app.exe executable are: 00001100
vrf - Enable application verifier
ust - Create user mode stack trace database
重起程序, 重现 crash,用 !heap 命令就可以看到这个地址到底是在什么时候被删除的。
0:000> !heap -p -a @esi
address 28b7afa8 found in
_DPH_HEAP_ROOT @ 4721000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
28a2b9c0: 28b7a000 2000
774fd9fa ntdll!RtlpFreeHeap+0x0000005f
774e1c21 ntdll!RtlFreeHeap+0x0000014e
72d6bb0b vfbasics!AVrfpRtlFreeHeap+0x0000016b
75df7a7e kernel32!HeapFree+0x00000014
705d38bb MSVCR90!free+0x000000cd
22ec5ed2 apMenu!CSkinMenu::`vector deleting destructor'+0x00000017
22ec81fc apMenu!CSkinMenuMgr::Unskin+0x00000093
22ec86c8 apMenu!CSkinMenuMgr::OnCallWndProc+0x000000fe
22ec8807 apMenu!CHookMgr<CSkinMenuMgr>::CallWndProc+0x00000056
76e03617 USER32!DispatchHookW+0x00000033
76df4e8b USER32!fnHkINLPCWPSTRUCTW+0x0000004f
76e0933f USER32!__fnINOUTLPWINDOWPOS+0x00000027
...
原来是在调用一个 Unskin() 函数的时候删除的。看一下源文件,还有一个对应的 Skin(), 其中会 new 出这个对象,分别在这两个函数放两个断点, 反汇编一下这个函数,就知道应该在哪里放断点了.
0:000> uf apMenu!CSkinMenuMgr::Skin
apMenu!CSkinMenuMgr::Skin:
22ec84ac 6a04 push 4
22ec84ae b889a5ec22 mov eax,offset apMenu!_clean_type_info_names_internal+0x4f1 (22eca589)
22ec84b3 e820100000 call apMenu!_EH_prolog3 (22ec94d8)
22ec84b8 8bf1 mov esi,ecx
22ec84ba ff7508 push dword ptr [ebp+8]
22ec84bd e854d2ffff call apMenu!CSkinMenu::IsMenuWnd (22ec5716)
22ec84c2 59 pop ecx
22ec84c3 85c0 test eax,eax
22ec84c5 0f84a1000000 je apMenu!CSkinMenuMgr::Skin+0xc0 (22ec856c)
... ...
apMenu!CSkinMenuMgr::Skin+0x35:
22ec84e1 6a58 push 58h
22ec84e3 e82e120000 call apMenu!operator new (22ec9716) // 这里是 new, eax 就是返回值
22ec84e8 59 pop ecx
22ec84e9 8bc8 mov ecx,eax
22ec84eb 894df0 mov dword ptr [ebp-10h],ecx
...
那就在 22ec84e8 这一行放断点吧:
0:000>bp 22ec84e8 '.printf \'skin \';r eax;g'
类似的, 在 unskin 函数 delete 之前也放一个
0:000>bp 22ec819f '.printf \'unskin\';r eax;g'
放好之后是这样的:
0:000> bl
0 e 22ec819f 0001 (0001) 0:**** apMenu!CSkinMenuMgr::Unskin+0x36 ".printf \"unskin \";r eax;g"
1 e 22ec84e8 0001 (0001) 0:**** apMenu!CSkinMenuMgr::Skin+0x3c ".printf \"skin \";r eax;g"
重启进程,重现:
skin eax=2d3ddfa8 // 第一次 skin new 了一个对象
unskin eax=2d3ddfa8 // 第一次 unskin 删除了这个对象
unskin eax=00000000 // 这次 unskin 没有作用,因为 对象已经清空了
unskin eax=00000000 // 同上
skin eax=2d40dfa8 // 第二次 skin
(e40.e74): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000027 ecx=2d3ddfa8 edx=00000030 esi=2d3ddfa8 edi=00000000
eip=22ec57df esp=0012f220 ebp=0012f2d4 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210246
apMenu!CSkinMenu::GetCurSel+0x4:
22ec57df ff761c push dword ptr [esi+1Ch] ds:0023:2d3ddfc4=???????? // 这里试图访问第一次 new 出来的那个对象
可以看到问题就在第二次的时候访问第一次已经删除了的对象。 分析到这里也差不多了 让负责这个模块的 dev 去查吧。