首先有主站的运营同学反馈某个用户的投稿工具一选择上传视频就崩溃,100% 重现。
要到 crash dump 之后挂上 windbg,首先用 lmvm bililive 检查一下用户使用的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Image path: D:\ugc_assistant\2.0.0.1054\bililive.dll Image name: bililive.dll Browse all global symbols  functions  data Timestamp:        Fri Sep  1 17:53:54 2017 (59A92E32) CheckSum:         00A12C47 ImageSize:        00A34000 File version:     2.0.0.1054 Product version:  2.0.0.1054 File flags:       0 (Mask 17) File OS:          4 Unknown Win32 File type:        1.0 App File date:        00000000.00000000 Translations:     0409.04b0 CompanyName:      bilibili ProductName:      bilibili投稿工具 InternalName:     bililive_dll OriginalFilename: bililive.dll 
 
拿到版本之后从构建机上拷出对应的 PDB,设置路径,重新加载符号。
因为投稿工具有大量的工作线程,所以紧接着用 ~# 检查发生异常的线程:
1 2 3 . 18  Id: 1a68.2620 Suspend: 0 Teb: fff72000 Unfrozen       Start: bililive!base::`anonymous namespace'::ThreadFunc (5da3abd0)       Priority: 0  Priority class: 32  Affinity: f 
 
崩溃发生在一个工作线程,接下来用 ~18s 将这个线程设置为当前线程,并且用 .ecxr 切换到异常 context 上。
因为已经有 PDB 了,所以直接用 kPL 把 callback 附加对应的源码参数显示出来,同时不显示对应的源码文件和行号(因为对源码比较熟悉,定位到第一现场后可以直接调到源文件,全都打印出来反而容易干扰输出):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30  # ChildEBP RetAddr 00 084ff4c0 5dea879f bililive!_invalid_parameter_noinfo(void)+0xc 01 084ff4d8 5da6c8d9 bililive!rand_s( 			unsigned int * _RandomValue = 0x084ff59c)+0x82 02 084ff5a0 5da6c936 bililive!`anonymous namespace'::RandUint32(void)+0x19 03 084ff5a8 5da51f65 bililive!base::RandUint64(void)+0x6 04 084ff670 5da52014 bililive!base::RandGenerator( 			unsigned int64 range = 0x80000000)+0x95 05 084ff73c 5da1c54e bililive!base::RandInt( 			int min = 0n0, 			int max = 0n2147483647)+0x84 06 084ff8a8 5da1bfa3 bililive!bililive::VideoUploadTask::DoUpload(void)+0x22e 07 084ff9b0 5da36c46 bililive!bililive::VideoUploadTask::DoStart(void)+0x3b3 08 084ffaf4 5da3611d bililive!base::MessageLoop::RunTask( 			struct base::PendingTask * pending_task = 0x084ffb44)+0x346 09 084ffb88 5da56fba bililive!base::MessageLoop::DoWork(void)+0x1cd 0a 084ffb94 5da579dd bililive!base::MessagePumpForIO::DoRunLoop(void)+0x7a 0b 084ffbb4 5da3686d bililive!base::MessagePumpWin::Run( 			class base::MessagePump::Delegate * delegate = 0x084ffcc8)+0x3d 0c 084ffc7c 5da380e3 bililive!base::MessageLoop::RunInternal(void)+0x9d 0d 084ffc84 5da367a6 bililive!base::RunLoop::Run(void)+0x13 0e 084ffca8 5da3afeb bililive!base::MessageLoop::Run(void)+0x16 0f 084ffcb0 5da3b462 bililive!base::Thread::Run( 			class base::MessageLoop * message_loop = 0x084ffcc8)+0xb 10 084ffd7c 5da3ac25 bililive!base::Thread::ThreadMain(void)+0xd2 11 084ffd90 751633ca bililive!base::`anonymous namespace'::ThreadFunc( 			void * params = 0x000006ec)+0x55 12 084ffd9c 771d9ed2 kernel32!BaseThreadInitThunk+0xe 13 084ffddc 771d9ea5 ntdll!__RtlUserThreadStart+0x70 14 084ffdf4 00000000 ntdll!_RtlUserThreadStart+0x1b 
 
发现异常是由 base::RandUint64() 内部调用的 CRT 的 rand_s() 触发的(这里 CRT 被静态链接了),并且看 _invalid_parameter_noinfo 的名字,很像是不满足函数参数一类的。
根据 MSDN 这里的描述 ,rand_s() 仅会在参数指针为空时,才会往外抛异常,然而这里我们明显能看出来,参数不为空。
既然这样,那我们就跳到 stack frame 00 对应的源码去看看,然而……
正值某国开会,对境外站点的访问遭到了封锁,不能从微软的服务器拖对应的 PDB….
死马当活马医,既然如此就考虑通过反汇编入手,看看能否找到 root cause。
先利用 uf bililive!rand_s 反汇编整个函数,还好不是很长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 0:018> uf bililive!rand_s bililive!rand_s [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 60]:    60 5dea871d 55              push    ebp    60 5dea871e 8bec            mov     ebp,esp    60 5dea8720 51              push    ecx    60 5dea8721 53              push    ebx    60 5dea8722 56              push    esi    61 5dea8723 ff359c171a5e    push    dword ptr [bililive!g_pfnRtlGenRandom (5e1a179c)]    61 5dea8729 ff154c64fd5d    call    dword ptr [bililive!_imp__DecodePointer (5dfd644c)]    61 5dea872f 8bd8            mov     ebx,eax    62 5dea8731 8b4508          mov     eax,dword ptr [ebp+8]    62 5dea8734 85c0            test    eax,eax    62 5dea8736 7516            jne     bililive!rand_s+0x31 (5dea874e)  Branch bililive!rand_s+0x1b [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 62]:    62 5dea8738 e87297ffff      call    bililive!_errno (5dea1eaf)    62 5dea873d 6a16            push    16h    62 5dea873f 5e              pop     esi    62 5dea8740 8930            mov     dword ptr [eax],esi    62 5dea8742 e8eb61ffff      call    bililive!_invalid_parameter_noinfo (5de9e932)    62 5dea8747 8bc6            mov     eax,esi    62 5dea8749 e9cf000000      jmp     bililive!rand_s+0x100 (5dea881d)  Branch bililive!rand_s+0x31 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 63]:    63 5dea874e 832000          and     dword ptr [eax],0    63 5dea8751 57              push    edi    65 5dea8752 85db            test    ebx,ebx    65 5dea8754 0f85a3000000    jne     bililive!rand_s+0xe0 (5dea87fd)  Branch bililive!rand_s+0x3d [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 75]:    75 5dea875a 8b352464fd5d    mov     esi,dword ptr [bililive!_imp__LoadLibraryExW (5dfd6424)]    75 5dea8760 6800080000      push    800h    75 5dea8765 53              push    ebx    75 5dea8766 bb4067135e      mov     ebx,offset bililive!`string' (5e136740)    75 5dea876b 53              push    ebx    75 5dea876c ffd6            call    esi    77 5dea876e 8b3d5464fd5d    mov     edi,dword ptr [bililive!_imp__GetLastError (5dfd6454)]    77 5dea8774 8945fc          mov     dword ptr [ebp-4],eax    77 5dea8777 85c0            test    eax,eax    77 5dea8779 7528            jne     bililive!rand_s+0x86 (5dea87a3)  Branch bililive!rand_s+0x5e [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 77]:    77 5dea877b ffd7            call    edi    77 5dea877d 83f857          cmp     eax,57h    77 5dea8780 750e            jne     bililive!rand_s+0x73 (5dea8790)  Branch bililive!rand_s+0x65 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 81]:    81 5dea8782 6a00            push    0    81 5dea8784 6a00            push    0    81 5dea8786 53              push    ebx    81 5dea8787 ffd6            call    esi    81 5dea8789 8945fc          mov     dword ptr [ebp-4],eax    84 5dea878c 85c0            test    eax,eax    84 5dea878e 7513            jne     bililive!rand_s+0x86 (5dea87a3)  Branch bililive!rand_s+0x73 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 86]:    86 5dea8790 e81a97ffff      call    bililive!_errno (5dea1eaf)    86 5dea8795 6a16            push    16h    86 5dea8797 5e              pop     esi    86 5dea8798 8930            mov     dword ptr [eax],esi    86 5dea879a e89361ffff      call    bililive!_invalid_parameter_noinfo (5de9e932)    86 5dea879f 8bc6            mov     eax,esi    86 5dea87a1 eb79            jmp     bililive!rand_s+0xff (5dea881c)  Branch bililive!rand_s+0x86 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 89]:    89 5dea87a3 68c862105e      push    offset bililive!`string' (5e1062c8)    89 5dea87a8 50              push    eax    89 5dea87a9 ff152864fd5d    call    dword ptr [bililive!_imp__GetProcAddress (5dfd6428)]    89 5dea87af 8bd8            mov     ebx,eax    90 5dea87b1 85db            test    ebx,ebx    90 5dea87b3 7522            jne     bililive!rand_s+0xba (5dea87d7)  Branch bililive!rand_s+0x98 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 92]:    92 5dea87b5 e8f596ffff      call    bililive!_errno (5dea1eaf)    92 5dea87ba 8bf0            mov     esi,eax    92 5dea87bc ffd7            call    edi    92 5dea87be 50              push    eax    92 5dea87bf e8fe96ffff      call    bililive!_get_errno_from_oserr (5dea1ec2)    92 5dea87c4 59              pop     ecx    92 5dea87c5 8906            mov     dword ptr [esi],eax    92 5dea87c7 e86661ffff      call    bililive!_invalid_parameter_noinfo (5de9e932)    92 5dea87cc ffd7            call    edi    92 5dea87ce 50              push    eax    92 5dea87cf e8ee96ffff      call    bililive!_get_errno_from_oserr (5dea1ec2)    92 5dea87d4 59              pop     ecx    92 5dea87d5 eb45            jmp     bililive!rand_s+0xff (5dea881c)  Branch bililive!rand_s+0xba [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 94]:    94 5dea87d7 8b356462fd5d    mov     esi,dword ptr [bililive!_imp__EncodePointer (5dfd6264)]    94 5dea87dd 53              push    ebx    94 5dea87de ffd6            call    esi    95 5dea87e0 6a00            push    0    95 5dea87e2 8bf8            mov     edi,eax    95 5dea87e4 ffd6            call    esi   100 5dea87e6 b99c171a5e      mov     ecx,offset bililive!g_pfnRtlGenRandom (5e1a179c)   100 5dea87eb 8739            xchg    edi,dword ptr [ecx]   100 5dea87ed 3bf8            cmp     edi,eax   100 5dea87ef 7409            je      bililive!rand_s+0xdd (5dea87fa)  Branch bililive!rand_s+0xd4 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 109]:   109 5dea87f1 ff75fc          push    dword ptr [ebp-4]   109 5dea87f4 ff152c64fd5d    call    dword ptr [bililive!_imp__FreeLibrary (5dfd642c)] bililive!rand_s+0xdd [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 109]:   109 5dea87fa 8b4508          mov     eax,dword ptr [ebp+8] bililive!rand_s+0xe0 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 113]:   113 5dea87fd 6a04            push    4   113 5dea87ff 50              push    eax   113 5dea8800 ffd3            call    ebx   113 5dea8802 85c0            test    eax,eax   113 5dea8804 7514            jne     bililive!rand_s+0xfd (5dea881a)  Branch bililive!rand_s+0xe9 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 115]:   115 5dea8806 e8a496ffff      call    bililive!_errno (5dea1eaf)   115 5dea880b c7000c000000    mov     dword ptr [eax],0Ch   116 5dea8811 e89996ffff      call    bililive!_errno (5dea1eaf)   116 5dea8816 8b00            mov     eax,dword ptr [eax]   116 5dea8818 eb02            jmp     bililive!rand_s+0xff (5dea881c)  Branch bililive!rand_s+0xfd [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 118]:   118 5dea881a 33c0            xor     eax,eax bililive!rand_s+0xff [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 118]:   118 5dea881c 5f              pop     edi bililive!rand_s+0x100 [f:\dd\vctools\crt\crtw32\misc\rand_s.c @ 118]:   118 5dea881d 5e              pop     esi   118 5dea881e 5b              pop     ebx   119 5dea881f 8be5            mov     esp,ebp   119 5dea8821 5d              pop     ebp   119 5dea8822 c3              ret 
 
那么发生问题的是哪一行呢?往回看调用栈第一个栈帧
1 00 084ff4c0 5dea879f bililive!_invalid_parameter_noinfo(void)+0xc 
 
5dea879f  是第一个栈帧的 return address,也就是这个函数返回到父函数后要执行的下一条指令的地址,于是就能定位到
1 2 86 5dea879a e89361ffff      call    bililive!_invalid_parameter_noinfo (5de9e932) 86 5dea879f 8bc6            mov     eax,esi 
 
因为汇编的特殊性,比较好的选择是从问题的指令开始,往回倒推,着重小心涉及 branching 的指令。
NOTE:为了专注问题,这里跳过其他非相关的汇编代码
1 2 3 4 86 5dea8790 e81a97ffff      call    bililive!_errno (5dea1eaf) 86 5dea8795 6a16            push    16h 86 5dea8797 5e              pop     esi 86 5dea8798 8930            mov     dword ptr [eax],esi 
 
这里往 eax 指向的内存写入了 0x16,根据上下文,我猜 函数 bililive!_errno() 返回了 CRT errno 的地址,于是存到了 eax;翻一下文档,0x16 错误值代表的刚好是 EINVAL。
根据
1 2 84 5dea878c 85c0            test    eax,eax 84 5dea878e 7513            jne     bililive!rand_s+0x86 (5dea87a3)  Branch 
 
我们知道,在此之前 eax 等于 0(否则已经跳转到另外一个地址,就不会执行到出错的地方了)。
1 2 3 4 81 5dea8782 6a00            push    0 81 5dea8784 6a00            push    0 81 5dea8786 53              push    ebx 81 5dea8787 ffd6            call    esi 
 
前面 eax 为 0 是 esi 的执行结果,大概率是失败了(FALSE);这句直接把刚才 eax 分析的重要性给抹杀了,orz…
这里 esi 应该是某个函数指针,目前看有三个参数,最后两个都是 0 (别忘了 cdecl calling convention 下,参数从右往左压栈)
1 2 3 77 5dea877b ffd7            call    edi 77 5dea877d 83f857          cmp     eax,57h 77 5dea8780 750e            jne     bililive!rand_s+0x73 (5dea8790)  Branch 
 
这段比较有意思,因为无论 jne 是不是要跳,两个目的地都是我们刚才执行过的指令,这意味着,这里 eax 可能为 57h,也可能不是,不过还好不影响我们的主逻辑。
1 2 3 4 5 6 7 8 9 10 75 5dea875a 8b352464fd5d    mov     esi,dword ptr [bililive!_imp__LoadLibraryExW (5dfd6424)] 75 5dea8760 6800080000      push    800h 75 5dea8765 53              push    ebx 75 5dea8766 bb4067135e      mov     ebx,offset bililive!`string' (5e136740) 75 5dea876b 53              push    ebx 75 5dea876c ffd6            call    esi 77 5dea876e 8b3d5464fd5d    mov     edi,dword ptr [bililive!_imp__GetLastError (5dfd6454)] 77 5dea8774 8945fc          mov     dword ptr [ebp-4],eax 77 5dea8777 85c0            test    eax,eax 77 5dea8779 7528            jne     bililive!rand_s+0x86 (5dea87a3)  Branch 
 
首先 jne 不跳,因为目标地址不在执行范围内,这意味着 eax 为 0。
同时可以发现 edi 保存的是 GetLastError() 的地址;并且这里的 edi 和上一段汇编代码的 edi 是一个
esi 保存了 LoadLibraryExW() 的地址,并且这个调用失败了(因为 eax 为 0)。
并且可以发现,上面分析过的有段汇编,同样执行了一次 esi。
这里 LoadLibraryEx() 加载的函数名字的地址是 5e136740 ,用 db 查看一下发现是
1 2 5e136740  41 00 44 00 56 00 41 00-50 00 49 00 33 00 32 00  A.D.V.A.P.I.3.2. 5e136750  2e 00 44 00 4c 00 4c 00-00 00 00 00 00 00 00 00  ..D.L.L......... 
 
在往上看,函数就到头了,而且没有直接跳到异常上下文的指令,所以基本上可以确定这个异常由 LoadLibraryEx() 调用失败引发
整理一下逻辑就是:
首先调用 bililive!_imp__DecodePointer 失败,然后进行一系列的 fallback,例如利用 LoadLibraryEx() 加载 ADVAPI32.DLL 
如果第一次调用失败且 Last-Error-Code 是 57h,则换一套参数在调用一次 LoadLibraryEx(),否则就直接失败
但是这里无论是不是 57H,结果都一样,失败了…
既然是调用失败,那有必要看一下具体的 last error code 是啥。
直接用 !teb 看这个线程 TEB 的数据,里面就有 last-error-code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0:018> !teb TEB at fff72000     ExceptionList:        084fed8c     StackBase:            08500000     StackLimit:           084fe000     SubSystemTib:         00000000     FiberData:            00001e00     ArbitraryUserPointer: 00000000     Self:                 fff72000     EnvironmentPointer:   00000000     ClientId:             00001a68 . 00002620     RpcHandle:            00000000     Tls Storage:          03ef1910     PEB Address:          fffde000     LastErrorValue:       126     LastStatusValue:      c0000135     Count Owned Locks:    0     HardErrorMode:        0 
 
126 意味着 Error code: (Win32) 0x7e (126) - The specified module could not be found. 
用 lmo 可以看到
1 2 3 4 756a0000 75740000   advapi32   (deferred)     Mapped memory image file: d:\dbgsymbols\sys_symbols\advapi32.dll\4CE7B706a0000\advapi32.dll     Image path: C:\Windows\SysWOW64\advapi32.dll     Image name: advapi32.dll 
 
advapi32 被延迟加载了,但是系统还是能够找到的。这种情况下的无法加载,我能想到的大概只有:
这个 DLL 某个依赖的 DLL 找不到 
加载行为被 HOOK 干掉了 
 
至于具体是哪个就不得而知了。
不过起码我们找到了问题不是么…..
完整的 dump 和 PDB 包可以在这里下载:链接: https://pan.baidu.com/s/1i5xjGt3  密码: nnsh
不要问我为什么是百度网盘。。。现在能够方便做外链的免费网盘没几家了啊