DBGENG的一个BUG
近日在调试DBGENG有关的问题,其中一个问题是这样的,在建立调试会话的时候,DBGENG总是报告下面的错误:
Debug API version does not match system version
Debugger data list address is NULL
而且出了这样的错误后,DBGENG会始终认为调试目标是32位的,即使是明显在GetKDVersion接口中返回了0x8664作为MachineType。
进一步说,下面是返回给调试引擎的DBGKD_GET_VERSION64结构体:
0:014> dt pbVerOut
Local var @ 0x6b0ef74 Type _DBGKD_GET_VERSION64*
0x049d2928
+0x000 MajorVersion : 5
+0x002 MinorVersion : 0xa28
+0x004 ProtocolVersion : 0x3 ''
+0x005 KdSecondaryVersion : 0 ''
+0x006 Flags : 3
+0x008 MachineType : 0x8664
+0x00a MaxPacketType : 0xc ''
+0x00b MaxStateChange : 0x3 ''
+0x00c MaxManipulate : 0x2f '/'
+0x00d Simulation : 0x1 ''
+0x00e Unused : [1] 0
+0x010 KernBase : 0xffffffff`80000000
+0x018 PsLoadedModuleList : 0xffffffff`c07141c0
+0x020 DebuggerDataList : 0xffffffff`c0710f40
其中的MachineType明显告诉dbgeng目标是64位,但是它却总是显示ptr64 FALSE。
Connected to Windows XP
2600 x64 target at (Sat Feb 16 18:37:09.502 2019 (UTC + 8:00)), ptr64 FALSE
在调试器里编辑如下内部变量,让dbgeng输出内部状态:
ed dbgeng!g_OutputControl 0xffffffff
ed dbgeng!g_AllOutMask 0xffffffff
可以看到它内部的MachineType 是14c,代表8086,32位的。
Debug API version does not match system version
Debugger data list address is NULL
Target MajorVersion 00000300
Target MinorVersion 00000a28
Target ProtocolVersion 00000000
Target KdSecondaryVersion 00000000
Target MachineType 0000014c
Target PsLoadedModuleList 00000000
Target DebuggerDataList 00000000
这是怎么回事呢?
网上搜索一番,没有找到什么有用的信息,于是只好深入跟踪了。
在接口函数下断点,确保接口中的信息无误后,层层返回,很快追踪到了下面这个函数:
dbgeng!ExdiLiveKernelTargetInfo::GetTargetKdVersion
在刚刚返回到这个函数时,正确的信息还在。单步跟踪,发现它会调用ExportAndReleaseSafeArray 函数。这是因为接口中定义的返回数据是如下这样:
SAFEARRAY **pSafeArray
在应用的代码中,是通过如下函数(来自官方示例代码)把普通的结构体转换为SAFEARRAY的。
static HRESULT SafeArrayFromByteArray(const unsigned char *pByteArray,
size_t arraySize, SAFEARRAY **pSafeArray)
assert(pByteArray != NULL && pSafeArray != NULL);
ULONG copiedSize = static_cast<ULONG>(arraySize);
*pSafeArray = SafeArrayCreateVector(VT_UI1, 0, copiedSize);
memcpy((*pSafeArray)->pvData, pByteArray, copiedSize);
在调试器里看一下观察在SAFEARRAY中的数据:
Local var @ 0x6b0ef74 Type tagSAFEARRAY**
+0x002 fFeatures : 0x2080
+0x00c pvData : 0x06e75ff0 Void // 数据所在
+0x010 rgsabound : [1] tagSAFEARRAYBOUND
看一下反汇编,可以分析出ExportAndReleaseSafeArray 函数是把SAFEARRAY类型的数据,输出到一个结构体,并且把SAFEARRAY释放和销毁。
分析调用过程的汇编代码:
5a119c64 8b4de8 mov ecx,dword ptr [ebp-18h]
5a119c67 8d45e4 lea eax,[ebp-1Ch]
5a119c6a 8365e000 and dword ptr [ebp-20h],0
5a119c6e 8bd7 mov edx,edi
5a119c71 8d45e0 lea eax,[ebp-20h]
5a119c77 e83bf4ffff call dbgeng!ExdiLiveKernelTargetInfo::ExportAndReleaseSafeArray (5a1190b7)
可以推测ExportAndReleaseSafeArray函数的原型大致如下:
ExportAndReleaseSafeArray (SAFEARRY *pArray,BYTE * pBuffer, DWORD * pdwExportedDataSize, size_t szBuffer);
根据上下文,GetTargetKdVersion函数应该是这样调用ExportAndReleaseSafeArray 函数的:
_DBGKD_GET_VERSION64 * pKdVersion;
ExportAndReleaseSafeArray(pSafeArray, pKdVersion, &dwExportedDataSize, sizeof(_DBGKD_GET_VERSION64 ));
单步跟踪ExportAndReleaseSafeArray,发现它执行的很顺利,调用memcpy把pSafeArray中的数据复制到了第二个参数指定的缓冲区。
跟踪到这里,一切都还很正常,但是当ExportAndReleaseSafeArray函数返回后,接下来的代码就让人费解了。
下面这条比较指令是关键:
5a119c83 837de004 cmp
dword ptr [ebp-20h],4
如果不等的话,则跳转到下面这样一片设置默认值的代码,不管用户模块返回什么,都设置成hard code的信息了:
dbgeng!ExdiLiveKernelTargetInfo::GetTargetKdVersion+0x1a8:
5a119cb8 33c9 xor ecx,ecx
5a119cba b800030000 mov eax,300h
5a119cbf 668907 mov word ptr [edi],ax
5a119cc2 6a0c push 0Ch
5a119cc4 58 pop eax
5a119cc5 884f04 mov byte ptr [edi+4],cl
5a119cc8 66894706 mov word ptr [edi+6],ax
5a119ccc 668b83141d0000 mov ax,word ptr [ebx+1D14h]
5a119cd3 66894708 mov word ptr [edi+8],ax
5a119cd7 894f10 mov dword ptr [edi+10h],ecx
5a119cda 894f14 mov dword ptr [edi+14h],ecx
5a119cdd 894f18 mov dword ptr [edi+18h],ecx
5a119ce0 894f1c mov dword ptr [edi+1Ch],ecx
5a119ce3 894f20 mov dword ptr [edi+20h],ecx
5a119ce6 894f24 mov dword ptr [edi+24h],ecx
如此看来,之所以应用代码里返回的信息不被采纳,是因为在刚才那条比较指令那里误入歧途了。如果在调试器里强行把ebp-20处的局部变量修改为4,那么就一切正常了。
看来就是这条比较指令的问题了,仔细分析它的作用,它应该是判断ExportAndReleaseSafeArray输出的数据到底多长,因为pSafeArray 中的数据有0x28个字节(也就是DBGKD_GET_VERSION64结构体的大小),所以ebp-20处的值是0x28。
那么这里为什么判断是不是等于4呢?
想了一会,老雷断定这是一个坑人的bug。
推测一下,那附近的代码,正确的写法应该是这样的:
_DBGKD_GET_VERSION64 * pKdVersion;
ExportAndReleaseSafeArray(pSafeArray, pKdVersion, &dwExportedDataSize, sizeof(_DBGKD_GET_VERSION64 ));
if(pKdVersion != NULL
&& dwExportedDataSize == sizeof(*pKdVersion) )
{
//正常的流程,接受用户模块返回的数据
}
else
{
// 设置错误码,使用默认值
}
但是不知道是哪位同行,把比较语句中的关键的*忘记了,即:
&& dwExportedDataSize == sizeof(pKdVersion) )
那么就变成与4比较了。
这是C/C++程序员经常容易犯的一个错误,按说这样的错误不应该出现在操作系统的系统库里啊,但它就是出现了,真是让人无耐。
让应用程序workaround操作系统的 bug是很麻烦的。怎么办呢?
一边想着如何联系微软,一边想到尝试新的版本,刚才分析的出问题的版本是Windows 10的16299:
Image name: dbgeng.dll
Timestamp: ***** Invalid (E3B8302B)
CheckSum: 004A694A
ImageSize: 004D7000
File version: 10.0.16299.309
Product version: 10.0.16299.309
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 2.0 Dll
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName: Microsoft Corporation
ProductName: Microsoft® Windows® Operating System
InternalName: DbgEng.Dll
OriginalFilename: DbgEng.Dll
ProductVersion: 10.0.16299.309
FileVersion: 10.0.16299.309 (WinBuild.160101.0800)
FileDescription: Windows Symbolic Debugger Engine
LegalCopyright: © Microsoft Corporation. All rights reserved.
花了点时间,更新到了2018年10月的版本,奇迹出现了,问题不见了,断到调试器里分析,刚才那条邪恶的比较指令变得正确了。有图为证:
在Windows 10中,微软对dbgeng做了很多修改,有些是较大规模的重构,这些修改难免引入bug,这已经老雷第二次遇到了。