首先有主站的运营同学反馈某个用户的投稿工具一选择上传视频就崩溃,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 被延迟加载了,但是系统还是能够找到的。这种情况下的无法加载,我能想到的大概只有:

  1. 这个 DLL 某个依赖的 DLL 找不到
  2. 加载行为被 HOOK 干掉了

至于具体是哪个就不得而知了。

不过起码我们找到了问题不是么…..

完整的 dump 和 PDB 包可以在这里下载:链接: https://pan.baidu.com/s/1i5xjGt3 密码: nnsh

不要问我为什么是百度网盘。。。现在能够方便做外链的免费网盘没几家了啊