您的位置:首页 > 其它

不使用全局函数如何编写线程类

2015-11-06 22:16 239 查看
        以下讨论内容仅限于Window系统和X86硬件架构。

        从汇编角度看程序,只能看到mov,sub,div,xcmpchg等指令,函数,结构体,指针,数组等中级编程语言中的语法糖均不存在,至于C++,Java等高级语言中的对象,类,虚函数等更是不复存在,那么我们在高级语言中建议的语法糖在哪呢,它们是怎么被转化到汇编的呢?......

1,汇编之于函数调用
        先看以下函数调用代码

int func(int x, int y){
return x + y;
}

int main(){
int a = 10, b = 100;
int c = func(a, b);
}

        在这上代码中,我们调用了func,参数是a(10),b(100),返回值存放在c中,现在问题来了,参数a,b是怎么传进来的,函数返回值是怎么返回的呢?在Visual Studio中我们使用cl -c -FA x.c(假设代码文件为x.c)命令来汇编以上代码,输出的即是以上代码对应的汇编信息:

; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.40629.0 

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

        从以上汇编代码中可以看出,调用函数时,默认情况下参数从右到左入栈,同时,函数返回值存放在eax中,我们可以使用以下代码测试:(在VS上输出110)

int func(int x, int y){

        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);

}

        以上汇编代码和实际测试代码表明在Win32上,函数调用是使用eax作返回值的,现在问题来了,如果需要返回的对象特别大,超过4个字节(eax大小为4字节,rax为8),那怎么处理呢?在这种情况下,主调函数会在堆栈上多分配一块内存,然后被调函数将返回值存放在这块内存中,最后返回这块内存的首地址---放在eax中。我们使用以下代码作测试:

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);

}

将文件存为x.c,然后使用cl -c -FA x.c汇编代码,将会生成x.asm,打开之后内容如下:

; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.40629.0 

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

        在x.asm中,有三块被标红,第一块是一条栈分配指令sub esp,这条指令用于存放两个record结构体,可能有人要问,我明明在main里面只定义了一个Record对象,为什么main函数要在栈上分配这么大一块内存,其实之前也已经说明了,在函数调用过程中,eax往往用来保存函数返回值,但是,如果返回值过大,主调函数就会在进入被调函数之前,在自己的栈上挖出一块内存,被调函数将待返回的对象写入这块地址,然后使用eax返回这个块内存的地址。在x.asm中,最后一块标红的汇编代码就是被调函数(func)将待返回的对象写入main所在的栈上预分配好的一块内存(ecx是循环计数器,101就是表明要复制101个int,而Record刚好就是这么大)。第二块标红的汇编代码就是将func返回的对象首地址的内容复制到r。为了验证以上猜想,我们可以使用以下代码验证:(代码毫无疑问肯定输出100)

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),先以以下代码为例子:

class CK{

public:

        CK(){

                m_iVal = 10;

        }

public:

        void Show(){

                m_iVal += 10;

        }

private:

        int m_iVal;

};

int main(){

        CK obj;

        obj.Show();

}

使用cl -FA -c x.cpp汇编以上代码,得到x.asm,内容如下:

; Listing generated by Microsoft (R) Optimizing Compiler Version 18.00.40629.0 

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

        以上代码中有三块标红,第一块是调用CK的构造,第二块是调用Show,第三块是Show的部分汇编代码,在调用Show之前,可以看到一个lea
 ecx, DWORD PTR _obj$[ebp],lea是X86中的取址指令,这条指令的意思是取对象的地址(this指针),在Show的汇编代码中,把ecx取出来,再基于ecx取对象的m_iVal值,以上一些汇编代码给人一种类的成员函数调用与普通成员函数调用完全一样,只是使用ecx传递this指针的感觉,事实上这种感觉是对的。我们使用以下代码去验证:

#include <stdio.h>

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方式,会自己解决参数造成的堆栈不平衡

        }

}

3,构造自己的线程类

        线程是操作系统中重要的概念,是操作系统中可以异步执行的执行体,在Win32中,如果要创建一个线程,我们需要使用CreateThread或者__beginthreadex等API,这些API有个特点,即需要传递一个非类成员函数(静态函数或普通C函数),在公司的AngelicaES引擎中,线程的创建使用的就是全局静态函数,然而,在Java中,创建线程只需要一个Thread对象或一个Thread对象加一个实现了Runable的接口,在Java中,使用线程,往往构造一个Thread对象,然后调用Start函数,线程就起来了,并没有我们看到的全局函数或者静态成员函数,那么Java等高级语言是怎么实现的呢?在前两节中,已经讲清楚了普通函数调用和类的成员函数调用的过程与原理,事实上,只需要使用这个原理就可以实现一个类似Java的线程函数(不使用任何全局函数或static函数),如果想将一个类的成员函数作为线程函数来执行,在语法层次我们无法逃脱this指针的束缚。但是,事实上,我们可以编写一段奇怪的二进制指令(可直接执行的机器码),在字节码里面设置好this指针等信息并跳转到类的成员函数中去,最后将这段字节码作为线程函数去执行(强制转换成CreateThread需要的线程函数类型),但是,我们并没有使用使用全局函数或static函数,先看以下一段字节码:

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 

};

         在以上一段字节码中,我们会设置好this指针和真正的线程函数地址,然后使用call跳转过去,所有的代码如下所示:

//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:

        virtual void run(void* lpParameter){

                cout<<"Hello,World!"<<endl;

        }

};

int main(){

        ZXThread boss(new ZXRun);

        boss.Start();

        boss.Wait();

}

        mark:直接运行以上程序会崩溃,因为DEP,解决方法有两种:1)在编译器里面关掉DEP;2)最靠谱的作法是使用VirtualProtect将ZXThread中的m_thread_proc对应的内存块设置为可执行即可。(缓冲区溢出攻击经常使用m_thread_proc字节码的手段)。

4,总结

        上面实际给出了解决一类难题的思路,即如果某个地方需要一个全局的函数(或类的static函数)---Thunk技术,而我们想要完全面向对象(即我们不想使用全局函数或非static类函数),解决方法就是使用机器码,在机器码内完全跳转(Thunk技术---跟Knuth有点像,以前看过国内一位大牛仅使用4个字节就实现封装Windows窗口消息函数的代码,而只要百度那4个字节,就可以搜索出那位牛人,貌似是金山的一位大牛,佩服)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: