科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件ATL布幔下的秘密之窗口类的秘密

ATL布幔下的秘密之窗口类的秘密

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

很多人认为ATL只是用来编写COM组件的,其实你也可以使用ATL中的窗口类来创建基于窗口的应用程序 .

作者:李马编译 来源:VCKBASE 2007年10月19日

关键字:

  • 评论
  • 分享微博
  • 分享邮件
首先我想出了一个最明显的解决方法,我们可以很容易地实现。方法是创建一个全局的结构,这个结构存储HWND和相应的派生类。但是,这个方法有两个主要的问题。第一,这个结构会在窗口逐渐加入程序的过程中越变越大;第二,在结构变得很大之后,在这个结构中进行搜索肯定也会花费大笔时间。

  而ATL的最主要目的就是使程序尽可能地小和快。并且,上述技术对于这两个标准都达不到。这个方法不单单是慢,还会在程序中包含大量窗口的情况下占用大量内存。

  另一个可能的解决方案是使用WNDCLASS或WNDCLASSEX结构的cbWndExtra域。还有一个问题是,为什么不用cbClsExtra,而要用cbWndExtra呢?答案很简单,cbClsExtra为每个窗口类存储额外的字节,而cbWndExtra为每个窗口存储额外的字节。并且,你可能会从一个窗口类创建多个窗口,这样,如果你使用了cbClsExtra的话,那么你就不能通过cbClsExtra区别不同的回调函数了,因为对于这些相同窗口类产生的窗口来说这个值是一样的。然后,将相应的派生类地址存储到cbWndExtra中。

  这个和方法看起来比第一个要好,但是它仍然有两个问题。第一,如果用户希望使用cbWndExtra,那么他/她就可能会覆盖着一技术所使用的数据,这样客户就需要在使用cbWndExtra的时候十分注意了,以防丢失信息。那么好了,你可以在文档中写明在使用你的库时不要使用cbWndExtra,但是仍然会有一个问题:这个方法并不是很快,又一次违背了ATL的规则——ATL应该尽可能地小和快。

  ATL没有使用这两个方法中的任何一个,它使用的方法被称作Thunk。Thunk是一个小系列的代码,并且这一术语被用在不同的地方。你可能曾经听过两种Thunking:

  Universal Thunking

  Universal Thunking允许在16位代码中调用32位的函数,在Win 9x和Win NT/2000/XP下都可以使用,也被称作Generic Thunking。

  General Thunking

  General Thunking允许在32位代码中调用16位的函数,它只能用在Win 9x中,因为Win NT/2000/XP是纯32位操作系统,所以在32位代码中调用16位的函数不合乎逻辑。General Thunking也被称作Flat Thunking。

  ATL没有使用这两种方法,因为你不会在ATL中将16位和32位的代码混合。事实上,ATL插入了一小段代码来调用正确的窗口过程。

  在研究ATL的Thunking之前,让我们先从一些基础概念开始。请看下面的简单程序。

  程序72.

#include <iostream>
using namespace std;

struct S
{
 char ch;
 int i;
};

int main()
{
 cout << "Size of character = " << sizeof(char) << endl;
 cout << "Size of integer = " << sizeof(int) << endl;
 cout << "Size of structure = " << sizeof(S) << endl;
 return 0;
}

  程序的输出为:

Size of character = 1
Size of integer = 4
Size of structure = 8

  一个整型和一个字符的尺寸之和应该是5而不是8。那么让我们略微修改一下程序,再添加一个成员变量,看看会发生什么。

  程序73.

#include <iostream>
using namespace std;

struct S
{
 char ch1;
 char ch2;
 int i;
};

int main()
{
 cout << "Size of character = " << sizeof(char) << endl;
 cout << "Size of integer = " << sizeof(int) << endl;
 cout << "Size of structure = " << sizeof(S) << endl;
 return 0;
}

  程序的输出和前一个一样。那么这里发生了什么?再修改一下程序,看看布幔之下发生了什么吧。

  程序74.

#include <iostream>
using namespace std;

struct S
{
 char ch1;
 char ch2;
 int i;
}s;

int main()
{
 cout << "Address of ch1 = " << (int)&s.ch1 << endl;
 cout << "Address of ch2 = " << (int)&s.ch2 << endl;
 cout << "Address of int = " << (int)&s.i << endl;
 return 0;
}

  程序的输出为:

Address of ch1 = 4683576
Address of ch2 = 4683577
Address of int = 4683580

  这是由于结构和联合成员的字对齐的缘故。如果你注意观察的话,你就能推断出来这个结构外的每个变量都存储在能被4整除的地址上,这是为了提高处理器的性能。所以,这里的结构分配了4的整数倍的内存空间,也就是4683576,ch1和它有相同的地址。ch2成员存储在这个位置之后,而int i存储在4683580的位置上。这个位置不是4683578的原因是它不能被4整除。现在的问题是,4683578和4683579的位置上是什么呢?答案是如果变量是本地变量,那么这里是垃圾值;如果是static或全局变量,那么是0。让我们看看下面这个程序来更好地理解这一点。

  程序75.

#include <iostream>
using namespace std;

struct S
{
 char ch1;
 char ch2;
 int i;
};

int main()
{
 S s = { ''A'', ''B'', 10};

 void* pVoid = (void*)&s;
 char* pChar = (char*)pVoid;

 cout << (char)*(pChar + 0) << endl;
 cout << (char)*(pChar + 1) << endl;
 cout << (char)*(pChar + 2) << endl;
 cout << (char)*(pChar + 3) << endl;
 cout << (int)*(pChar + 4) << endl;
 return 0;
}
 
  程序的输出为:

A
B


10

  程序的输出清楚地表明,那些空间中是垃圾值,就像下表一样。


  现在,如果我们不想浪费那些空间的话应该怎么做呢?有两个选择:或者使用编译器开关/Zp,或者在声明结构之前使用#pragma语句。

  程序76.

#include <iostream>
using namespace std;

#pragma pack(push, 1)
struct S
{
 char ch;
 int i;
};
#pragma pack(pop)

int main()
{
 cout << "Size of structure = " << sizeof(S) << endl;
 return 0;
}

  程序的输出为:Size of structure = 5


  这就意味着现在已经没有字对齐了。事实上,ATL使用这一技术来制作thunk。ATL使用了一个结构,这个结构没有使用字对齐,并且这个结构中直接储存了微处理器的机器代码。

#pragma pack(push,1)
// 存储机器代码的结构
struct Thunk
{
 BYTE m_jmp; // jmp指令的操作码
 DWORD m_relproc; // 相对jmp
};
#pragma pack(pop)

  这种类型的结构保存了thunk代码,它可以在不工作的时候执行。让我们来看看下面这种简单的情况,我们将要使用thunk来执行我们想要执行的函数。

  程序77.

#include <iostream>
#include <windows.h>
using namespace std;

class C;

C* g_pC = NULL;

typedef void(*pFUN)();

#pragma pack(push,1)
// 存储机器代码的结构
struct Thunk
{
 BYTE m_jmp; // jmp指令的操作码
 DWORD m_relproc; // 相对jmp
};
#pragma pack(pop)

class C
{
 public:
  Thunk m_thunk;

 void Init(pFUN pFun, void* pThis)
 {
  // 跳转指令的操作码
  m_thunk.m_jmp = 0xe9;
  // 相应函数的地址
  m_thunk.m_relproc = (int)pFun - ((int)this+sizeof(Thunk));

  FlushInstructionCache(GetCurrentProcess(), &m_thunk, sizeof(m_thunk));
 }

 // 这是回调函数
 static void CallBackFun()
 {
  C* pC = g_pC;

  // 初始化thunk
  pC->Init(StaticFun, pC);

  // 获得thunk代码地址
  pFUN pFun = (pFUN)&(pC->m_thunk);

  // 开始执行thunk代码,调用StaticFun
  pFun();

  cout << "C::CallBackFun" << endl;
 }

 static void StaticFun()
 {
  cout << "C::StaticFun" << endl;
 }
};

int main()
{
 C objC;
 g_pC = &objC;
 C::CallBackFun();
 return 0;
}

  程序的输出为:

C::StaticFun
C::CallBackFun

  在这里,StaticFun是通过thunk调用的,而thunk是在Init成员函数中初始化的。程序的执行是类似这个样子

  ·CallBackFun
  ·Init(初始化thunk)
  ·获得thunk地址
  ·执行thunk
  ·Thunk代码调用StaticFun


  ATL也使用了相同的技术来调用正确的回调函数,但是它在调用函数之前还做了一件事情。现在ZWindow又有了一个虚函数ProcessWindowMessage,它在这个类中什么也不做。但是,ZWindow的每个派生类都需要重写它来处理自己的消息。整个的处理过程和我们将ZWindow的派生类地址存入一个指针并调用派生类的虚函数是相同的,但是现在WindowProc的名字是StartWndProc。在这里,ATL使用了这一技术来将HWND参数替换为了this指针。但是,HWND怎么样了,我们就这么失去它了吗?事实上,我们已经将HWND存入了ZWindow类的成员变量中了。

  要达到这一点,ATL使用了一个较前一个程序大一点的结构。

#pragma pack(push,1)
struct _WndProcThunk
{
 DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
 DWORD m_this;
 BYTE m_jmp; // jmp WndProc
 DWORD m_relproc; // 相对jmp
};
#pragma pack(pop)

  并且,在初始化的时刻,写入操作码“mov dword ptr [esp +4], pThis”。是类似这个样子:

void Init(WNDPROC proc, void* pThis)
{
 thunk.m_mov = 0x042444C7; //C7 44 24 04
 thunk.m_this = (DWORD)pThis;
 thunk.m_jmp = 0xe9;
 thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));

 FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk));
}

  并且,在初始化thunk代码之后,获得thunk的地址并向thunk代码设置新的回调函数。然后,thunk代码会调用WindowProc,但是现在第一个参数就不是HWND了,事实上它是this指针。所以我们可以将它安全的转换为ZWindow*,并调用ProcessWindowMessage函数。

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
 ZWindow* pThis = (ZWindow*)hWnd;

 if (uMsg == WM_NCDESTROY)
  PostQuitMessage(0);

 if (!pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam))
  return ::DefWindowProc(pThis->m_hWnd, uMsg, wParam, lParam);
 else
  return 0;
}

  现在,每个窗口正确的窗口过程就可以被调用了。整个的过程如下图所示:

 

  由于代码长度所限,程序的完整代码将随本文配套提供。我希望能在本系列中之后的文章中继续探究ATL的其它秘密。

查看本文来源

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章