先贴一段简单的用 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函数实际地址。