C++函数调用的反汇编过程及Thunk应用
x86汇编基础知识
1. 汇编常用寄存器
- esp,(Extended stack pointer)栈顶指针。因为x86的栈内存是向下扩展的,因此当push入栈时,esp–。pop出栈时,esp++。esp主要维护当前栈。
- ebp,(Extended Base Pointer)栈基地址。一般都是在函数入口时,保存前函数的ebp,并将esp赋值给ebp,然后通过ebp来操作形参和临时参数。
- eax,(Extended Accumulator)累加器寄存器,加法乘法指令的缺省寄存器。函数的返回值一般也会存在eax。
- ebx,(Extended Base)基址寄存器,在内存寻址时存放基地址。
- ecx,(Extended Counter)计数器寄存器,配合rep/loop指令,主要用来表示循环次数。C++中,this指针会存在ecx中。
- edx,存入除法的余数。
- esi/edi,(source/destination index)源/目标索引寄存器,因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串。
- eip,CPU每次执行指令都要先读取EIP寄存器的值,然后定位EIP指向的内存地址(偏移地址),并且读取汇编指令,最后执行。其实就是当前的汇编内存地址。
2. 汇编常用指令基础
- push指令,压栈。
- pop指令,出栈。
- mov指令,将源数(可以是立即数,也可以是寄存器或内存单元),拷贝到目标指定位置。
- lea指令,(Load Effective Address)将源数(可以是表达式)的地址拷贝到指定寄存器。lea edi,[ebp];因为取值符[],这样lea edi,[ebp]和mov edi,ebp是等价的。不同点是,lea的源数可以是表达式完成加减法,相较于mov不能,所以在源数是表达式时,lea更有效率些。lea必须以寄存器为操作目标数。
- call指令,保存当前指令的下一条指令,然后跳转到指定代码处。call指令是相对寻址,call的机器码003BDC8B E8 69 3F FF FF。003BDC8B是当前指令地址,E8表示call,0xFFFF3F69是相对地址(小端),那么目标地址=当前地址(003BDC8B)+相对地址(0xFFFF3F69) +5(call指令所占的机器码数)。
- jmp指令,跳转到指定代码处,也是相对寻址。目标地址的计算和call是一样的。jmp与call相同的地方是都会跳转到指定代码处,不同点是call在代码调用完之后会退回到保存在eip中的代码位置,而jmp即不会。
- ret指令,将栈顶的值pop到eip寄存器,然后就执行eip指向的位置,也就是call时保存的下一条指令地址。ret默认是pop一个地址,即4个字节。但是ret X,此处的X是额外pop的内存,可以用来恢复栈顶esp。
- rep指令,重复ecx中记录的数值这么多次rep后面指定的指令。
- stos指令,将寄存器eax的值保存到目标地址。目标地址es:[edi],es是段选择符,edi是段内偏移地址,它们两个组成一个目标地址。
- add指令,加法指令,将源数与目标数相加并存在目标数中。
- sub指令,减法指令,将目标数减去源数结果存放在目标数中。
函数调用的反汇编过程
C/C++代码
测试代码
#include <stdio.h>
#include <tchar.h> int Add(int a, int b)
{
int sum = ;
sum = a + b;
return sum;
} class CTest
{
public:
int Add(int a, int b)
{
int sum = ;
sum = a + b;
return sum;
}
}; class CCalculator
{
public:
CCalculator(int nVal)
{
m_nValue = nVal;
}
int Add(int a, int b)
{
int sum = ;
sum = a + b + m_nValue;
return sum;
} private:
int m_nValue;
}; int _tmain(int argc, _TCHAR* argv[])
{
CTest test;
int sum = test.Add(, ); CCalculator calc();
sum = calc.Add(, ); sum = Add(, ); return ;
}
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 push ebp // ebp压栈,为了最后ebp恢复
011E1781 8B EC mov ebp,esp // esp赋给ebp,后面通过ebp操作参数及临时变
011E1783 EC CC sub esp,0CCh // 栈地址减,扩大栈内存
011E1789 push ebx // 压栈,保存ebx
011E178A push esi // 压栈,保存esi
011E178B push edi // 压栈,保存edi
011E178C 8D BD FF FF FF lea edi,[ebp-0CCh] // 取出前面扩展栈内存
011E1792 B9 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 = ;
011E179E C7 F8 mov dword ptr [sum], // 赋值初始化,[]取地址,dowrd ptr4字节取值。
sum = a + b;
011E17A5 8B mov eax,dword ptr [a] // 将a赋值给eax,便于加法器
011E17A8 0C add eax,dword ptr [b] // 加法器,结果存放在eax
011E17AB F8 mov dword ptr [sum],eax // 将eax赋值给sum
return sum;
011E17AE 8B 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(, );
00A0DA0A 6A push // 从右至左的形参1入栈
00A0DA0C 6A push // 形参2入栈
00A0DA0E 8D 4D EF lea ecx,[ebp-11h]/[test] // 前面是标准汇编代码,/后面是符号代码可以看出临时变量test是分配在ebp-11h上的。类成员函数会先将类对象存放在ecx中。
00A0DA11 E8 1A FF FF call 00A01C30/CCalculator::CCalculator // 函数调用,同普通函数
00A0DA16 F8 mov dword ptr [ebp-],eax/dword ptr [sum],eax // 从eax中取出返回值 CCalculator calc();
00A0DA19 6A push // 构造函数的形参入栈
00A0DA1B 8D 4D E0 lea ecx,[ebp-20h]/[calc] // 临时变量calc在ebp-20h位置,存入ecx。所有类成员函数都是__thisCall调用风格,都会将函数对象指针存放在ecx。
00A0DA1E E8 FF FF call 00B61C2B/CCalculator::CCalculator // 函数调用
sum = calc.Add(, );
00A0DA23 6A push // 形参1入栈
00A0DA25 6A push // 形参2入栈
00A0DA27 8D 4D E0 lea ecx,[ebp-20h]/ecx,[calc] // 函数对象指针存入ecx
00A0DA2A E8 F2 FF FF call 0A01C21h/CCalculator::Add // 函数调用
00A0DA2F 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 push ebp // ebp压栈,为了最后ebp恢复
00B6DF61 8B EC mov ebp,esp // esp赋给ebp,后面通过ebp操作参数及临时变
00B6DF63 EC D8 sub esp,0D8h // 栈地址减,扩大准备的栈内存
00B6DF69 push ebx // 压栈,保存ebx
00B6DF6A push esi // 压栈,保存esi
00B6DF6B push edi // 压栈,保存edi
00B6DF6C push ecx // 压栈,相当于保存this指针
00B6DF6D 8D BD FF FF FF lea edi,[ebp-0D8h] // 取出扩展栈内存指针存入edi以备后用
00B6DF73 B9 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 pop ecx // 还原ecx即取得this指针
00B6DF80 4D F8 mov dword ptr [ebp-],ecx // 将this指针存放在ebp-8位置
int sum = ;
00B6DF83 C7 EC mov dword ptr [ebp-14h]/[sum], // 将临时变量summ赋初值0
sum = a + b + m_nValue;
00BBDF8A 8B mov eax,dword ptr [ebp+]/[a] // ebp+8处存放a
00BBDF8D 0C add eax,dword ptr [ebp+0Ch]/[b] // ebp+0Ch处存放b
00BBDF90 8B 4D F8 mov ecx,dword ptr [ebp-]/[this] // ebp-8即上面存入的this,而this指向的内存即唯一的成员变量m_nValue的内容,即可以通过this指针偏移需要更多成员变量
00BBDF93 add eax,dword ptr [ecx] // 值相加存放eax
00BBDF95 EC mov dword ptr [ebp-14h]/[sum],eax // 结果eax处存放sum上
return sum;
00B6DF93 8B 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 ret // __thisCall实际依然是__stdCall调用风格,即被调用者完成栈内存的恢复,所以额外出栈8字节,即恢复因为两个形参的入栈,再出栈call时保存的eip,完成恢复栈内存,并跳转到eip指定的代码处。
C风格函数及类成员函数调用的比较
- C风格函数的入栈和出栈都是调用者完成;而类成员函数的入栈由调用者完成,出栈即由被调用函数完成。
- 类成员函数会通过
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
机器码对应指令lea
,4D
机器码对应寄存器ebp
,E0
即为-20
。calc
对象的this
指针即为栈上的ebp-20h
。这是用的是通过寄存器短地址寻址。实际确定栈偏移比较麻烦,所以通过长地址直接寻址8D 0D 5C F5 14 00 lea ecx,ds:[0014F55C]
,0014F55C
即为this
指针值。然后再jmp
到指定函数。
3. 具体的方法
- 函数调用
call Fun(xxxxxxxx1)
- 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()
{
}
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 = ;
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+) = 0x0D; // register ecx
*(long*)(m_pCode+) = reinterpret_cast<ULONG>(pThis);
*(m_pCode+) = (char)0xE9; // short jmp machine opcode
*(long*)(m_pCode+) = 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(, );
fun = (FunStdCall1)thunk.GetCallBackFun(UnionCastType<DWORD_PTR>(&CTest::Multiply), &test);
int ret = fun(, );
FunStdCall2 fun2 = (FunStdCall2)thunk.GetCallBackFun(UnionCastType<DWORD_PTR>(&CTest::Add), &test);
ret = fun2();
}
C++函数调用的反汇编过程及Thunk应用的更多相关文章
- 一个c程序反汇编过程(zz)
zz from http://blog.luoyuanhang.com/ 最基本的反汇编方法是gdb xxx: disassemble main/其他函数 #反汇编一个简单的C程序并分析 C 源码: ...
- C++反汇编代码分析–函数调用
转载:http://shitouer.cn/2010/06/method-called/ 代码如下:#include “stdlib.h” int sum(int a,int b,int m,int ...
- C++反汇编代码分析--函数调用
推荐阅读: C++反汇编代码分析–函数调用 C++反汇编代码分析–循环结构 C++反汇编代码分析–偷调函数 走进内存,走进汇编指令来看C/C++指针 代码如下: #include "stdl ...
- 深入理解 C 语言的函数调用过程
来源: wjlkoorey 链接:http://blog.chinaunix.net/uid-23069658-id-3981406.html 本文主要从进程栈空间的层面复习一下C语言中函数调用的具体 ...
- 深入理解C语言的函数调用过程
本文主要从进程栈空间的层面复习一下C语言中函数调用的具体过程,以加深对一些基础知识的理解. 先看一个最简单的程序: 点击(此处)折叠或打开 /*test.c*/ #include stdio. ...
- 深入理解C语言的函数调用过程 【转】
转自:http://blog.chinaunix.net/uid-25909619-id-4240084.html 原文地址:深入理解C语言的函数调用过程 作者:wjlkoorey258 本文 ...
- [汇编与C语言关系]1.函数调用
对于以下程序: int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); ...
- C语言的本质(31)——C语言与汇编之函数调用的本质
我们一段代码来研究函数调用的过程.首先我们写一段简单的小程序: int sum(int c, int d) { inte = c + d; returne; } int func(int a, int ...
- 从汇编角度来理解linux下多层函数调用堆栈执行状态
注:在linux下开发经常使用的辅助小工具: readelf .hexdump.od.objdump.nm.telnet.nc 等,详细能够man一下. 我们用以下的C代码来研究函数调用的过程. C ...
随机推荐
- git-ftp 用git管理ftp空间
ftp管理不能实现版本控制,而且多电脑工作时,同步很成问题. git-ftp可以完美的解决问题 下面是我的趟坑之路,本机的环境是win10,首先你的机器得装有git. git-ftp的地址https: ...
- AJAX做增删改查详细!
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- 好久没发贴了,最近捣鼓了个基于node的图片压缩小网站解析。
看了下,距离上次发帖都是去年10月份的事,忙于工作的我很少跑博客园里面来玩了. 做这个小网站的初衷是 https://tinypng.com/ 这个网站有时候访问很慢,然后自己去研究了下图片压缩. 网 ...
- DFB系列 之 Clear清空surface缓存
1. 函数原型解析 函数声明: DFBResult Clear ( IDirectFBSurface * thiz, u8 r, u8 g, ...
- margin重叠
margin重叠也就是我们常说的CSS 外边距合并,W3C给出如下定义: 外边距合并指的是,当两个垂直外边距相遇时,它们将形成一个外边距. 合并后的外边距的高度等于两个发生合并的外边距的高度中的较大者 ...
- 蓝桥杯-逆波兰表达式-java
/* (程序头部注释开始) * 程序的版权和版本声明部分 * Copyright (c) 2016, 广州科技贸易职业学院信息工程系学生 * All rights reserved. * 文件名称: ...
- linux 内核的各种futex
futex 设计成用户空间快速锁操作,由用户空间实现fastpath,以及内核提供锁竞争排队仲裁服务,由用户空间使用futex系统调用来实现slowpath.futex系统调用提供了三种配对的调用接口 ...
- MySQL游标的简单实践
Q:为什么要使用游标? A: 在存储过程(或函数)中,如果某条select语句返回的结果集中只有1行,可以使用select into语句(上几篇博客有介绍到用法)来得到该行进行处理:如果结果集中有多行 ...
- 设计模式--MVC(C++版)
MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式.这种模式用于应用程序的分层开发. Model(模型)-是应用程序中用于处理应用程序数据逻辑的部分.通常模型对象 ...
- JavaScript知识点整理(一)
JavaScript知识点(一)包括 数据类型.表达式和运算符.语句.对象.数组. 一.数据类型 1) js中6种数据类型:弱类型特性 5种原始类型:number(数字).string(字符串).bo ...