问题场景
-
只用某一特定软件版本才能复现,小于该版本或大于该版本的都无法复现
-
只用poly sync 40作为HID设备,并在windows系统上,当选中poly 40作为当前麦克风设备时,几乎每次都crash。
-
不挑windows机器,用不同的两台windows,装alpha17都能复现。
详细排查过程
崩溃堆栈:
从栈顶看,memmove时,原地址esi = 0x2,这个地址显然不对,做内存拷贝时源地址有问题导致crash了,其他看不出有用信息,下载dmp加载符号解析:
这里可以看出来,是在HIDDeviceImpl::SetFeatureInner函数的局部变量ScopedFuncLogger析构时crash了,这个ScopedFuncLogger析构时原本是要打出exit HIDDeviceImpl::SetFeatureInner, cost xxx这种log的。
照理说,这个HIDDeviceImpl::SetFeatureInner是函数名,是用编译宏__FUNCTION__在构造ScopedFuncLogger对象时传给ScopedFuncLogger构造函数的,不可能有其他人改这个对象,但现在的确是析构时crash了,查看析构代码:
析构时,会访问_moduleName和 _functionName这两个成员变量,那是哪个变量被写坏了呢?
.frame 0b; dv; dt -b this可以查看当时所有的成员变量:
_functionName的内存布局:
_moduleName的内存布局:
可以看到_functionName的ptr刚好为0x2,和出错的内存地址对上了,而且_functionName的长度是MySize = 0x1e, 如果切到.frame 1, dv, 可以看到end - begin 刚好等于0x1e
所以目前可以确定是_functionName这个变量被写坏了。
从log中可以看到: entry HIDDeviceImpl::SetFeatureInner这条log是正常的,那就是执行这一个函数中被改了,可是如何确定是哪里被改了呢?
幸好,这个问题复现的概率比较高,用visual studio attach上去,在函数执行中每一行逐个加断点,定位到了一个地方:
经过多次调试确定:在执行到1时,_functionName还是正常的,但是执行到2的时候,_functionName中char的地址就变成0x2了。
一个奇怪的地方是,如果在1下断点,再走到2时就是正常的,不会crash,必须在2下断点之前,不能有任何断点,那到2时,_functionName就被写坏了。
也就是,断点1和断点2不能同时下?猜测是下断点导致程序暂停,暂停了就不会出现这个问题了。
那怎么确定一定是这里写坏了呢?
此时,可以借助windbg的下断点后,可以打印这一块附近的内存,然后再继续执行,这样就既可以看出内存的变化,又能保证程序一直在执行:
断点1:bp HIDDeviceImpl::WriteDataToDevice+0x375 ".printf \"WaitFor\";dd (ebp-0x4c) L256;g"
断点2:bp HIDDeviceImpl::WriteDataToDevice+0x45A ".printf \"GetOver\";dd eax L256;g"
通过.frame 11; dv, 得知_functionName的地址是0x0eefef38:
可以看出,在WaitForSignalObject前后,_functionName确实被改写!
WriteDataToDevice这个函数是在做写入HID Report操作,照理说这里不可能改_functionName的值,出现这种问题,只能怀疑为设备问题,设备在WriteFile返回后,在很短的时间内,还在操作附近的内存地址,所以才会出现被写坏,而稍微停一下,就不会出这个问题了。
该问题在某一个新的软件包上又出现了,还是用poly 40,切换为该设备的时候崩溃:
可以看出来是WriteDataToDevice已经出来了,把返回值和m_outputReportByteLength作比较时出了问题
byteview_bytertc!HIDDeviceImpl::SetFeatureInner+0x7c2 [C:\18573\bv-falcon\byteview-bytertc\src\hid\win\HIDDeviceWinImpl.cpp @ 282]:
282 00007ffc`71e4bfe2 448b4620 mov r8d,dword ptr [rsi+20h]
282 00007ffc`71e4bfe6 488b5628 mov rdx,qword ptr [rsi+28h]
282 00007ffc`71e4bfea 488bce mov rcx,rsi
282 00007ffc`71e4bfed e80e020000 call byteview_bytertc!HIDDeviceImpl::WriteDataToDevice (00007ffc`71e4c200)
282 00007ffc`71e4bff2 3b4620 cmp eax,dword ptr [rsi+20h]
282 00007ffc`71e4bff5 0f94c3 sete bl
出问题的汇编代码是cmp eax,dword ptr [rsi+20h],查看此时的寄存器:
rax=0000000000000002 rbx=0000015ea159f780 rcx=0000015e87740000
rdx=0000015e87740000 rsi=0000000000000002 rdi=0000000000000000
rip=00007ffc71e4bff2 rsp=0000003aaecfede0 rbp=0000003aaecfeee0
r8=0000015e87625580 r9=0000000000000001 r10=0000015e87620000
r11=0000003aaecfeb80 r12=0000000000000008 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
根据x64汇编约定,eax是函数返回值,所以dword ptr [rsi+20h]应该就是m_outputReportByteLength,而看寻址方式,rsi应该保存的是this指针,也就是HIDDeviceImpl的地址,+20h是找它的成员变量。
rsi = 0x2是有问题的,所以出现了异常。
根据win-x64约定,rsi属于callee save寄存器,也就是说,call完了之后,这个值不会变,但这里确实是变了,看这段汇编代码,call前后,这个rsi应该都是保存this指针的。
查看WriteDataToDevice的汇编代码,发现的确是有push和pop rsi的,那为什么会变呢?
联系到第一个crash的排查过程,就清楚了,应该还是栈上的内存被破坏了,所以pop rsi时,就不再是push时的那个值。
解决方案
目前只能用work around去解决,把ol放在堆上,避免和funtionName在内存布局上靠的太近,并且把ol保存到类的成员变量一个map中,持久化ol,防止ol过早的析构:
这样改完后,再也没有出现过崩溃。