CFileDialog退出时异常 2010-9-20 17:22

来源:互联网 发布:企业如何优化人才结构 编辑:程序博客网 时间:2024/06/06 19:23

环境:XP SP2 + VC6 SP6 + SDK2003
SDK的头文件和库在路径配置中位于VC6之前。

代码:
定义了操作系统版本_WIN32_WINNT=0x0500。当使用新的API时,就会去安装SDK2003,接着会定义该宏。

当使用CFileDialog dlg(TRUE)之后,函数退出析构CFileDialog时就会异常。

原因分析:
CFileDialog会使用到OPENFILENAME结构。
SDK关于该结构的定义比MFC42的多三个成员变量,该三个成员变量通过宏开关_WIN32_WINNT来控制。由于SDK的include路径在VC6之前,所以会找到SDK的头文件。在析构时,析构到Cstring,导致异常。

解决办法:
方法1:调整SDK头文件和VC6头文件的位置。该方法并不值得推荐,因为SDK的头文件在后面会引发其他一些编译链接方面的问题。
方法2:去除_WIN32_WINNT的宏定义。该方法也不值得推荐,因为安装SDK是为了使用一些新的API。去除这个宏定义,也会引起编译和链接的问题。
方法3:使用new和delete创建和销毁CFileDialog。该方法值得推荐。

遗留问题:
1、 为什么在栈上产生CFIleDialog对象,其构造函数与析构函数不一致。构造使用的是SDK的还是MFC的,析构使用的又是哪个的呢?析构时是因为何种原因导致异常?
2、 使用new和delete之后,new和delete都使用的SDK版本的还是MFC版本的?

1、为什么在栈上产生CFileDialog对象,其构造函数与析构函数不一致。构造使用的是SDK的还是MFC的,析构使用的又是哪个的呢?析构时是因为何种原因导致异常?
2、使用new和delete之后,new和delete都使用的SDK版本的还是MFC版本的?

今天在电脑上写了个测试程序,分析了一下,答案如下:
1、栈上产生的CFileDialog对象析构时使用的是编译器合成的析构函数。构造使用的是MFC的函数。析构异常是因为访问了非法内存。下面会具体描述一下原因。
2、new和delete都是使用MFC版本的CFileDialog。之所以会有这个问题,是因为对SDK不熟悉。我只在SDK目录下找到64位的mfc42d.dll。

CFileDialog的声明简述如下:
class CFileDialog : public CCommonDialog
{
DECLARE_DYNAMIC(CFileDialog)
public:
// Attributes
OPENFILENAME m_ofn; // open file parameter block
//...省略函数声明
protected:
BOOL m_bOpenFileDialog; // TRUE for file open, FALSE for file save
CString m_strFilter; // filter string
TCHAR m_szFileTitle[64];
//...省略以下成员
}

这个头文件本身没有问题,与SDK头文件不一致的是OPENFILENAME结构,其描述如下:
typedef struct tagOFN {
DWORD lStructSize;
//...省略其他声明
#if (_WIN32_WINNT >= 0x0500)
void * pvReserved;
DWORD dwReserved;
DWORD FlagsEx;
#endif // (_WIN32_WINNT >= 0x0500)
} OPENFILENAME, *LPOPENFILENAME;

在如下程序代码中设置断点:
void CTestDlg::OnBtnTest()
{
CFileDialog dlg(TRUE); //断点1
return; //断点2
}

在断点1处跟进,通过查看堆栈的地址和模块加载的地址可以得知,CFileDialog使用的是MFC42D.dll中的代码,其地址空间在MFC42D.dll。运行到断点2,查看dlg的m_ofn大小为76字节,m_ofn的lStructSize标识了其大小。但是如果在断点1和断点2之间假如int nSize = sizeof(dlg.m_ofn)会发现,nSize为88字节。这是因为示例程序是根据SDK的头文件来计算m_ofn大小的。
在断点2处跟进汇编码,该析构函数是编译器合成的,其地址空间在示例程序。在析构CString型的m_strFilter会有如下代码:
004027AC add ecx,0BCh
004027B2 call CString::~CString (0040284a)

这是编译器直接根据SDK的头文件中结构定义,认为dlg偏移0xBC字节就是m_strFilter。如果没有定义_WIN32_WINNT 0x0500,查看此段汇编,你会发现这个偏移是0xB0。示例代码中正好比正常的大12。这是因为定义了_WIN32_WINNT 0x0500导致m_ofn比实际大小大12字节。这时查看寄存器ecx的值。如果你有记录之前dlg中各对象的地址,你会发现ecx的值正好已经越过了m_strFilter,因为m_strFilter只有4个字节。这样ecx就来到了TCHAR m_szFileTitle[64]中间的一段内存中。并尝试将这段内存当成CString去析构,这时就导致了异常。

那为什么使用new/delete不会出错呢?因为使用delete会使得程序去查找CFileDialog虚表中的析构函数地址,该地址位于MFC42空间内,其已有代码对m_strFilter的偏移计算是0xB0,和构造函数一致,故而不会出错。但是如果你有一个类是派生自CFileDialog,那么你不能在析构函数中显示delete父类,就一样会出错。

结论小述如下:当在栈上产生CFileDialog对象时,其构造函数会链接到MFC42D的代码,而其析构函数由编译器合成。如果这时使用了SDK的头文件,且定义了_WIN32_WINNT就会导致编译器合成的析构函数计算m_strFilter的偏移地址出错。导致析构异常。当使用delete的时候,程序会根据虚表地址绑定MFC42的代码,所以不会出错。但是如果使用的是派生自CFileDialog的自定义类时,还是会出错。因为编译器给你的类析构函数加上CFileDialog代码时又会计算错偏移地址。

这篇还是留一个问题:
因为CFileDialog的声明中没有析构函数,为了验证是否是因为该原因导致编译器为栈上的CFileDialog就地产生一个析构函数,我写了一个测试用的dll,导出一个CMyClass类,使用lib文件静态链接,发现在栈上产生的CMyClass,编译器为其合成的析构函数中却能够调用到测试用的dll的地址空间内。为什么MFC42的却不能呢?我的dll是通过静态链接的,MFC42是怎么加载的呢?会不会是加载方式的不同造成的差异?


曾经的这一天... 天下风云出我辈,一入江湖岁月催。
转载自http://shokey520.ycool.com/post.3919646.html