If you have SDK programming experience, you must know that you need to specify the window class when creating the window. An important parameter in the window class is the window process. Messages received by any window are processed by this window process.
In object-oriented programming, if developers need to use the original Window Process as a process-oriented development method, the object-oriented method is not so pure. Therefore, in the interface programming framework, the Framework often hides the Window Process, and developers can see classes one by one.
To process a message, you must add the response message map to the class corresponding to the window.
Then, how does the framework associate the Window Process with the class corresponding to the window? ATL uses a mechanism called thunk. Because the dump we pulled out has a lot of cases with window problems, and finally found that it has a certain relationship with thunk, I did some research on the ATL thunk.
The basic principle of Thunk is to allocate a piece of memory, and then set the window process to this part of memory. This section of memory is used to replace the first parameter (window handle) in the window process with the this pointer of the class and run the jump command into the winproc function of the class. This completes a conversion from the window process to the member functions of the class.
There are several points to focus on:
- When will the thunk memory be allocated, and when will the window process be set to the thunk memory.
- How is the memory allocated? Is it a heap of memory?
- What is the memory?
Let's take a look at the first question:
When to allocateThunkWhen will the window process be setThunkMemory.
When creating a window, ATL uses a macro to define the window class: declare_wnd_class (_ T ("My window class "))
The macro is defined as follows:
# DefineDeclare_wnd_class (wndclassname )\
StaticATL: cwndclassinfo & getwndclassinfo ()\
{\
StaticATL: cwndclassinfo WC = \
{\
{Sizeof(Wndclassex), cs_hredraw | cs_vredraw | cs_dblclks, startwindowproc ,\
0,0, Null, (hbrush) (color_window +1), Null, wndclassname, null },\
Null, null, idc_arrow, true,0, _ T ("")\
};\
ReturnWC ;\
}
We can see that this macro is actually defining a static function. Its function is to return an ATL: cwndclassinfo object, which is actually the encapsulation of the window class by ATL, the window procedure is specified as startwindowproc.
When you call cwindowimpl: Create to create a window, the create function of cwindowimplbaset, the parent class of cwindowimpl, is called:
This function is as follows:
Template <ClassTbase,ClassTwintraits>
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 =: createmediawex (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 );
.............
ReturnHwnd;
}
We can see that Thunk is initialized with null first. As mentioned in the comment, the initialization here is because if the memory allocation fails, it can better handle errors. In fact, Thunk can also be initialized when processing the first message in the window.
Next, call the Windows API createmediawex to create a window. We know that the Window Process of the window class used in this window is startwindowproc. Let's see how it works.Code.
Template < Class Tbase, Class Twintraits>
Lresult callback cwindowimplbaset <tbase, twintraits >:: startwindowproc (hwnd, uint umsg, wparam, 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 is a static function of the class, so it can be used directly as a window process. We can see that its parameters are exactly the four parameters of the window process.
The function first obtains the this pointer and assigns the passed window handle to the member variable m_hwnd of this. Then m_thunk.init is used to re-Initialize thunk. At this time, two null values are passed in, but a member function of the class and the this pointer.
The initialization details will be analyzed later.
After initialization, we can get the thunk address (m_thunk.getwndproc is to get the thunk address), and then call setwindowlongptr to set the window process to the thunk address. When there is a message in the next window, it will go directly to thunk.
The thunk address is directly called to pass the message that startwindowproc is processing to the thunk for processing, so as to avoid the loss of the first message in the window.
What is the memory?
Next we will focus on analyzing what functions m_thunk.init is to accomplish.
The code for cdynamicstdcallthunk. init is as follows:
Bool Init (dword_ptr proc,Void* Pthis)
{
If(Pthunk = NULL)
{
Pthunk =New_ Stdcallthunk;
If(Pthunk = NULL)
{
ReturnFalse;
}
}
ReturnPthunk-> Init (Proc, pthis );
}
The code is very simple. assign a structure (_ stdcallthunk) and then continue to call the init function of this structure. (Override the operator "new" here. The actual task is not simply to allocate memory from the stack, which will be detailed later)
Structure
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 = 0x010944c7 ; // 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 has four members. The first member is m_mov and is assigned 0x0000044c7. The second member is m_this, which is assigned the address of the corresponding class of the window.
The two Dwords actually constitute an Assembly statement:
MoV dword ptr [esp + 0x4], pthis
As we know before, the window process has been set to the starting address of the memory segment. That is to say, the first line of code in the window process is this line of code.
ESP is the pointer to the top of the stack, and ESP + 0x4 is the first parameter hwnd in the window process. This Code indicates that this pointer overwrites the first parameter hwnd in the window process.
We know that the first parameter of the class member function is the this pointer. With this pointer, the class member function can be called.
The following is to prepare the jump to the member function: m_jmp is assigned 0 x E9, a relative jump command, m_relproc is assigned the address of the relative member function relative to thunk, these two member variables also form an Assembly statement:
JMP wndproc
Go back to the front and check the prototype of the passed member function:
Pthis-> m_thunk.init (pthis-> getwindowproc (), pthis );
Getwindowproc actually returns the member function windowproc. The prototype is as follows:
Template <class tbase, class twintraits>
Lresult callback cwindowimplbaset <tbase, twintraits>: windowproc (hwnd, uint umsg, wparam, lparam );
It can be seen that the member function has only three parameters, and the first parameter in the window process is modified to the this pointer, so it cleverly modifies the window process to a member function of the class.
The basic principle of Thunk is now over, and the next problem is the override of New mentioned above. This involves the last question about ATL thunk:
How is the memory allocated? Is it a heap of memory?
Why override New? Since Windows XP SP2, in order to deal with endless buffer overflow attacks, Windows has launched a new feature called Data Execution Prevention. If this feature is enabled, the data on the heap and stack cannot be executed. If the Thunk is in the new code, the execution will crash.
To solve this problem, ATL override uses the new and delete operators.
The new after override will eventually call the function _ allocstdcallthunk_cen:
Pvoid _ allocstdcallthunk_cen (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
// 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;
}
The function first checks whether it is called for the first time. If it is called for the first time, it calls _ initializethunkpool for initialization (which will be detailed later ). Initialization is mainly used to determine whether the Data Execution Prevention function is enabled.
If it is not enabled, it is much simpler to directly call heapalloc to allocate memory.
If enabled, it is much more complicated. ATL will call virtualalloc to allocate a memory segment with the page_execute_readwrite attribute. This memory segment can be executed. To save the memory, it is divided into multiple blocks, each of which is the size of a thunk.
Then, these blocks are pushed to a list. When necessary, the blocks are taken out, and when released, the blocks are pushed to the list.
Because a page size needs to be allocated even if only one window is created, if there are multiple DLL in this process and each DLL creates an ATL window, it will occupy a lot of page space, memory waste. To save memory usage, Windows adds a domain to an important structure of the process, peb offset 0x34:
0: 007> dt ntdll! _ Peb
+ 0x000 inheritedaddressspace: uchar
+ 0x001 readimagefileexecoptions: uchar
.....
+ 0x030 systemreserved: [1] uint4b
+ 0x034 atlthunkslistptr32: uint4b
+ 0x038 apisetmap: ptr32 void
......
In the previous initialization function _ initializethunkpool, we will try to obtain the head of the thunk list from this position. If it is found to be empty, we will call virtualalloc to create a new page:
BoolStaticDeclspec_noinline _ initializethunkpool (void)
{
# DefinePeb_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 shoshould 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;
}