<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

文章分类

导航

订阅

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 Flags              0000000c
Target MachineType        0000014c
Target MaxPacketType      c
Target MaxStateChange     3
Target MaxManipulate      2f
Target KernBase           00000000
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);
    if (*pSafeArray == NULL)
    {
        return E_FAIL;
    }

    memcpy((*pSafeArray)->pvData, pByteArray, copiedSize);

    return S_OK;
}
在调试器里看一下观察在SAFEARRAY中的数据:
0:014> dt pSafeArray -r
Local var @ 0x6b0ef74 Type tagSAFEARRAY**
0x06b0f004 
 -> 0x06e75fd8 
   +0x000 cDims            : 1
   +0x002 fFeatures        : 0x2080
   +0x004 cbElements       : 1
   +0x008 cLocks           : 0
   +0x00c pvData           : 0x06e75ff0 Void // 数据所在
   +0x010 rgsabound        : [1] tagSAFEARRAYBOUND
      +0x000 cElements        : 0x28
      +0x004 lLbound          : 0n0

看一下反汇编,可以分析出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
5a119c70 50              push    eax
5a119c71 8d45e0          lea     eax,[ebp-20h]
5a119c74 50              push    eax
5a119c75 6a28            push    28h
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,这已经老雷第二次遇到了。

posted on 2019年2月18日 21:40 由 Raymond

Powered by Community Server Powered by CnForums.Net