Dll地狱的一次实际碰撞

今日碰到一个 bug ,调试很久才发现是由于链接库与头文件不匹配造成的。实际问题当然很复杂,做个小例子来说明当时的情况吧。

--------------------------------------------------- 问题发现与再现 ------------------------------------------------------------

先用 VS2005 创建两个项目,第一个为 MFC Dll 项目,名称 my_lib 在项目属性 , “Configuration Properties”, “General” 中将 ”Configuration Type” 设为 ”Static Library(.lib)”

然后添加一个类 (CBaseExport) ( BaseExport.h)

class CBaseExport

{

public :

     CBaseExport(void );

     ~CBaseExport(void );

 

     int get_x();

 

private :

     int _x;

};

 

其实现:( BaseExport.cpp

CBaseExport::CBaseExport(void ) : _x(0)

{

}

 

CBaseExport::~CBaseExport(void )

{

}

 

int CBaseExport::get_x()

{

     return _x;

}

 

然后创建第二个项目,并在项目属性, Configuration Properties , Linker , input , Additional Dependencies 加上前一个项目生成的Lib 文件 my_lib.lib

然后添加类:

class CDerive : public CBaseExport

{

public :

     CDerive(void );

     ~CDerive(void );

 

private :

     int _x2;

};

CDerive::CDerive(void ) : _x2(10)

{

}

 

CDerive::~CDerive(void )

{

}

 

 

当我们在项目2 中使用类CDerive ,例如:

CDerive *pd = new CDerive;

int x = pd->get_x();

 

毫无疑问,这时x 的值是0

 

但假设某个不幸的时刻发生了,就像我今天碰到的这样:

项目 1 中的 CBaseExport 类发生了变化,比如添加了一个成员变量:

class CBaseExport

{

public :

     CBaseExport(void );

     ~CBaseExport(void );

 

     int get_x();

 

private :

     int _new_x; // 我是新来的

     int _x;

};

 

然后在项目 2 中,我们获取了最新的 lib 文件,但是很不幸,头文件没更新,还是以前的(这种情况应该很有可能发生吧),这时再 使用类CDerive ,例如:

CDerive *pd = new CDerive;

int x = pd->get_x();

 

这时 x 的值会是什么呢?还会是 0 吗?

No, 它变成了 10

为什么会这样?咱们一步一步调试看看。

 

--------------------------------------------------- 问题原因 ------------------------------------------------------------

首先第一步,调用 new 分配内存。

     CDerive *pd = new CDerive;

004024C0  push        8   

004024C2  call        operator new (401398h)

004024C7  add         esp,4

004024CA  mov         dword ptr [ebp-1Ch],eax

004024CD  cmp         dword ptr [ebp-1Ch],0

004024D1  je          CUseDllView::CUseDllView+60h (4024E0h)

004024D3  mov         ecx,dword ptr [ebp-1Ch]

004024D6  call        CDerive::CDerive (4023E0h)

……

可以看到编译器认为 CDerive 的大小是8 ,因为在编译器看来,CBaseExport 只有一个整型变量,大小是4 ,而CDerive 增加了一个整型变量,大小就是8 了。而实际上CBaseExport 的大小是8 ,这样在调用CBaseExport 的构造函数时,肯定会发生内存越界。

 

然后进入CDerive 的构造函数:

CDerive::CDerive(void) : _x2(10)

{

004023E0  push        ebp 

004023E1  mov         ebp,esp

004023E3  push        ecx 

004023E4  mov         dword ptr [ebp-4],ecx

004023E7  mov         ecx,dword ptr [this]

004023EA  call        CBaseExport::CBaseExport (4023B0h)

004023EF  mov         eax,dword ptr [this]

004023F2  mov         dword ptr [eax+4],0Ah

}

004023F9  mov         eax,dword ptr [this]

004023FC  mov         esp,ebp

004023FE  pop         ebp 

004023FF  ret  

从这里可以看出CDerive 的构造函数先调用基类CBaseExport 的构造函数,然后通过初始化列表初始化所有成员变量(事实上没在初始化列表中的类变量也会调用默认构造函数初始化),最后在执行函数体内的函数(当然这里是空函数体)。

 

然后CBaseExport 的构造函数:

CBaseExport::CBaseExport(void) : _x(0), _new_x(0)

{

004023B0  push        ebp 

004023B1  mov         ebp,esp

004023B3   push        ecx 

004023B4  mov         dword ptr [ebp-4],ecx

004023B7  mov         eax,dword ptr [this]

004023BA  mov         dword ptr [eax],3

004023C0  mov         ecx,dword ptr [this]

004023C3  mov         dword ptr [ecx+4],0

}

004023CA  mov          eax,dword ptr [this]

004023CD  mov         esp,ebp

004023CF  pop         ebp 

004023D0  ret    

 

其中mov eax,dword ptr [this] CBaseExport 的地址存入寄存器eax 中,然后mov dword ptr [eax],3 其实就是给变量_new_x 赋值。这里要注意变量的赋值顺序并不是按照变量初始化列表的顺序,而是按照他在类中声明的顺序。然后mov ecx,dword ptr [this]mov dword ptr [ecx+4],0 时给变量_x 赋值。

在这时,我们记录一下ecx 的值是0x003AA750 (每次运行时会不一样),也就是说_x 的内存地址是0x003AA754

 

CBaseExport 的构造函数完成后,回到CDerive 的构造函数中:

CDerive::CDerive(void) : _x2(10)

{ ……

004023EA  call        CBaseExport::CBaseExport (4023B0h)

004023EF  mov          eax,dword ptr [this]

004023F2  mov         dword ptr [eax+4],0Ah

} ……

这里再给_x2 赋值。注意eax 的值同样是0x003AA750, 也就是编译器认为_x2 的内存地址是0x003AA754 ,和基类CBaseExport 中的_x 完全一样,结果_x 的值就被覆盖了。

 

 

--------------------------------------------------- 引申思考 ------------------------------------------------------------

以前经常听到Dll 地狱这个名词,但对其没有太大感觉,今天通过这个例子一下理解了Dll 地狱的说法。我想,之所以这种事情发生,还是由于dll 在头文件中暴露了太多的细节,而这些没必要的细节暴露就在不经意间引发了很多问题,例如当头文件和libdll 不匹配时编译器根本没能力发现,因为毕竟没改动任何接口,也就没法给出警告,这样只能在运行时靠运气和经验来解决问题了。

 

我想,解决方法就是不要向用户暴露实现细节,例如使用PROXY 代理设计模式:

给用户提供封装的代理类,其中只有公共接口和一个指向实际对象的指针:

class CBaseExportImpl;

class CBaseExport

{

public :

     CBaseExport(void );

     ~CBaseExport(void );

 

     int get_x();

 

private :

     CBaseExportImpl *_impl;

};

 

在公共接口中,直接将调用转给实际的对象处理。

int CBaseExport::get_x()

{

     return _impl->get_x();

}

 

这样无论底层的实现如何变化,对用户都没有任何影响了(当然接口变化除外,这时编译器会给出错误警告)。

 

另一种方式是使用COM 。由于COM 具有自我描述,只向外提供接口的特点,所以像Dll 地狱的问题很容易避免。

 

终归到底,就是小心设计,不要随便对外提供实现细节,否则,会死得很惨!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值