ATL Thunk机制深入分析
如果你有SDK的编程经验,就一定应该知道在创建窗口时需要指定窗口类,窗口类中的一种重要的参数就是窗口过程。任何窗口接收到的消息,都是由该窗口过程来处理。
在面向对象编程中,如果还需要开发人员来使用原始的窗口过程这种面向过程的开发方式,面向对象就显得不那么纯粹了。所以,在界面编程的框架中,框架往往会隐藏窗口过程,开发人员看到的都是一个个的类。
如果要处理某一个消息,则需要在窗口对应的类中加入响应的message map即可。
那么,框架是如何将窗口过程跟窗口对应的类关联起来呢? ATL中用的是一个叫thunk的机制。由于我们收回来的dump有大量的窗口过程出问题的case,最后发现跟thunk有一定的关系,所以我对ATL的thunk做了 一番研究。
Thunk的基本原理是分配一段内存,然后将窗口过程设置为这段内存。这段内存的作用是将窗口过程的第一个参数(窗口句柄)替换成类的This指针,并jump到类的WinProc函数中。这样就完成了窗口过程到类的成员函数的一个转换。
这里面有几个点需要重点研究一下:
- 什么时候分配thunk这段内存,又在什么时候将窗口过程设置为thunk的这段内存。
- 内存是怎么分配的,是一段堆上的内存吗?
- 这段内存到底是什么东西?
我们先来看看第一个问题:
什么时候分配thunk这段内存,又在什么时候将窗口过程设置为thunk的这段内存。
ATL在创建窗口时,使用的窗口类是通过一段宏来定义的:DECLARE_WND_CLASS(_T("My Window Class"))
这段宏的定义如下:

#define DECLARE_WND_CLASS(WndClassName) \
static ATL::CWndClassInfo& GetWndClassInfo() \
{ \
static ATL::CWndClassInfo wc = \
{ \
{ sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, \
0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, \
NULL, NULL, IDC_ARROW, TRUE, 0, _T("") \
}; \
return wc; \
}

可以看到,这个宏实际上是在定义一个静态函数,他的功能是返回一个ATL::CWndClassInfo对象,这个对象实际上就是ATL对窗口类的封装,其中窗口过程被指定为StartWindowProc。
当用户调用CWindowImpl::Create来创建窗口时,会调用到CWindowImpl的父类CWindowImplBaseT的create函数:
这个函数如下:

template <class TBase, class TWinTraits>
HWND CWindowImplBaseT< TBase, TWinTraits >::Create(HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName,
DWORD dwStyle, DWORD dwExStyle, _U_MENUorID MenuOrID, ATOM atom, LPVOID lpCreateParam)
{
。。。。 // Allocate the thunk structure here, where we can fail gracefully.
result = m_thunk.Init(NULL,NULL);
.......
HWND hWnd = ::CreateWindowEx(dwExStyle, MAKEINTATOM(atom), szWindowName,
dwStyle, rect.m_lpRect->left, rect.m_lpRect->top, rect.m_lpRect->right - rect.m_lpRect->left,
rect.m_lpRect->bottom - rect.m_lpRect->top, hWndParent, MenuOrID.m_hMenu,
_AtlBaseModule.GetModuleInstance(), lpCreateParam);
.............
return hWnd;
}

可以看到,我们首先对thunk用NULL进行了初始化,正如注释所说的,这这里初始化是因为如果分配内存失败了,可以更好的进行错误处理。实际上thunk也完全可以在后面处理窗口的第一个消息时进行初始化。
接着调用windows API CreateWindowEx来创建窗口,前面我们知道,这个窗口的使用的窗口类的窗口过程是StartWindowProc,我们接着看它的处理代码。

template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();
pThis->m_hWnd = hWnd; // Initialize the thunk. This is allocated in CWindowImplBaseT::Create,
// so failure is unexpected here.
pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
WNDPROC pProc = pThis->m_thunk.GetWNDPROC();
WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
return pProc(hWnd, uMsg, wParam, lParam);
}

这是类的一个静态函数,所以可以作为窗口过程直接使用。我们可以看到,他的参数正是窗口过程的四个参数。
函数首先拿到This指针,将传递进来的窗口句柄赋给this的成员变量m_hWnd。然后m_thunk.Init来重新初始化thunk,这个时候传递进去的不再是两个NULL,而是类的一个成员函数和This指针。
这个初始化具体做什么我们后面再具体分析。
初始化完成之后,我们就可以拿到这个thunk的地址(m_thunk.GetWNDPROC就是获取thunk的地址),然后调用SetWindowLongPtr将窗口过程设置成thunk的地址。下次窗口有消息来时就直接跑到thunk里面去了
最后直接调用thunk的地址,是为了将StartWindowProc正在处理的这个消息也传递给thunk处理,以免丢失了窗口的第一个消息。
这段内存到底是什么东西?
我们下面来重点分析m_thunk.Init是完成什么功能。
CDynamicStdCallThunk.Init的代码如下:

BOOL Init(DWORD_PTR proc, void *pThis)
{
if (pThunk == NULL)
{
pThunk = new _stdcallthunk;
if (pThunk == NULL)
{
return FALSE;
}
}
return pThunk->Init(proc, pThis);
}

代码很简单,分配一段结构(_stdcallthunk),然后继续调用这个结构的init函数。(提前说一下的是,这里override了new这个operator,真正做的事情不是简单的从堆上分配内存,后面会详细介绍)
结构

struct _stdcallthunk
{
DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
DWORD m_this; //
BYTE m_jmp; // jmp WndProc
DWORD m_relproc; // relative jmp
BOOL Init(DWORD_PTR proc, void* pThis)
{
m_mov = 0x042444C7; //C7 44 24 0C
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));
return TRUE;
}
。。。。
}

Thunk有四个成员,第一个成员是m_mov,被赋值为0x042444C7,第二个成员是m_this,被赋值为窗口对应类的地址。
这两个DWORD实际上组成了一条汇编语句:
mov dword ptr [esp+0x4], pThis
通过前面我们知道,窗口过程已经被设置成这段内存的起始地址,也就是说窗口过程的第一行代码就是这行代码。
Esp是指向栈顶的指针,esp+0x4则是窗口过程的第一个参数hWnd,这段代码的意思就是说将this指针覆盖掉窗口过程的第一个参数hWnd。
我们知道,类成员函数的第一个参数都是this指针,有了this指针,类成员函数就可以调用了。
下面的事情就是准备jump到成员函数中:m_jmp赋值为0xe9,一个相对跳转指令,m_relproc被赋值为相对成员函数相对thunk的地址,这两个成员变量也组成了一条汇编语句:
jmp WndProc
回到前面看看传递进来的成员函数的原型:
pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
GetWindowProc实际上就是返回成员函数WindowProc,原型如下:
template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
可以看到,成员函数只有三个参数,窗口过程的第一个参数被修改成了this指针,所以达到了巧妙将窗口过程修改成类的成员函数的目的。
Thunk的基本原理到这里已经结束了,还剩下一个问题就是前面提到的对new的override,这就牵涉到关于ATL thunk的最后一个问题:
内存是怎么分配的,是一段堆上的内存吗?
为什么要对new进行override?因为windows xp sp2之后,为了对付层出不穷的缓冲区溢出攻击,windows推出了一个新的feature叫Data execution prevention。如果这个feature被启用,那么堆上和栈上的数据是不可以执行的,如果thunk是位于new出来的代码,那么一执行就会crash。
为了解决这个问题,ATL override了new和delete运算符。
Override后的new最终会调用到函数__AllocStdCallThunk_cmn:

PVOID __AllocStdCallThunk_cmn ( VOID )
{
PATL_THUNK_ENTRY lastThunkEntry;
PATL_THUNK_ENTRY thunkEntry;
PVOID thunkPage; if (__AtlThunkPool == NULL) {
if (__InitializeThunkPool() == FALSE) {
}
} if (ATLTHUNK_USE_HEAP()) {
// On a non-NX capable platform, use the standard heap.
thunkEntry = (PATL_THUNK_ENTRY)HeapAlloc(GetProcessHeap(), 0, sizeof(ATL::_stdcallthunk));
return thunkEntry;
}
thunkPage = (PATL_THUNK_ENTRY)VirtualAlloc(NULL, PAGE_SIZE, MEM_COMMIT,PAGE_EXECUTE_READWRITE); // Create an array of thunk structures on the page and insert all but
// the last into the free thunk list. // The last is kept out of the list and represents the thunk allocation.
thunkEntry = (PATL_THUNK_ENTRY)thunkPage;
lastThunkEntry = thunkEntry + ATL_THUNKS_PER_PAGE - 1;
do {
__AtlInterlockedPushEntrySList(__AtlThunkPool,&thunkEntry->SListEntry);
thunkEntry += 1;
} while (thunkEntry < lastThunkEntry); return thunkEntry;
}

函数首先判断是不是第一次被调用,如果第一次被调用,则调用__InitializeThunkPool来进行初始化(后面会详细介绍他)。初始化主要是用来判断Data execution prevention功能是否启用了。
如果没有启用,则简单多了,直接调用HeapAlloc来分配内存。
如果启用了则复杂多了。ATL会调用VirtualAlloc来分配一段PAGE_EXECUTE_READWRITE属性的内存,这段内存是可以被执行的,为了节省内存,将这段内存分成很多块,每一块大小就是一个thunk的大小。
然后将这些块压入到一个list当中,需要的时候则从中取出,释放的时候又将块压入到list中。
由于即使只创建一个窗口也需要分配一个页面的大小,如果这个进程中有多个dll,每个dll都创建一个ATL的窗口,那么就会占用到很多页面空间,浪费内存。为了节省内存的使用,windows在进程的一个重要结构PEB偏移0x34的地方加入了一个域:
0:007> dt ntdll!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
。。。。。
+0x030 SystemReserved : [1] Uint4B
+0x034 AtlThunkSListPtr32 : Uint4B
+0x038 ApiSetMap : Ptr32 Void
。。。。。。
在前面的初始化函数__InitializeThunkPool中,会尝试从这个位置获取Thunk的list的head,如果发现是空的,才会调用VirtualAlloc来创建新的页面:

BOOL static DECLSPEC_NOINLINE __InitializeThunkPool ( VOID )
{
#define PEB_POINTER_OFFSET 0x34
PSLIST_HEADER *atlThunkPoolPtr;
PSLIST_HEADER atlThunkPool; result = IsProcessorFeaturePresent( 12 /*PF_NX_ENABLED*/ );
if (result == FALSE) {
// NX execution is not happening on this machine.
// Indicate that the regular heap should be used by setting
// __AtlThunkPool to a special value.
__AtlThunkPool = ATLTHUNK_USE_HEAP_VALUE;
return TRUE;
} atlThunkPoolPtr = (PSLIST_HEADER *)((PCHAR)(Atl_NtCurrentTeb()->ProcessEnvironmentBlock) + PEB_POINTER_OFFSET);
atlThunkPool = *atlThunkPoolPtr;
__AtlThunkPool = atlThunkPool;
return TRUE;
}

https://www.cnblogs.com/georgepei/archive/2012/03/30/2425472.html
ATL Thunk机制深入分析的更多相关文章
- MySQL Innodb日志机制深入分析
MySQL Innodb日志机制深入分析 http://blog.csdn.net/yunhua_lee/article/details/6567869 1.1. Log & Checkpoi ...
- Linux select 机制深入分析
Linux select 机制深入分析 作为IO复用的实现方式.select是提高了抽象和batch处理的级别,不是传统方式那样堵塞在真正IO读写的系统调用上.而是堵塞在sele ...
- 【MySQL】InnoDB日志机制深入分析
版权声明:尊重博主劳动成果,欢迎转载,转载请注明出处 --爱技术的华仔 Log & Checkpoint Innodb的事务日志是指Redo log,简称Log,保存在日志文件ib_logfi ...
- python反射机制深入分析
对编程语言比较熟悉的朋友,应该知道“反射”这个机制.Python作为一门动态语言,当然不会缺少这一重要功能.然而,在网络上却很少见到有详细或者深刻的剖析论文.下面结合一个web路由的实例来阐述pyth ...
- Java threadpool机制深入分析
简介 在前面的一篇文章里我对java threadpool的几种基本应用方法做了个总结.Java的线程池针对不同应用的场景,主要有固定长度类型.可变长度类型以及定时执行等几种.针对这几种类型的创建,j ...
- C++中const的实现机制深入分析
via:http://www.jb51.net/article/32336.htm C语言以及C++语言中的const究竟表示什么?其具体的实现机制又是如何实现的呢?本文将对这两个问题进行一些分析,需 ...
- linux IO多路复用POLL机制深入分析
POLL机制的作用这里就不进行介绍,根据linux man手册,解释为在一个文件描述符上等待某个事件.按照抽象一点的理解,当某个事件被触发(条件被满足),文件描述符变为有状态,那么用户空间可以根据此进 ...
- InnoDB Redo Flush及脏页刷新机制深入分析
概要: 我们知道InnoDB采用Write Ahead Log策略来防止宕机数据丢失,即事务提交时,先写重做日志,再修改内存数据页,这样就产生了脏页.既然有重做日志保证数据持久性,查询时也可以直接从缓 ...
- 理解ATL中的一些汇编代码(通过Thunk技术来调用类成员函数)
我们知道ATL(活动模板库)是一套很小巧高效的COM开发库,它本身的核心文件其实没几个,COM相关的(主要是atlbase.h, atlcom.h),另外还有一个窗口相关的(atlwin.h), 所以 ...
随机推荐
- 15款css3鼠标悬停图片动画过渡特效
分享15款css3鼠标悬停图片动画过渡特效.这是一款15款不同效果的css3 hover动画过渡效果代码.效果图如下: 在线预览 源码下载 实现的代码. html代码: <div class ...
- cad巧用插件自定义填充图形
很多同志如果遇到奇葩的填充图案,怎么办,找不到合适的,自己辛苦画了一遍,想把它作为自己的自定义的图案,怎么办呢. 今天老王给你您介绍个好用的插件. 首先在命令行输入命令 ap 弹出加载对话框 打开窗 ...
- WebSphere集群环境修改IHS端口号的方法 分类: WebSphere 2015-08-06 13:41 14人阅读 评论(0) 收藏
参考资料:http://wenku.baidu.com/link?url=E9BkuEjJ16i9lg7l91L0-xhKCYkHV0mAnlwAeSlDCFM4TjZyk4ZVxmUu64BGd4F ...
- [CNN] Tool - Deep Visualization
From: http://www.infoq.com/cn/news/2016/12/depth-neural-network-fake-photos 当时大部分的DNN在识别图像中对象的过程中主要依 ...
- OSG描边特效osgFX::Outline的修改
对一个三维场景中的物体实现描边特效,可以参考osg范例osgoutline 这个描边特效使用了模板缓存Stencil来实现,参见源代码osgFX/Outline.cpp 使用了两个Pass 第一个Pa ...
- [Linux] ssh-key 公钥文件格式
SSH 协议(Secure Shell 协议)最初在 1995 年由芬兰的 Tatu Ylönen 设计开发,由 IETF(Internet Engineering Task Force)的网络工作小 ...
- Linux内核 GPIO操作部分API
内核中关于GPIO的操作API主要集中在<linux/of_gpio.h>和<linux/gpio.h>中,前者主要是GPIO直接与设备树相关的操作,在Linux 设备树操作A ...
- [Python]编程之美
Task 1 : 首字母大写 import re #python 正则表达式包:re s='hello world' s=re.sub(r"\w+",lambda match:ma ...
- Go学习笔记(三)Go语言学习
这里我就不写具体的教程了,整理了一些很适合入门学习的网站 菜鸟Go入门 http://www.runoob.com/go/go-basic-syntax.html Go 语言的基本数据类型 https ...
- EF Core 2.1变化
EF Core 2.1随.NET Core 2.1一起发布,本篇文章总结一下EF Core的新增功能,先从简单的开始说. 一.延迟加载 延迟加载不用介绍了吧,直接看一下怎样配置吧.EF Core 2. ...