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


    欢迎转载 作者:张佩】【镜像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;
                }
            } 
        }    
    
        delete adapterList;
        return bRetValue;
    }

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

    下面是Application Verifier的测试信息:

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

    多拷贝了8字节

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

    gMyAdapter = *adapter;
    293 0179a276 8b7ddc     mov     edi,dword ptr [ebp-24h]
    293 0179a279 83c720     add     edi,20h
    293 0179a27c b9a2000000 mov     ecx,0A0h
    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结构体中是旧的,别处都是新的。

  • 相关阅读:
    IO 模型知多少 | 代码篇
    IO 模型知多少 | 理论篇
    ASP.NET Core 反向代理部署知多少
    ASP.NET Core 借助 Helm 部署应用至K8S
    Orleans 知多少 | 4. 有状态的Grain
    Goodbye 2019,Welcome 2020 | 沉淀 2020
    Orleans 知多少 | 3. Hello Orleans
    集群环境下,你不得不注意的ASP.NET Core Data Protection 机制
    .NET Core 使用 K8S ConfigMap的正确姿势
    ASP.NET Core知多少(13):路由重写及重定向
  • 原文地址:https://www.cnblogs.com/keanuyaoo/p/3255910.html
Copyright © 2020-2023  润新知