• C++函数调用的反汇编过程及Thunk应用


    x86汇编基础知识

    1. 汇编常用寄存器

    1. esp,(Extended stack pointer)栈顶指针。因为x86的栈内存是向下扩展的,因此当push入栈时,esp–。pop出栈时,esp++。esp主要维护当前栈。
    2. ebp,(Extended Base Pointer)栈基地址。一般都是在函数入口时,保存前函数的ebp,并将esp赋值给ebp,然后通过ebp来操作形参和临时参数。
    3. eax,(Extended Accumulator)累加器寄存器,加法乘法指令的缺省寄存器。函数的返回值一般也会存在eax。
    4. ebx,(Extended Base)基址寄存器,在内存寻址时存放基地址。
    5. ecx,(Extended Counter)计数器寄存器,配合rep/loop指令,主要用来表示循环次数。C++中,this指针会存在ecx中。
    6. edx,存入除法的余数。
    7. esi/edi,(source/destination index)源/目标索引寄存器,因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串。
    8. eip,CPU每次执行指令都要先读取EIP寄存器的值,然后定位EIP指向的内存地址(偏移地址),并且读取汇编指令,最后执行。其实就是当前的汇编内存地址。

    2. 汇编常用指令基础

    1. push指令,压栈。
    2. pop指令,出栈。
    3. mov指令,将源数(可以是立即数,也可以是寄存器或内存单元),拷贝到目标指定位置。
    4. lea指令,(Load Effective Address)将源数(可以是表达式)的地址拷贝到指定寄存器。lea edi,[ebp];因为取值符[],这样lea edi,[ebp]和mov edi,ebp是等价的。不同点是,lea的源数可以是表达式完成加减法,相较于mov不能,所以在源数是表达式时,lea更有效率些。lea必须以寄存器为操作目标数。
    5. call指令,保存当前指令的下一条指令,然后跳转到指定代码处。call指令是相对寻址,call的机器码003BDC8B E8 69 3F FF FF。003BDC8B是当前指令地址,E8表示call,0xFFFF3F69是相对地址(小端),那么目标地址=当前地址(003BDC8B)+相对地址(0xFFFF3F69) +5(call指令所占的机器码数)。
    6. jmp指令,跳转到指定代码处,也是相对寻址。目标地址的计算和call是一样的。jmp与call相同的地方是都会跳转到指定代码处,不同点是call在代码调用完之后会退回到保存在eip中的代码位置,而jmp即不会。
    7. ret指令,将栈顶的值pop到eip寄存器,然后就执行eip指向的位置,也就是call时保存的下一条指令地址。ret默认是pop一个地址,即4个字节。但是ret X,此处的X是额外pop的内存,可以用来恢复栈顶esp。
    8. rep指令,重复ecx中记录的数值这么多次rep后面指定的指令。
    9. stos指令,将寄存器eax的值保存到目标地址。目标地址es:[edi],es是段选择符,edi是段内偏移地址,它们两个组成一个目标地址。
    10. add指令,加法指令,将源数与目标数相加并存在目标数中。
    11. sub指令,减法指令,将目标数减去源数结果存放在目标数中。

    函数调用的反汇编过程

    C/C++代码

    测试代码

    #include <stdio.h>
    #include <tchar.h>
    
    int Add(int a, int b)
    {
        int sum = 0;
        sum = a + b;
        return sum;
    }
    
    class CTest
     {
     public:
         int Add(int a, int b)
         {
             int sum = 0;
             sum = a + b;
             return sum;
         }
     };
    
     class CCalculator
     {
     public:
         CCalculator(int nVal)
         {
             m_nValue = nVal;
         }
         int Add(int a, int b)
         {
             int sum = 0;
             sum = a + b + m_nValue;
             return sum;
         }
    
     private:
         int m_nValue;
     };
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        CTest test;  
        int sum = test.Add(4, 3);
    
        CCalculator calc(4);
        sum = calc.Add(2, 3);
    
        sum = Add(1, 3);
    
        return 0;
    }

    C风格函数反汇编

    1. 调用函数反汇编

      int sum = Add(1, 3);
    011E17DE 6A 03                push        3                     // 参数压栈,从右往左
    011E17E0 6A 01                push        1                     // 第2个压栈参数
    011E17E2 E8 C7 F9 FF FF       call        Add (11E11AEh)        // 函数调用指令,目标地址(11E11AEh)=11E17E2+FFFFF9C7+5
    011E17E7 83 C4 08             add         esp,8                 // 因为VS默认是__cdel调用方式,即调用者恢复栈,两次push栈减8,所以加8恢复栈
    011E17EA 89 45 F8             mov         dword ptr [sum],eax   // 从eax中取出Add函数的返回值

    2. call Add跳转到的代码

    011E11AE E9 CD 05 00 00       jmp         Add (11E1780h)       // call指令保存eip并跳转到此处,此处跳转到真正的子函数Add

    3. 被调用函数Add反汇编

    int Add(int a, int b)
    {
    011E1780 55                   push        ebp                   // ebp压栈,为了最后ebp恢复
    011E1781 8B EC                mov         ebp,esp               // esp赋给ebp,后面通过ebp操作参数及临时变    
    011E1783 81 EC CC 00 00 00    sub         esp,0CCh              // 栈地址减,扩大栈内存
    011E1789 53                   push        ebx                   // 压栈,保存ebx                  
    011E178A 56                   push        esi                   // 压栈,保存esi  
    011E178B 57                   push        edi                   // 压栈,保存edi  
    011E178C 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]        // 取出前面扩展栈内存
    011E1792 B9 33 00 00 00       mov         ecx,33h               // 确定下面rep的循环次数
    011E1797 B8 CC CC CC CC       mov         eax,0CCCCCCCCh        // 4字节对齐,VS调试下将内存格式为0xCCCCCCCC便于确定变量没有初始化。eax为stos的源.
    011E179C F3 AB                rep stos    dword ptr es:[edi]    // 将eax循环填充到指定内存
        int sum = 0;
    011E179E C7 45 F8 00 00 00 00 mov         dword ptr [sum],0     // 赋值初始化,[]取地址,dowrd ptr4字节取值。
        sum = a + b;
    011E17A5 8B 45 08             mov         eax,dword ptr [a]     // 将a赋值给eax,便于加法器
    011E17A8 03 45 0C             add         eax,dword ptr [b]     // 加法器,结果存放在eax
    011E17AB 89 45 F8             mov         dword ptr [sum],eax   // 将eax赋值给sum
        return sum;
    011E17AE 8B 45 F8             mov         eax,dword ptr [sum]   // 将返回值存放在eax中。
    }
    011E17B1 5F                   pop         edi                   // 出栈,恢复edi
    011E17B2 5E                   pop         esi                   // 出栈,恢复esi
    011E17B3 5B                   pop         ebx                   // 出栈,恢复ebx
    011E17B4 8B E5                mov         esp,ebp               // 从ebp中取出保存的esp
    011E17B6 5D                   pop         ebp                   // 出栈,恢复ebp    
    011E17B7 C3                   ret                               // 出栈call时保存的eip,跳转到eip指定位置,也即返回call的下一条指令

    C++类函数调用反汇编

    1. 调用函数反汇编

     CTest test;
        sum = test.Add(4, 3);
    00A0DA0A 6A 03                push        3  // 从右至左的形参1入栈
    00A0DA0C 6A 04                push        4  // 形参2入栈
    00A0DA0E 8D 4D EF             lea         ecx,[ebp-11h]/[test]  // 前面是标准汇编代码,/后面是符号代码可以看出临时变量test是分配在ebp-11h上的。类成员函数会先将类对象存放在ecx中。
    00A0DA11 E8 1A 42 FF FF       call        00A01C30/CCalculator::CCalculator // 函数调用,同普通函数  
    00A0DA16 89 45 F8             mov         dword ptr [ebp-8],eax/dword ptr [sum],eax  // 从eax中取出返回值
    
        CCalculator calc(4);
    00A0DA19 6A 04                push        4  // 构造函数的形参入栈
    00A0DA1B 8D 4D E0             lea         ecx,[ebp-20h]/[calc]  // 临时变量calc在ebp-20h位置,存入ecx。所有类成员函数都是__thisCall调用风格,都会将函数对象指针存放在ecx。
    00A0DA1E E8 08 42 FF FF       call        00B61C2B/CCalculator::CCalculator // 函数调用 
        sum = calc.Add(2, 3);
    00A0DA23 6A 03                push        3  // 形参1入栈
    00A0DA25 6A 02                push        2  // 形参2入栈
    00A0DA27 8D 4D E0             lea         ecx,[ebp-20h]/ecx,[calc]  // 函数对象指针存入ecx  
    00A0DA2A E8 F2 41 FF FF       call        0A01C21h/CCalculator::Add // 函数调用
    00A0DA2F 89 45 F8             mov         dword ptr [ebp-14h],eax/dword ptr [sum],eax  // 取出返回值

    2. call Add跳转到的代码

    00B61C21 E9 3A C3 00 00       jmp         0B6DF60h/CCalculator::Add         // call指令保存eip并跳转到此处,此处跳转到真正的子函数Add

    3. 被调用函数Add反汇编

        int Add(int a, int b)
         {
    00B6DF60 55                   push        ebp           // ebp压栈,为了最后ebp恢复
    00B6DF61 8B EC                mov         ebp,esp       // esp赋给ebp,后面通过ebp操作参数及临时变
    00B6DF63 81 EC D8 00 00 00    sub         esp,0D8h      // 栈地址减,扩大准备的栈内存
    00B6DF69 53                   push        ebx           // 压栈,保存ebx 
    00B6DF6A 56                   push        esi           // 压栈,保存esi
    00B6DF6B 57                   push        edi           // 压栈,保存edi 
    00B6DF6C 51                   push        ecx           // 压栈,相当于保存this指针
    00B6DF6D 8D BD 28 FF FF FF    lea         edi,[ebp-0D8h]  // 取出扩展栈内存指针存入edi以备后用 
    00B6DF73 B9 36 00 00 00       mov         ecx,36h         // 确定下面rep的循环次数
    00B6DF78 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  // 准备下面存储在内存上的值,Debug下用
    00B6DF7D F3 AB                rep stos    dword ptr es:[edi]  // 循环ecx次,用eax写入指定内存
    00B6DF7F 59                   pop         ecx                 // 还原ecx即取得this指针
    00B6DF80 89 4D F8             mov         dword ptr [ebp-8],ecx  // 将this指针存放在ebp-8位置
             int sum = 0;
    00B6DF83 C7 45 EC 00 00 00 00 mov         dword ptr [ebp-14h]/[sum],0 // 将临时变量summ赋初值0
             sum = a + b + m_nValue;
    00BBDF8A 8B 45 08             mov         eax,dword ptr [ebp+8]/[a]      // ebp+8处存放a  
    00BBDF8D 03 45 0C             add         eax,dword ptr [ebp+0Ch]/[b]    // ebp+0Ch处存放b  
    00BBDF90 8B 4D F8             mov         ecx,dword ptr [ebp-8]/[this]   // ebp-8即上面存入的this,而this指向的内存即唯一的成员变量m_nValue的内容,即可以通过this指针偏移需要更多成员变量 
    00BBDF93 03 01                add         eax,dword ptr [ecx]            // 值相加存放eax
    00BBDF95 89 45 EC             mov         dword ptr [ebp-14h]/[sum],eax  // 结果eax处存放sum上  
             return sum;
    00B6DF93 8B 45 EC             mov         eax,dword ptr [ebp-14h]/[sum]  // 返回值存放eax
         }
    00B6DF96 5F                   pop         edi           // 出栈恢复edi                         
    00B6DF97 5E                   pop         esi           // 出栈恢复esi
    00B6DF98 5B                   pop         ebx           // 出栈恢复ebx
    00B6DF99 8B E5                mov         esp,ebp       // 从ebp中取出esp
    00B6DF9B 5D                   pop         ebp           // 出栈恢复ebp
    00B6DF9C C2 08 00             ret         8             // __thisCall实际依然是__stdCall调用风格,即被调用者完成栈内存的恢复,所以额外出栈8字节,即恢复因为两个形参的入栈,再出栈call时保存的eip,完成恢复栈内存,并跳转到eip指定的代码处。

    C风格函数及类成员函数调用的比较

    1. C风格函数的入栈和出栈都是调用者完成;而类成员函数的入栈由调用者完成,出栈即由被调用函数完成。
    2. 类成员函数会通过ecx完成类对象this指针的传递,C函数无此指令。

    Thunk技术的应用

    概念

    Thunk的理解,一个表达式,被它所在的环境所限制,在需要的时候重新计算这个表达式的值。在此处,即类的非静态成员函数想成为回调函数,需要一个转换的过程,可以称为Thunk转换。MFC采用消息宏来完成成员函数的消息响应,用起来还是很方便,但是消息宏的实现还是比较复杂的。而ATL则用了Thunk技术来完成成员函数的消息响应。

    实现方式

    1. 根据C风格函数及类成员函数调用的比较,因为类成员函数一定是__thiscall调用规则,即出入栈管理必须为__stdcall,而且必须在调用函数代码之前将类对象this指针赋给ecx。也就是说类成员函数只可能适用于__stdcall规则的回调函数。

    2. 根据C++类函数调用反汇编,函数调用即对应call指令,如果要在call之前将类对象this指针赋给ecx,则必须在函数调用之前就进行,这样频繁使用时,非常不方便。上面的代码都是通过00A0DA1B 8D4DE0 lea ecx,[ebp-20h]/[calc]8D机器码对应指令lea4D机器码对应寄存器ebpE0即为-20calc对象的this指针即为栈上的ebp-20h。这是用的是通过寄存器短地址寻址。实际确定栈偏移比较麻烦,所以通过长地址直接寻址8D 0D 5C F5 14 00 lea ecx,ds:[0014F55C]0014F55C即为this指针值。然后再jmp到指定函数。

    3. 具体的方法

    1. 函数调用 
      call Fun(xxxxxxxx1)

    2. call跳转代码处
    xxxxxxxx1 8D0D5CF51400 lea ecx,ds:[0014F55C]  // 0014F55C为this的16进制值
    xxxxxxxx6 E9XXXXXXXBX  jmp PFUN               // XXXXXXXB为函数指针PFUN的值的相对地址,即xxxxxxxx6+XXXXXXXB+6=PFUN,5是E9XXXXXXXB的大小。那么xxxxxxx1+5=xxxxxxx6,此处的5为B9XXXXXXXA的大小。即可以得出XXXXXXXB=PFUN-xxxxxxx1-11。xxxxxxx1为自定义的代码段内存的起始地址。

    4. 具体的代码

    template< typename TDst, typename TSrc >
    TDst  UnionTypeCast( TSrc src )
    {
        union
        {
            TDst  uDst;
            TSrc  uSrc;
        }uMedia;
        uMedia.uSrc = src;
        return uMedia.uDst;
    }
    
    typedef  int  (__stdcall *FunStdCall1)(int, int );
    typedef  int  (__stdcall *FunStdCall2)(int);
    class CTest
    {
    public:
        CTest() : m(9)
        {
        }
        int Add(int a)
        {
            return m + a;
        }
        int AddEx(int a, int b)
        {
            return a + b + m;
        }
        int Multiply(int a, int b)
        {
            return a*b*m;
        }
    private:
        int m;
    };
    
    class CThunk
    {
        const static long CODE_SEGMENT_SIZE = 11;
    public:
        CThunk() : m_pCode(NULL)
        {
            // To execute dynamically generated code, use VirtualAlloc to allocate memory 
            m_pCode = (char*)VirtualAlloc(NULL, CODE_SEGMENT_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        }
        ~CThunk()
        {
            VirtualFree(m_pCode, CODE_SEGMENT_SIZE, MEM_DECOMMIT);
        }
        char* GetCallBackFun(DWORD_PTR proc, void* pThis)
        {
            // 8D0DXXXXXXXX lea ecx fun, XXXXXXXX is the address of fun
            *m_pCode = (char)0x8D;              // lea machine opcode
            *(m_pCode+1) = 0x0D;                // register ecx
            *(long*)(m_pCode+2) = reinterpret_cast<ULONG>(pThis);
            *(m_pCode+6) = (char)0xE9;          // short jmp machine opcode
            *(long*)(m_pCode+7) = proc - (DWORD_PTR)m_pCode - CODE_SEGMENT_SIZE;
    
            // When creating a region that will be executable, the calling program bears responsibility for
            //ensuring cache coherency via an appropriate call to FlushInstructionCache once the code has been set in place. 
            //Otherwise attempts to execute code out of the newly executable region may produce unpredictable results.
            FlushInstructionCache(GetCurrentProcess(), m_pCode, MEM_DECOMMIT);
    
            return m_pCode;
        }
    private:
        char* m_pCode;
    };
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        CTest test;
        CThunk thunk;
        FunStdCall1 fun = (FunStdCall1)thunk.GetCallBackFun(UnionCastType<DWORD_PTR>(&CTest::AddEx), &test);
        int sum = fun(3, 4);
        fun = (FunStdCall1)thunk.GetCallBackFun(UnionCastType<DWORD_PTR>(&CTest::Multiply), &test);
        int ret = fun(4, 5);
        FunStdCall2 fun2 = (FunStdCall2)thunk.GetCallBackFun(UnionCastType<DWORD_PTR>(&CTest::Add), &test);
        ret = fun2(4);
    }
  • 相关阅读:
    神奇的条件注解-Spring Boot自动配置的基石
    Spring 注解配置原理
    元注解之@Repeatable
    MyBatis批量操作
    MapperScannerConfigurer源码解析
    Spring包扫描机制详解
    SqlSessionTemplate源码解析
    DataSourceUtils源码分析
    Spring事务源码分析
    多核CPU
  • 原文地址:https://www.cnblogs.com/feihe0755/p/6963986.html
Copyright © 2020-2023  润新知