最近心情越来越差了,我心情不好的时候就喜欢倒腾些小玩意和吃东西,下面就是我的小玩意之一,其实也是两个月以前的代码了,现在把它写成文档,希望能帮我回复心情,现在开始正题~~
两年前我就听过这个技术,大概其也算是懂点,但是没实做过,前些时用MFC做UI时需要一个对SetTimer API的封装,因为这类API对C++对象的支持很不好,所以,我们总得耍点花招对付它,于是重拾起了这个Thunk。
对于SetTimer API我就不做过多介绍了,MSDN都有,现在只看他这个回调函数
MSDN的上是这样的: VOID CALLBACK TimerProc(HWND, UINT,UINT_PTR, DWORD);
注意,调用方式是 CALLBACK 也就是stdcall,压栈方式从右至左,函数本身清理stack(因为stdcall不考虑可变参数问题,不明白的请google),例如:
void stdcall test(int n)
{
}
int main()
{
test(33);
return 0;
}
在main中生成的代码应该是类似
push 21h //传递参数33
call test //调用test
而test的代码应该是
void _stdcall(int n)
{
....
ret 4 //因为这里需要函数本身清理堆栈(stdcall)
}
因此,根据VOID CALLBACK TimerProc(HWND, UINT,UINT_PTR, DWORD);我们大概可以知道操作系统调用我们注册的回调函数TimerProc时候的动作了,下面的问题是如果玩点花样~
既然我们想把一个C++的对象和回调函数联系起来,那么只要把这个对象的指针存到某个地方就可以了,不考虑thunk技术的话用个全局表也未尝不可,现在单说thunk
既然我们知道系统调用TimerProc生成的汇编代码是call TimerProc,那么注册进去的函数指针只要最终能修正堆栈就可以了(我这里针对这个settimer就不考虑任何其他参数问题了,不过大同小异)
看这个class的一部分
class TimerEx
{
private:
#pragma pack(push, 1)
struct Thunk
{
t_byte push;
t_uint32 pthis;
t_byte call;
t_uint32 offset;
t_byte ret;
t_uint16 retn;
};
#pragma pack(pop)
private:
Thunk m_thunk;
t_uint32 m_timer_id;
public:
static void TimerFunc(TimerEx *ptimer)
{
ptimer->OnTimer();
}
public:
virtual void OnTimer()
{
printf("OnTimer/n");
}
......
};
#pragma pack(push,1)是为了防止编译器做优化对其操作
我现在希望每当设定的timer到时之后都调用一次这个类OnTimer,因此也就要调用TimerFunc(废话~),从上面我们得知,既然一定会有call操作,那么不如让系统call到一段可配置的内存段(这段瞧不懂得要看汇编去了),这个所谓的可配置的内存段就是那个Thunk结构,也负责记录这个类的this指针(基地址),看下我的实现代码:
TimerEx() : m_timer_id(0)
{
memset(&m_thunk, 0, sizeof(Thunk));
m_thunk.push = 0x68;//push指令的二进制码
m_thunk.pthis = (t_uint32)this;//记录this指针
m_thunk.call = 0xE8; //call的二进制码
/*注意,以下三行都是因为call(jmp)的定址方式,在win32中,call的函数地址其实是当前指令地址的下一条指令与被call的地址(TimerEx::TimerFunc)的偏移,
因为call + 操作数为5个字节,所以才有op_offset += 5*/
t_uint32 proc_addr = (t_uint32)(&TimerEx::TimerFunc);
t_uint32 op_offset = (t_uint32)&(m_thunk.call);
op_offset += 5;
m_thunk.offset = proc_addr - op_offset;
m_thunk.ret = 0xC2; //返回
/*注意: 因为 static void TimerFunc(TimerEx *ptimer)是_cdecl,所以调用方清理堆栈,因此算上已经压入的四个参数,一共是20
*/
m_thunk.retn = 20;
}
以下是设置代码
bool TimerEx::StartTimer(t_uint32 interval)
{
m_timer_id = ::SetTimer(NULL, 0, interval, (::TIMERPROC)m_thunk);
return m_timer_id != 0;
}
class TimerEx
{
private:
#pragma pack(push, 1)
struct Thunk
{
t_byte push;
t_uint32 pthis;
t_byte call;
t_uint32 offset;
t_byte ret;
t_uint16 retn;
};
#pragma pack(pop)
private:
Thunk m_thunk;
t_uint32 m_timer_id;
public:
static void TimerFunc(TimerEx *ptimer)
{
ptimer->OnTimer();
}
public:
virtual void OnTimer()
{
printf("OnTimer/n");
}
public:
TimerEx() : m_timer_id(0)
{
memset(&m_thunk, 0, sizeof(Thunk));
m_thunk.push = 0x68;
m_thunk.pthis = (t_uint32)this;
m_thunk.call = 0xE8;
t_uint32 proc_addr = (t_uint32)(&TimerEx::TimerFunc);
t_uint32 op_offset = (t_uint32)&(m_thunk.call);
op_offset += 5;
m_thunk.offset = proc_addr - op_offset;
m_thunk.ret = 0xC2;
m_thunk.retn = 20;
}
virtual ~TimerEx()
{
if(m_timer_id != 0)
{
StopTimer();
}
}
private:
TimerEx(const TimerEx &other);
TimerEx& operator=(const TimerEx &other);
public:
bool StartTimer(t_uint32 interval)
{
m_timer_id = ::SetTimer(NULL, 0, interval, (::TIMERPROC)&m_thunk);
return m_timer_id != 0;
}
void StopTimer()
{
if(m_timer_id != 0)
{
::KillTimer(NULL,m_timer_id);
m_timer_id = 0;
}
}
};
class TimerExTest : public TimerEx
{
public:
virtual void OnTimer()
{
printf("TimerExTest/n");
}
};
void Run();
int main()
{
TimerEx tex;
TimerExTest tex2;
tex.StartTimer(1000);
tex2.StartTimer(2000);
Run();
printf("done/n");
cin.get();
return 0;
}
void Run()
{
::MSG msg;
BOOL bRet;
HWND hWnd;
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
cout << ::GetLastError() << endl;
exit(-1);
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
两年前我就听过这个技术,大概其也算是懂点,但是没实做过,前些时用MFC做UI时需要一个对SetTimer API的封装,因为这类API对C++对象的支持很不好,所以,我们总得耍点花招对付它,于是重拾起了这个Thunk。
对于SetTimer API我就不做过多介绍了,MSDN都有,现在只看他这个回调函数
MSDN的上是这样的: VOID CALLBACK TimerProc(HWND, UINT,UINT_PTR, DWORD);
注意,调用方式是 CALLBACK 也就是stdcall,压栈方式从右至左,函数本身清理stack(因为stdcall不考虑可变参数问题,不明白的请google),例如:
void stdcall test(int n)
{
}
int main()
{
test(33);
return 0;
}
在main中生成的代码应该是类似
push 21h //传递参数33
call test //调用test
而test的代码应该是
void _stdcall(int n)
{
....
ret 4 //因为这里需要函数本身清理堆栈(stdcall)
}
因此,根据VOID CALLBACK TimerProc(HWND, UINT,UINT_PTR, DWORD);我们大概可以知道操作系统调用我们注册的回调函数TimerProc时候的动作了,下面的问题是如果玩点花样~
既然我们想把一个C++的对象和回调函数联系起来,那么只要把这个对象的指针存到某个地方就可以了,不考虑thunk技术的话用个全局表也未尝不可,现在单说thunk
既然我们知道系统调用TimerProc生成的汇编代码是call TimerProc,那么注册进去的函数指针只要最终能修正堆栈就可以了(我这里针对这个settimer就不考虑任何其他参数问题了,不过大同小异)
看这个class的一部分
class TimerEx
{
private:
#pragma pack(push, 1)
struct Thunk
{
t_byte push;
t_uint32 pthis;
t_byte call;
t_uint32 offset;
t_byte ret;
t_uint16 retn;
};
#pragma pack(pop)
private:
Thunk m_thunk;
t_uint32 m_timer_id;
public:
static void TimerFunc(TimerEx *ptimer)
{
ptimer->OnTimer();
}
public:
virtual void OnTimer()
{
printf("OnTimer/n");
}
......
};
#pragma pack(push,1)是为了防止编译器做优化对其操作
我现在希望每当设定的timer到时之后都调用一次这个类OnTimer,因此也就要调用TimerFunc(废话~),从上面我们得知,既然一定会有call操作,那么不如让系统call到一段可配置的内存段(这段瞧不懂得要看汇编去了),这个所谓的可配置的内存段就是那个Thunk结构,也负责记录这个类的this指针(基地址),看下我的实现代码:
TimerEx() : m_timer_id(0)
{
memset(&m_thunk, 0, sizeof(Thunk));
m_thunk.push = 0x68;//push指令的二进制码
m_thunk.pthis = (t_uint32)this;//记录this指针
m_thunk.call = 0xE8; //call的二进制码
/*注意,以下三行都是因为call(jmp)的定址方式,在win32中,call的函数地址其实是当前指令地址的下一条指令与被call的地址(TimerEx::TimerFunc)的偏移,
因为call + 操作数为5个字节,所以才有op_offset += 5*/
t_uint32 proc_addr = (t_uint32)(&TimerEx::TimerFunc);
t_uint32 op_offset = (t_uint32)&(m_thunk.call);
op_offset += 5;
m_thunk.offset = proc_addr - op_offset;
m_thunk.ret = 0xC2; //返回
/*注意: 因为 static void TimerFunc(TimerEx *ptimer)是_cdecl,所以调用方清理堆栈,因此算上已经压入的四个参数,一共是20
*/
m_thunk.retn = 20;
}
以下是设置代码
bool TimerEx::StartTimer(t_uint32 interval)
{
m_timer_id = ::SetTimer(NULL, 0, interval, (::TIMERPROC)m_thunk);
return m_timer_id != 0;
}
class TimerEx
{
private:
#pragma pack(push, 1)
struct Thunk
{
t_byte push;
t_uint32 pthis;
t_byte call;
t_uint32 offset;
t_byte ret;
t_uint16 retn;
};
#pragma pack(pop)
private:
Thunk m_thunk;
t_uint32 m_timer_id;
public:
static void TimerFunc(TimerEx *ptimer)
{
ptimer->OnTimer();
}
public:
virtual void OnTimer()
{
printf("OnTimer/n");
}
public:
TimerEx() : m_timer_id(0)
{
memset(&m_thunk, 0, sizeof(Thunk));
m_thunk.push = 0x68;
m_thunk.pthis = (t_uint32)this;
m_thunk.call = 0xE8;
t_uint32 proc_addr = (t_uint32)(&TimerEx::TimerFunc);
t_uint32 op_offset = (t_uint32)&(m_thunk.call);
op_offset += 5;
m_thunk.offset = proc_addr - op_offset;
m_thunk.ret = 0xC2;
m_thunk.retn = 20;
}
virtual ~TimerEx()
{
if(m_timer_id != 0)
{
StopTimer();
}
}
private:
TimerEx(const TimerEx &other);
TimerEx& operator=(const TimerEx &other);
public:
bool StartTimer(t_uint32 interval)
{
m_timer_id = ::SetTimer(NULL, 0, interval, (::TIMERPROC)&m_thunk);
return m_timer_id != 0;
}
void StopTimer()
{
if(m_timer_id != 0)
{
::KillTimer(NULL,m_timer_id);
m_timer_id = 0;
}
}
};
class TimerExTest : public TimerEx
{
public:
virtual void OnTimer()
{
printf("TimerExTest/n");
}
};
void Run();
int main()
{
TimerEx tex;
TimerExTest tex2;
tex.StartTimer(1000);
tex2.StartTimer(2000);
Run();
printf("done/n");
cin.get();
return 0;
}
void Run()
{
::MSG msg;
BOOL bRet;
HWND hWnd;
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
cout << ::GetLastError() << endl;
exit(-1);
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}