虫趣:不同模块对指定变量类型的定义不同

欢迎转载 作者:张佩】【原文http://www.yiiyee.cn/Blog/dll-1/

引子

周末写了一个简单的程序(后文以Test.exe代指),通过Iphlpapi.dll提供的API函数GetAdaptersInfo,读取系统中的网卡信息,通过网卡名找到我想要的虚拟网卡后,将网卡信息结构体(IP_ADAPTER_INFO)保存到一个全局变量中。

逻辑很简单,写完之后测试也没有发现问题。后来开启Application Verifier并运用到Test.exe,竟然每次运行都发生崩溃。仔细看去,问题出在保存结构体到全局变量的语句上。看到这个错误后,本想凭借猜测把问题解决掉,试了三五分钟后,竟不能够。最后还只能上调试器,错误原来是不同模块对同一个变量类型(time_t)有不同的定义(默认定义的长度为8字节;而Iphlpapi模块出于和Win2K系统兼容的缘故,使用的长度为4字节)。

本文就讲一讲这个奇怪的Bug。下面是我整理后的简要代码逻辑:

#pragma comment(lib, "Iphlpapi.lib")
#include <Iphlpapi.h>

IP_ADAPTER_INFO gMyAdapter; // 全局变量

bool getOneMacAddr ()
{
    bool bRetValue = false;
    ULONG len = 0;
    IP_ADAPTER_INFO* adapterList = NULL;

    if(ERROR_BUFFER_OVERFLOW ==GetAdaptersInfo (adapterList, &len))
    {
        adapterList = (IP_ADAPTER_INFO*) malloc (len);
        if (adapterList == nullptr)
            {return false;}
    }
    else {return false;}

    if (GetAdaptersInfo (adapterList, &len) == NO_ERROR)
    {
        for (PIP_ADAPTER_INFO adapter = adapterList; 
             adapter != nullptr; 
             adapter = adapter->Next)
        {
            if (true)
            {
                bRetValue = true;
                gMyAdapter = *adapter; // hang!
                break;
            }
        } 
    }    

    free adapterList;
    return bRetValue;
}

这段简单的代码逻辑是:获取举所有网卡信息并保存到adaperList中;枚举列表,并根据网卡名称找到对应网卡后,保存adapter信息到全局变量中。

下面是Application Verifier的测试信息:

  1. 开启Application Verifier的Basics测试项后,每运行即hang。
  2. 关闭Application Verifier的Basics\Memory测试项后,不再hang。

多拷贝了8字节

上调试器后,在出问题的语句下断点,然后让程序飞。很快击中断点,看一下汇编代码:

gMyAdapter = *adapter;
293 0179a276 8b7ddc     mov     edi,dword ptr [ebp-24h]
293 0179a279 83c720     add     edi,20h
293 0179a27c b9a2000000 mov     ecx,0A2h
293 0179a281 8b75e4     mov     esi,dword ptr [ebp-1Ch]
293 0179a284 f3a5       rep movs dword ptr es:[edi],dword ptr [esi]

上面的汇编指令以循环方式进行内存拷贝:esi寄存器中保存的是源地址(adapter),edi中保持的是目的地址(gMyAdapter地址);为4字节为单位,把源地址中内容循环0xA2次(ecx中值),拷贝到目标内存。

我再这里确认了一下拷贝的的长度:0xA2 × 4 = 0x288

又用调试器取了一次长度:

0:010> ?? sizeof(_IP_ADAPTER_INFO)
unsigned int64 0x288

从这里看来,可见拷贝的长度是对的,说明出问题的语句(gMyAdapter = *adapter)本身并没有问题。这时候就需要考虑拷贝的双方了,既然是内存错误,那么不外乎两点:要么是目的内存溢出;要么是源内存无效。gMyAdapter发生内存溢出是不可能的,因为这是个全局变量,拷贝的是和它自身结构体长度相等的内容。所以问题可能来自源内存(adapterList)。

0:000> r esi
esi=066bc880

0:014:x86> dt _IP_ADAPTER_INFO 066bc880
YTingBox!_IP_ADAPTER_INFO
   +0x000 Next             : 0x066bcb00 _IP_ADAPTER_INFO
   //…省略   
   +0x280 LeaseExpires     : 0n73122172288

0:014:x86> dt _IP_ADAPTER_INFO 0x066bcb00
YTingBox!_IP_ADAPTER_INFO
   +0x000 Next             : 0x066bcd80 _IP_ADAPTER_INFO
   //…省略   
   +0x280 LeaseExpires     : 0n77309411328

0:014:x86> dt _IP_ADAPTER_INFO 0x066bcd80
YTingBox!_IP_ADAPTER_INFO
   +0x000 Next             : (null) 
    //…省略   
   +0x278 LeaseObtained    : 0n0
   +0x280 LeaseExpires     : ??

列表共包含三个Adapter结构体,注意到最后一个结构体的最后一个变量,显示异常。看一下它的内存:

0:000> dd 0x066bcd80+0x280 L4
066bd000  ???????? ???????? ???????? ????????

内存无效。往回看一下:

0:000> dd 0x066bcd80+0x270 L4
066bcff0  00000000 00000000 00000000 00000000

则是有效的。暂时得到的结论是,最后一个结构体的最后一个成员变量,内容无效。因为访问这个无效的内容,导致了出错。这时再关注到三个结构体之间的offset:

0x066bcb00 – 0x066bc880 = 0x280
0x066bcd80 – 0x066bcb00 = 0x280

上面计算到的结构体大小是0x288,这里怎么是0x280?正好小了8个字节,应该就是出问题的原因了。

发现问题

 这时候,我打开MSDN仔细阅读GetAdaptersInfo和IP_ADAPTER_INFO的说明,在注解中找到了下面的内容。

When using Visual Studio 2005 and later, the time_t datatype defaults to an 8-byte datatype, not the 4-byte datatype used for the LeaseObtained and LeaseExpires members on a 32-bit platform. To properly use the IP_ADAPTER_INFO structure on a 32-bit platform, define _USE_32BIT_TIME_T (use -D _USE_32BIT_TIME_T as an option, for example) when compiling the application to force the time_t datatype to a 4-byte datatype.

原来Iphlpapi模块中的GetAdaptersInfo函数是一个比较老的API,它在实现的时候使用4字节的time_t定义。这个DLL模块现在仍然被使用,但是为了兼容旧的系统,它没有改变对time_t变量的定义,依旧使用4字节定义。但VS 2005以后的编译器讲time_t定义成了8字节的变量类型。我使用VS2012进行编译,所以IP_ADAPTER_INFO结构体中的两个time_t变量长度一共是16个字节,比旧的定义多出了8个字节。

注解中同时说明,为了避免这个变量定义不统一的问题,用户可以通过定义宏_USE_32BIT_TIME_T使编译器强制使用旧的4字节定义。我在试验了这个方法后,程序果然通过了Application Verifier的测试。从调试器中得到的time_t长度变成4字节。MSDN同时建议用户在XP及以后的系统中,使用函数GetAdaptersAddresses来替代GetAdaptersInfo,也能避免此问题。

// 1,正常情况下,time_t是8字节
0:000> dt time_t
Test!time_t
Int8B

0:000> ?? sizeof(time_t)
unsigned int 8

// 2,定义_USE_32BIT_TIME_T后,变成Int4B类型,长度4字节
0:000> dt time_t
Test!time_t
Int4B

后记

这是一个非常隐蔽的BUG,如果不开启Application Verifier很难一下子把它抓到。

有一个说法叫DLL Hell。DLL的好处在于它可以被动态链接,不必静态包含在链接它的可执行模块中,而是物理上分开的两个独立的模块文件。所以,DLL模块可以被独立地维护、更新。但是厂商在更新DLL模块的时候,要千万注意一件事情,就是不能改变DLL的外部接口或类型定义。如果违反了这一点的话,就会导致DLL Hell。因为链接它的可执行程序并不知道它的接口变更和类型变化,而依旧使用旧的定义,问题就出大了,而且很难找到根源。所以软件厂商在DLL更新的时候,非常慎重。一定要努力维护它的外部接口不变,和类型定义不变。本文用到的dll模块Iphlpapi.dll做到了这一点,它没有“更新”time_t类型的定义,虽然对于其他的新模块,此类型已经改变了。

那种“更新”是可怕的,Iphlpapi.dll并没有犯此错误。但它的这份坚持,却也正是本文Bug的根源所在。程序员要足够的“渊博”,才能知道,原来time_t有两个不同的定义,IP_ADAPTER_INFO结构体中是旧的,别处都是新的。

4,675 total views, 3 views today

《虫趣:不同模块对指定变量类型的定义不同》有6个想法

  1. 虽然未涉及到对象,delete adapterList; 不出错,
    为了配对使用,delete adapterList; 还是改为free(adapterList);吧

发表评论

电子邮件地址不会被公开。