欢迎转载 【作者:张佩】【镜像: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的测试信息:
上调试器后,在出问题的语句下断点,然后让程序飞。很快击中断点,看一下汇编代码:
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结构体中是旧的,别处都是新的。