虫趣:不同模块对同一变量类型的不同定义
2013-08-13 19:43
609 查看
欢迎转载 【作者:张佩】【镜像:http://www.yiiyee.cn/Blog/dll-1/】
逻辑很简单,写完之后测试也没有发现问题。后来开启Application Verifier并运用到Test.exe,竟然每次运行都发生崩溃。仔细看去,问题出在保存结构体到全局变量的语句上。看到这个错误后,本想凭借猜测把问题解决掉,试了三五分钟后,竟不能够。最后还只能上调试器,错误原来是不同模块对同一个变量类型(time_t)有不同的定义(默认定义的长度为8字节;而Iphlpapi模块出于和Win2K系统兼容的缘故,使用的长度为4字节)。
本文就讲一讲这个奇怪的Bug。下面是我整理后的简要代码逻辑:
这段简单的代码逻辑是:获取举所有网卡信息并保存到adaperList中;枚举列表,并根据网卡名称找到对应网卡后,保存adapter信息到全局变量中。
下面是Application Verifier的测试信息:
开启Application Verifier的Basics测试项后,每运行即hang。
关闭Application Verifier的Basics\Memory测试项后,不再hang。
上面的汇编指令以循环方式进行内存拷贝:esi寄存器中保存的是源地址(adapter),edi中保持的是目的地址(gMyAdapter地址);为4字节为单位,把源地址中内容循环0xA2次(ecx中值),拷贝到目标内存。
我再这里确认了一下拷贝的的长度:0xA2 × 4 = 0x288
又用调试器取了一次长度:
从这里看来,可见拷贝的长度是对的,说明出问题的语句(gMyAdapter = *adapter)本身并没有问题。这时候就需要考虑拷贝的双方了,既然是内存错误,那么不外乎两点:要么是目的内存溢出;要么是源内存无效。gMyAdapter发生内存溢出是不可能的,因为这是个全局变量,拷贝的是和它自身结构体长度相等的内容。所以问题可能来自源内存(adapterList)。
列表共包含三个Adapter结构体,注意到最后一个结构体的最后一个变量,显示异常。看一下它的内存:
内存无效。往回看一下:
则是有效的。暂时得到的结论是,最后一个结构体的最后一个成员变量,内容无效。因为访问这个无效的内容,导致了出错。这时再关注到三个结构体之间的offset:
0x066bcb00 – 0x066bc880 = 0x280
0x066bcd80 - 0x066bcb00 = 0x280
上面计算到的结构体大小是0x288,这里怎么是0x280?正好小了8个字节,应该就是出问题的原因了。
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,也能避免此问题。
有一个说法叫DLL Hell。DLL的好处在于它可以被动态链接,不必静态包含在链接它的可执行模块中,而是物理上分开的两个独立的模块文件。所以,DLL模块可以被独立地维护、更新。但是厂商在更新DLL模块的时候,要千万注意一件事情,就是不能改变DLL的外部接口或类型定义。如果违反了这一点的话,就会导致DLL Hell。因为链接它的可执行程序并不知道它的接口变更和类型变化,而依旧使用旧的定义,问题就出大了,而且很难找到根源。所以软件厂商在DLL更新的时候,非常慎重。一定要努力维护它的外部接口不变,和类型定义不变。本文用到的dll模块Iphlpapi.dll做到了这一点,它没有“更新”time_t类型的定义,虽然对于其他的新模块,此类型已经改变了。
那种“更新”是可怕的,Iphlpapi.dll并没有犯此错误。但它的这份坚持,却也正是本文Bug的根源所在。程序员要足够的“渊博”,才能知道,原来time_t有两个不同的定义,IP_ADAPTER_INFO结构体中是旧的,别处都是新的。
引子
周末写了一个简单的程序(后文以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的测试信息:
开启Application Verifier的Basics测试项后,每运行即hang。
关闭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,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结构体中是旧的,别处都是新的。
相关文章推荐
- 虫趣:不同模块对同一变量类型的不同定义
- Python基础:数据类型、变量定义、输入/输出、逻辑、函数/模块(导入)/类、异常处理
- 多次包含同一个头文件,实现只写一句宏定义就可以定义出两个不同类型的变量
- 一个使用泛型堆栈模块创建的两个容纳不同类型数据的实例
- 类是一个数据类型;对象是类的变量,定义一个类,是一个抽象概念
- Python 3 实现定义跨模块的全局变量和使用
- 【PGM】Representation--Knowledge Engineering,不同的模型表示,变量的类型,structure & parameters
- linux——Shell 脚本基础篇(变量类型,变量操作,定义,运算与逻辑关系)
- 数据类型——变量常量和声明与定义
- C#不同类型的成员变量(字段)的默认值
- Maven <Profiles>定义不同环境的参数变量
- (转)定义接口类型的引用变量有什么好处?
- python:global,可变数据类型与不可变数据类型,在声明全局变量中的不同
- C不同变量类型存储大小引发的BUG
- perl 子程序传递参数不同类型变量 设置
- typedef定义的struct类型与struct定义的结构体变量在使用上的区别
- verilog模块中各个变量的类型怎么确定
- Java定义接口变量为接收类型有什么好处(面向接口编程)
- Shell变量:Shell变量的定义、删除变量、只读变量、变量类型
- C语言中一些类型的变量的定义和使用