不使用全局函数如何编写线程类
Posted 计算机科学家的世界
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了不使用全局函数如何编写线程类相关的知识,希望对你有一定的参考价值。
以下讨论内容仅限于Window系统和X86硬件架构。从汇编角度看程序,只能看到mov,sub,div,xcmpchg等指令,函数,结构体,指针,数组等中级编程语言中的语法糖均不存在,至于C++,Java等高级语言中的对象,类,虚函数等更是不复存在,那么我们在高级语言中建议的语法糖在哪呢,它们是怎么被转化到汇编的呢?......
1,汇编之于函数调用 先看以下函数调用代码
int func(int x, int y) return x + y;在这上代码中,我们调用了func,参数是a(10),b(100),返回值存放在c中,现在问题来了,参数a,b是怎么传进来的,函数返回值是怎么返回的呢?在Visual Studio中我们使用cl -c -FA x.c(假设代码文件为x.c)命令来汇编以上代码,输出的即是以上代码对应的汇编信息:
int main() int a = 10, b = 100; int c = func(a, b);
; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.40629.0从以上汇编代码中可以看出,调用函数时,默认情况下参数从右到左入栈,同时,函数返回值存放在eax中,我们可以使用以下代码测试:(在VS上输出110)
TITLE E:\\tmp\\x.c
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES
PUBLIC _func
PUBLIC _main
; Function compile flags: /Odtp
_TEXT SEGMENT
_c$ = -12 ; size = 4
_a$ = -8 ; size = 4
_b$ = -4 ; size = 4
_main PROC
; File e:\\tmp\\x.c
; Line 6
push ebp
mov ebp, esp
sub esp, 12 ; 0000000cH
; Line 7
mov DWORD PTR _a$[ebp], 10 ; 0000000aH
mov DWORD PTR _b$[ebp], 100 ; 00000064H
; Line 8
mov eax, DWORD PTR _b$[ebp]
push eax
mov ecx, DWORD PTR _a$[ebp]
push ecx
call _func
add esp, 8
mov DWORD PTR _c$[ebp], eax
; Line 9
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
; Function compile flags: /Odtp
_TEXT SEGMENT
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_func PROC
; File e:\\tmp\\x.c
; Line 2
push ebp
mov ebp, esp
; Line 3
mov eax, DWORD PTR _x$[ebp]
add eax, DWORD PTR _y$[ebp]
; Line 4
pop ebp
ret 0
_func ENDP
_TEXT ENDS
END
int func(int x, int y)以上汇编代码和实际测试代码表明在Win32上,函数调用是使用eax作返回值的,现在问题来了,如果需要返回的对象特别大,超过4个字节(eax大小为4字节,rax为8),那怎么处理呢?在这种情况下,主调函数会在堆栈上多分配一块内存,然后被调函数将返回值存放在这块内存中,最后返回这块内存的首地址---放在eax中。我们使用以下代码作测试:
return x + y;
int main()
int a = 10, b = 100, c;
int (*fptr)(int,int) =func;
__asm
push b //b入stack
push a //a入stack
call fptr //jmp到func
mov c, eax //eax是返回值,用c存储
add esp, 8 //平衡stack
printf("%d\\n",c);
将文件存为x.c,然后使用cl -c -FA x.c汇编代码,将会生成x.asm,打开之后内容如下:
typedef struct Record
int ary[100];
int x;
Record;
Record func()
Record rc;
rc.x = 100;
return rc;
int main()
Record r = func();
printf("%d", r.x);
; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.40629.0在x.asm中,有三块被标红,第一块是一条栈分配指令sub esp,这条指令用于存放两个record结构体,可能有人要问,我明明在main里面只定义了一个Record对象,为什么main函数要在栈上分配这么大一块内存,其实之前也已经说明了,在函数调用过程中,eax往往用来保存函数返回值,但是,如果返回值过大,主调函数就会在进入被调函数之前,在自己的栈上挖出一块内存,被调函数将待返回的对象写入这块地址,然后使用eax返回这个块内存的地址。在x.asm中,最后一块标红的汇编代码就是被调函数(func)将待返回的对象写入main所在的栈上预分配好的一块内存(ecx是循环计数器,101就是表明要复制101个int,而Record刚好就是这么大)。第二块标红的汇编代码就是将func返回的对象首地址的内容复制到r。为了验证以上猜想,我们可以使用以下代码验证:(代码毫无疑问肯定输出100)
TITLE E:\\tmp\\x.c
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES
_DATA SEGMENT
$SG1335 DB '%d', 00H
_DATA ENDS
PUBLIC _func
PUBLIC _main
EXTRN _printf:PROC
EXTRN @__security_check_cookie@4:PROC
EXTRN ___security_cookie:DWORD
; Function compile flags: /Odtp
_TEXT SEGMENT
$T1 = -812 ; size = 404
_r$ = -408 ; size = 404
__$ArrayPad$ = -4 ; size = 4
_main PROC
; File e:\\tmp\\x.c
; Line 14
push ebp
mov ebp, esp
sub esp, 812 ; 0000032cH
mov eax, DWORD PTR ___security_cookie
xor eax, ebp
mov DWORD PTR __$ArrayPad$[ebp], eax
push esi
push edi
; Line 15
lea eax, DWORD PTR $T1[ebp]
push eax
call _func
add esp, 4
mov ecx, 101 ; 00000065H
mov esi, eax
lea edi, DWORD PTR _r$[ebp]
rep movsd
; Line 16
mov ecx, DWORD PTR _r$[ebp+400]
push ecx
push OFFSET $SG1335
call _printf
add esp, 8
; Line 17
xor eax, eax
pop edi
pop esi
mov ecx, DWORD PTR __$ArrayPad$[ebp]
xor ecx, ebp
call @__security_check_cookie@4
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
; Function compile flags: /Odtp
_TEXT SEGMENT
_rc$ = -408 ; size = 404
__$ArrayPad$ = -4 ; size = 4
$T1 = 8 ; size = 4
_func PROC
; File e:\\tmp\\x.c
; Line 8
push ebp
mov ebp, esp
sub esp, 408 ; 00000198H
mov eax, DWORD PTR ___security_cookie
xor eax, ebp
mov DWORD PTR __$ArrayPad$[ebp], eax
push esi
push edi
; Line 10
mov DWORD PTR _rc$[ebp+400], 100 ; 00000064H
; Line 11
mov ecx, 101 ; 00000065H
lea esi, DWORD PTR _rc$[ebp]
mov edi, DWORD PTR $T1[ebp]
rep movsd
mov eax, DWORD PTR $T1[ebp]
; Line 12
pop edi
pop esi
mov ecx, DWORD PTR __$ArrayPad$[ebp]
xor ecx, ebp
call @__security_check_cookie@4
mov esp, ebp
pop ebp
ret 0
_func ENDP
_TEXT ENDS
END
以上讨论我们明白了函数调用过程中,返回值的传递与保存过程,以下再看面向对象中类的成员函数调用过程。
typedef struct Record
int ary[100];
int x;
Record;
Record func()
Record rc;
rc.x = 100;
return rc;
int main()
Record* r;
func(); //调用函数
__asm
mov r, eax //返回值就是func中返回的对象的首地址,在此取出
printf("%d", r->x);
2,面向对象程序设计中成员函数调用过程与原理 面向对象程序设计中,我们经常使用类,而事实上,面向对象的三大特性就是封装、继承、多态,虚函数的使用极大方便程序的编写,麻烦了程序的调试(面向对象程序比C语言程序难调)。在这一节,将主要讨论类的成员函数调用过程,上一节中主要讨论了普通函数的调用(从语义上讲,类的静态函数就是外部普通函数),与普通函数比,类的成员函数多了一个this指针(有的语言称为self),先以以下代码为例子:
使用cl -FA -c x.cpp汇编以上代码,得到x.asm,内容如下:
class CK
public:
CK()
m_iVal = 10;
public:
void Show()
m_iVal += 10;
private:
int m_iVal;
;
int main()
CK obj;
obj.Show();
; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.40629.0以上代码中有三块标红,第一块是调用CK的构造,第二块是调用Show,第三块是Show的部分汇编代码,在调用Show之前,可以看到一个lea ecx, DWORD PTR _obj$[ebp],lea是X86中的取址指令,这条指令的意思是取对象的地址(this指针),在Show的汇编代码中,把ecx取出来,再基于ecx取对象的m_iVal值,以上一些汇编代码给人一种 类的成员函数调用与普通成员函数调用完全一样,只是使用ecx传递this指针的感觉,事实上这种感觉是对的。 我们使用以下代码去验证:
TITLE E:\\tmp\\x.cpp
.686P
.XMM
include listing.inc
.model flat
INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES
PUBLIC ??0CK@@QAE@XZ ; CK::CK
PUBLIC ?Show@CK@@QAEXXZ ; CK::Show
PUBLIC _main
; Function compile flags: /Odtp
_TEXT SEGMENT
_obj$ = -4 ; size = 4
_main PROC
; File e:\\tmp\\x.cpp
; Line 16
push ebp
mov ebp, esp
push ecx
; Line 17
lea ecx, DWORD PTR _obj$[ebp]
call ??0CK@@QAE@XZ ; CK::CK
; Line 18
lea ecx, DWORD PTR _obj$[ebp]
call ?Show@CK@@QAEXXZ ; CK::Show
; Line 19
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
; Function compile flags: /Odtp
; COMDAT ?Show@CK@@QAEXXZ
_TEXT SEGMENT
_this$ = -4 ; size = 4
?Show@CK@@QAEXXZ PROC ; CK::Show, COMDAT
; _this$ = ecx
; File e:\\tmp\\x.cpp
; Line 8
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
; Line 9
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR [eax]
add ecx, 10 ; 0000000aH
mov edx, DWORD PTR _this$[ebp]
mov DWORD PTR [edx], ecx
; Line 10
mov esp, ebp
pop ebp
ret 0
?Show@CK@@QAEXXZ ENDP ; CK::Show
_TEXT ENDS
; Function compile flags: /Odtp
; COMDAT ??0CK@@QAE@XZ
_TEXT SEGMENT
_this$ = -4 ; size = 4
??0CK@@QAE@XZ PROC ; CK::CK, COMDAT
; _this$ = ecx
; File e:\\tmp\\x.cpp
; Line 4
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
; Line 5
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], 10 ; 0000000aH
; Line 6
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
??0CK@@QAE@XZ ENDP ; CK::CK
_TEXT ENDS
END
#include <stdio.h>3,构造自己的线程类 线程是操作系统中重要的概念,是操作系统中可以异步执行的执行体,在Win32中,如果要创建一个线程,我们需要使用CreateThread或者__beginthreadex等API,这些API有个特点,即需要传递一个非类成员函数(静态函数或普通C函数),在公司的AngelicaES引擎中,线程的创建使用的就是全局静态函数,然而,在Java中,创建线程只需要一个Thread对象或一个Thread对象加一个实现了Runable的接口,在Java中,使用线程,往往构造一个Thread对象,然后调用Start函数,线程就起来了,并没有我们看到的全局函数或者静态成员函数,那么Java等高级语言是怎么实现的呢?在前两节中,已经讲清楚了普通函数调用和类的成员函数调用的过程与原理,事实上,只需要使用这个原理就可以实现一个类似Java的线程函数(不使用任何全局函数或static函数),如果想将一个类的成员函数作为线程函数来执行,在语法层次我们无法逃脱this指针的束缚。但是,事实上,我们可以编写一段奇怪的二进制指令(可直接执行的机器码),在字节码里面设置好this指针等信息并跳转到类的成员函数中去,最后将这段字节码作为线程函数去执行(强制转换成CreateThread需要的线程函数类型),但是,我们并没有使用使用全局函数或static函数,先看以下一段字节码:
class Base
public:
virtual void Show(int x, int y) = 0;
;
class Derived: public Base
public:
Derived()
m_iValue = 10;
public:
virtual void Show(int x, int y)
printf("%d", x + y + m_iValue);
private:
int m_iValue;
;
int main()
Derived * obj = new Derived;
auto pMemFunc =&Derived::Show;
__asm
mov ecx, obj //传递this
push 10 //y
push 100 //x
call pMemFunc; //调用成员函数
// add esp, 8 //平衡stack,并不需要。因为类的成员函数是__thiscall方式,会自己解决参数造成的堆栈不平衡
const static unsigned char g_thread_proc[]=在以上一段字节码中,我们会设置好this指针和真正的线程函数地址,然后使用call跳转过去,所有的代码如下所示:
//------------parameter-----------------
0x8B,0x44,0x24,0x04, // mov eax,dword ptr [esp+10h]
0x50, // push eax
//-----------this pointer-------------
0xB9,0x00,0x00,0x00,0x00, // mov ecx,0x12FF5C
//-----------call back function-------------
0xB8,0x00,0x00,0x00,0x00, // mov eax,0
0xFF,0xD0, // call eax
//return
0xC2,0x10,0x00 // ret 10h
;
//core.h
#ifndef __ZX_CORE_H__
#define __ZX_CORE_H__
#include <windows.h>
#ifndef interface
#define interface struct
#endif
#ifndef implement
#define implement :public
#endif
const static unsigned char g_thread_proc[]=
//------------parameter-----------------
0x8B,0x44,0x24,0x04, // mov eax,dword ptr [esp+10h]
0x50, // push eax
//-----------this pointer-------------
0xB9,0x00,0x00,0x00,0x00, // mov ecx,0x12FF5C
//-----------call back function-------------
0xB8,0x00,0x00,0x00,0x00, // mov eax,0
0xFF,0xD0, // call eax
//return
0xC2,0x10,0x00 // ret 10h
;
#endif
//runnable.h
#ifndef __ZX_RUNNABLE_H__
#define __ZX_RUNNABLE_H__
#include "core.h"
interface ZXRunnable
virtual void run(void* lpParameter)= 0;
;
#endif
//thread.h
#ifndef __ZX_THREAD_H__
#define __ZX_THREAD_H__
#include "core.h"
#include "runnable.h"
class ZXThread
public:
ZXThread();
ZXThread(ZXRunnable* runnable);
virtual ~ZXThread();
public:
void Start();
void Wait();
void SetRunnable(ZXRunnable* runnable);
ZXRunnable* GetRunnable();
private:
ZXRunnable* m_pRunnable;
HANDLE m_hThread;
unsigned char m_thread_proc[sizeof(g_thread_proc)];
;
#endif
//thread.cpp
#include "thread.h"
ZXThread::ZXThread(): m_pRunnable(NULL), m_hThread(NULL)
ZXThread::ZXThread(ZXRunnable* runnable): m_pRunnable(runnable), m_hThread(NULL)
ZXThread::~ZXThread()
delete m_pRunnable;
void ZXThread::SetRunnable(ZXRunnable* runnable)
m_pRunnable= runnable;
ZXRunnable* ZXThread::GetRunnable()
return(m_pRunnable);
void ZXThread::Start()
CopyMemory(m_thread_proc, g_thread_proc, sizeof(g_thread_proc));
*(int*)(&m_thread_proc[6])= (int)m_pRunnable;
void (ZXRunnable::*func)(void* lpParameter)= &ZXRunnable::run;
int addr;
__asm
mov eax, func
mov addr, eax
*(int*)(&m_thread_proc[11])= addr;
m_hThread= ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(void*)m_thread_proc,
NULL, 0, NULL);
void ZXThread::Wait()
::WaitForSingleObject(m_hThread, INFINITE);
测试代码:
#include <iostream>
#include "thread.h"
using namespace std;
class ZXRun implement ZXRunnable
public:mark:直接运行以上程序会崩溃,因为DEP,解决方法有两种:1)在编译器里面关掉DEP;2)最靠谱的作法是使用VirtualProtect将 ZXThread中的 m_thread_proc对应的内存块设置为可执行即可。(缓冲区溢出攻击经常使用 m_thread_proc字节码的手段)。
virtual void run(void* lpParameter)
cout<<"Hello,World!"<<endl;
;
int main()
ZXThread boss(new ZXRun);
boss.Start();
boss.Wait();
4,总结 上面实际给出了解决一类难题的思路,即如果某个地方需要一个全局的函数(或类的static函数)--- Thunk技术 ,而我们想要完全面向对象(即我们不想使用全局函数或非static类函数),解决方法就是使用机器码,在机器码内完全跳转(Thunk技术---跟Knuth有点像,以前看过国内一位大牛仅使用4个字节就实现封装Windows窗口消息函数的代码,而只要百度那4个字节,就可以搜索出那位牛人,貌似是金山的一位大牛,佩服)。
以上是关于不使用全局函数如何编写线程类的主要内容,如果未能解决你的问题,请参考以下文章