windows窗口类的封装——内联汇编传this

先贴一段简单的用 Windows Api创建窗口的代码:

 

#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[]	= TEXT("HelloWin");
	HWND hwnd,hwnd1;
	MSG msg;
	WNDCLASS wndclass;

	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc = WndProc;
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hInstance  = hInstance;
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
	wndclass.lpszMenuName = NULL;
	wndclass.lpszClassName = szAppName;

	if(!RegisterClass(&wndclass))
	{
		MessageBox(NULL, TEXT("this program requires windows nt!"), szAppName, MB_ICONERROR);
		return 0;
	}

	hwnd = CreateWindow( szAppName,
		TEXT("the hello program"),
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		NULL,
		NULL,
		hInstance,
		NULL);

	ShowWindow(hwnd, iCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);

	}
	
	return msg.wParam;

}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hDC;
	PAINTSTRUCT PaintSt;
	RECT aRect;
	
	switch(message)
	{
	case WM_PAINT:
		hDC = BeginPaint(hwnd,&PaintSt);

		//Get upper left and lower right of client area
		GetClientRect(hwnd,&aRect);

		SetBkMode(hDC,TRANSPARENT);

		//Now draw the text in window client area
		DrawText(
		hDC,
		L"Just test",
		-1,
		&aRect,
		DT_SINGLELINE|DT_CENTER|DT_VCENTER);

		EndPaint(hwnd,&PaintSt);
		
		return 0;
	
	case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);

}
窗口创建之前要先注册 wndclass,wndclass是一个跟窗口相关的数据结构,他的一些成员可以设置我们将要创建的窗口的属性。其中,我们比较最为关心的因该是wndclass.lpfnWndProc, 前缀 lpfn (long point funtion)表示该成员是一个指向函数的长 指针 。它指向要创建的窗口的回调函数——窗口过程,每一个wndclass必须有一个窗口过程,当 Windows 把属于特定窗口的消息发送给该窗口时,该窗口的窗口类负责处理所有的消息,如键盘消息或鼠标消息。由于窗口过程差不多智能地处理了所有的窗口消息循环,所以我们只要在其中加入消息处理过程即可。这个窗口过程函数是由操作系统负责调用的,只要有消息发往某窗口,操作系统就会调用相应的窗口过程,并传入该窗口的句柄。

而我们要封装窗口类,窗口过程就应该是属于该类的成员函数。创建了一个窗口对象之后,如果有消息发往该窗口,操作系统应该调用的是属于该对象的窗口过程,也就是窗口类的那个成员函数。可是回掉函数指针的类型是windows早已定义好的,而且操作系统调用窗口过程的时候是不会传入this指针的,因此需要我们在注册给操作系统的函数中保存this指针,并跳转到真正的成员窗口过程中去执行。一个简单的窗口封装Demo如下:

MyWnd.h

#include <windows.h>

class MyWnd
{
public:
	MyWnd(){};
	virtual ~MyWnd();
	LRESULT TrueWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
	BOOL Create(LPCTSTR lpszClassName,
                   LPCTSTR lpszWindowName, DWORD dwStyle,
                   const RECT& rect,
                   HWND pParentWnd, UINT nID,HINSTANCE hInst,
                   LPVOID lpParam = NULL);
	BOOL ShowWindow(int nCmdShow);
	BOOL UpdateWindow();
private:
	HWND m_hwnd;
	char *ProcPtr;
};

MyWnd.cpp

#include "MyWnd.h"

__declspec(naked) void tmp() //模板函数, __declspec(naked) 声明一个裸函数,他不会在函数中添加其他操作,即保证函数代码反汇编之后和其中的内联汇编是一样的
{
	__asm
	{
		call load //此时会把下一条指令的地址压栈
		nop
		nop     //空出八个字节存放this指针和实际成员窗口过程MyWnd::TrueWndProc的地址
		nop
		nop
		nop
		nop
		nop
		nop
load:	              
		pop eax  //pop call load之后指令的地址
		mov ecx, [eax] //实际上这个地址指向的存储空间中存放的就是this指针, 放入ecx寄存器
		mov ebx, 4[eax]	//之后四个字节存放的是<span style="font-family: Arial, Helvetica, sans-serif;">MyWnd::TrueWndProc的地址</span>
		jmp ebx  //因为窗口call这段代码的时候会把参数压栈,之前又把this放到了ecx寄存器中,相当于模拟thiscall调用约定用这个this指针调用了<span style="font-family: Arial, Helvetica, sans-serif;">TrueWndProc</span>
	}

}

LRESULT MyWnd::TrueWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hDC;
	PAINTSTRUCT PaintSt;
	RECT aRect;
	switch(message)
	{
	case WM_PAINT:
		hDC = BeginPaint(hwnd,&PaintSt);

		//Get upper left and lower right of client area
		GetClientRect(hwnd,&aRect);

		SetBkMode(hDC,TRANSPARENT);

		//Now draw the text in window client area
		DrawText(
		hDC,
		L"Just test",
		-1,
		&aRect,
		DT_SINGLELINE|DT_CENTER|DT_VCENTER);

		EndPaint(hwnd,&PaintSt);
		return 0;
	case WM_DESTROY:
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, lParam);

}

BOOL MyWnd::Create(LPCTSTR lpszClassName,
                   LPCTSTR lpszWindowName, DWORD dwStyle,
                   const RECT& rect,
                   HWND ParentWnd, UINT nID,HINSTANCE hInst,
                   LPVOID lpParam)
{
	HWND hwnd=::CreateWindow(lpszClassName, lpszWindowName,
					dwStyle,
					rect.left, rect.top,
					rect.right-rect.left, rect.bottom-rect.top,
					ParentWnd,
					(HMENU)nID, hInst,lpParam);
	if(hwnd != INVALID_HANDLE_VALUE)
	{
		m_hwnd = hwnd;
		
		DWORD thedw;
		char *f = (char *)malloc(30);<span style="white-space:pre">		</span>//开辟一段空间存放注册给操作系统调用的代码
		VirtualProtect(f,30,PAGE_EXECUTE_READWRITE,&thedw); //把这段空间设置为可读写执行
		char *true_func = (char *)(*((int *)((char *)tmp + 1)) + (int)tmp + 5); //true_func存放 tmp函数即那段汇编模板代码的实际地址
		memcpy(f, true_func, 30); //把汇编模板代码复制到开辟的空间中
		int *this_pos = (int *)(f + 5);
		*this_pos = (int)this;  //在前四个nop所占字节中放入this
		int *func_pos = this_pos + 1;
		int func_addr;
		
		__asm mov eax, MyWnd::TrueWndProc //在后四个字节中放入函数地址
		__asm mov func_addr, eax
		
		*func_pos = func_addr;
		ProcPtr = f;
		SetWindowLongW(hwnd, GWL_WNDPROC,(DWORD)f); //把存有代码的malloc出来的空间首地址设置为操作系统回调的窗口过程
		return true;
	}
	else
		return false;

}

BOOL MyWnd::ShowWindow(int nCmdShow)
{
	if(::IsWindow(m_hwnd))
		::ShowWindow(m_hwnd,nCmdShow);
	return false;
}

BOOL MyWnd::UpdateWindow()
{
	if(::IsWindow(m_hwnd))
		::UpdateWindow(m_hwnd);
	return false;
}

MyWnd::~MyWnd()
{
	free(ProcPtr);
}

Main.cpp

#include "MyWnd.h"

ATOM MyRegisterClass(HINSTANCE hInstance,TCHAR szAppName[])
{
	WNDCLASS wndclass;
	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc = DefWindowProc; //注册的时候先把窗口过程设置为默认,在create的时候再设置为实际的窗口过程
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hInstance  = hInstance;
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
	wndclass.lpszMenuName = NULL;
	wndclass.lpszClassName = szAppName;

	return RegisterClass(&wndclass);
}

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
	MSG msg;
	static TCHAR szAppName[]	= TEXT("HelloWin");
	MyRegisterClass(hInstance,szAppName);  //注册wndclass

	MyWnd test;
	RECT rt;
	rt.left=0,rt.top=0, rt.right=CW_USEDEFAULT,rt.bottom=0;
	if(!test.Create(szAppName,szAppName,WS_OVERLAPPEDWINDOW|WS_BORDER,rt,NULL, NULL, hInstance))
		return false;

	test.ShowWindow(iCmdShow);
	test.UpdateWindow();
 
    //先创建主窗口,然后进入消息循环
    while (GetMessage(&msg, NULL, 0, 0))
    {                 
		TranslateMessage(&msg);
        DispatchMessage(&msg);	
    }
         //收到WM_QUIT消息,退出
    return msg.wParam;
}

说一下基本思路:

要封装窗口,窗口句柄肯定是窗口类的私有成员,窗口过程是窗口类的公有成员函数,成员函数不能注册给操作系统调用,我的方法是在操作系统调用和成员函数调用之间加一个中间层,实际上就是tmp这段汇编代码模板。每创建一个对象malloc一段空间,f存放首地址,把tmp里边的代码内容memcpy到f指向的空间里,把this指针和要跳转的成员窗口过程的地址放到f中用nop占位的预留空间,把这段空间设置为可执行后注册给操作系统。这样就实现了this的保存和成员窗口过程的调用。

关于tmp模板的代码,主要就是利用call 会自动把原ip压栈,可以利用这个地址取出指令中保存的this指针和函数地址,具体有注释。

在数据段设置回调的那段内联汇编代码的时候应特别注意:

1、要用VirtualProtect设置这段内存的权限为可执行,默认数据段是不可执行的

2、char *true_func = (char *)(*((int *)((char *)tmp + 1)) + (int)tmp + 5); 为什么要有这段代码获得tmp函数的真正地址呢?因为编译器会把成员函数地址统一放在一个类似函数表的地方,tmp这个标号实际上表示的并不是函数真正的首地址,而是一句指令的地址:jmp trueAddr ,trueAddr是一个一个四字节的地址,也就是真正的tmp函数实际地址。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值