0x01 函数的识别
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | OD |
实验过程
如图所示,call指令将call指令的下一条指令地址压入栈中并且jmp到了00401010
随后retn会将执行结果同pop eip
0x02 函数的参数
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
如图所示,call指令将call指令的下一条指令地址压入栈中并且jmp到了00401010
随后retn会将执行结果同pop eip
fastcall调用约定,最左边的两个参数分别用ecx,edx传递了
thiscall调用约定,用ecx传递了this指针。
0x03 局部变量
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
较为容易看明白,首先将一个静态or全局变量(单看汇编无从辨认,都是固定地址)赋值给edi,esp+eax+4很明显是一个基址+偏移的形式但是这个基址准确的指向数组的时候是eax>=4的时候,所以add eax,4在其前面,随后就是一个循环三次。其赋值给的新数组分别存了原数组的第一个数,第一第二个数之和,第1-3个数之和。
0x02 函数的参数
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
如图所示,call指令将call指令的下一条指令地址压入栈中并且jmp到了00401010
随后retn会将执行结果同pop eip
fastcall调用约定,最左边的两个参数分别用ecx,edx传递了
thiscall调用约定,用ecx传递了this指针。
0x04 虚表1
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
C代码
#include "stdafx.h"
class CVirtual {
public:
CVirtual() {
m_nMember1 = 1;
m_nMember2 = 2;
printf("CVirtual()\r\n");
}
virtual ~CVirtual() {
printf("~CVirtual()\r\n");
}
virtual void fun1() {
printf("fun1()\r\n");
}
virtual void fun2() {
printf("fun2()\r\n");
}
private:
int m_nMember1;
int m_nMember2;
};
int main(int argc, char* argv[]) {
CVirtual object;
object.fun1();
object.fun2();
return 0;
}
rcx=this随后进入构造函数。
构造函数内部。
虚表指向的地方的三个虚函数。
地址对上了。
这里有两个析构函数,并且我们调用的析构函数同样lea reg,this;mov [reg],reg初始化了虚表,返回值为this指针。
区分析构函数和构造函数可以看先后顺序。虚表中的函数时按照函数声明的顺序依次放入到,有时候不一定相同(虚函数重载)。虚表的每一项都是8字节,存储的成员函数地址。虚表的最后一项不一定以0结尾,虚表项的个数会根据其他信息决定。
虚表里的析构函数对比我们调用的析构函数多了一个delete操作。所以我们调用的时出作用域时调用的析构函数,虚表的时delete的时候调用的。delete对象的时候,先调用析构函数,在释放堆空间。
0x05 虚表2
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
C代码
#include "stdafx.h"
class CBase {
public:
CBase() {
m_nMember = 1;
printf("CBase()\r\n");
}
virtual ~CBase() {
printf("~CBase()\r\n");
}
virtual void fun1() {
printf("CBase::fun1()\r\n");
}
private:
int m_nMember;
};
class CDerived :public CBase {
public:
CDerived() {
m_nMember = 2;
printf("CDerived()\r\n");
}
~CDerived() {
printf("~CDerived()\r\n");
}
virtual void fun1() {
printf("CDerived::fun1()\r\n");
}
virtual void fun2() {
printf("CDerived::fun2()\r\n");
}
private:
int m_nMember;
};
int _tmain(int argc, _TCHAR* argv[]) {
CBase *pBase = new CDerived();
pBase->fun1();
delete pBase;
return 0;
}
这里new申请的空间为24字节是因为虚表需要8字节空间,而变量需要8字节对齐。
构造函数先调用CBase的构造函数会初始化一次虚表CBase的虚表,后续又会初始化一次CDrived的虚表。构造函数中CBase给成员变量赋值1,CDrived赋值2。所以最后的值为2。
随后调用析构函数,这里传入1,表示析构函数要delete,进入虚表的虚构函数发现delete那里检测了这个传入的1,不是1就不delete。
虚表中的析构函数
先调用CDerived析构函数,CDerived析构函数内部结束的时候会调用CBase的析构函数。
0x05 虚表3
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
C代码
#include "stdafx.h"
class CBase1 {
public:
CBase1() {
m_nMember = 1;
printf("CBase1()\r\n");
}
~CBase1() {
printf("~CBase1()\r\n");
}
virtual void fun1() {
printf("CBase1::fun1()\r\n");
}
private:
int m_nMember;
};
class CBase2 {
public:
CBase2() {
m_nMember = 2;
printf("CBase2()\r\n");
}
~CBase2() {
printf("~CBase2()\r\n");
}
virtual void fun2() {
printf("CBase2::fun1()\r\n");
}
private:
int m_nMember;
};
class CDerived :public CBase1, public CBase2 {
public:
CDerived() {
m_nMember = 2;
printf("CDerived()\r\n");
}
~CDerived() {
printf("~CDerived()\r\n");
}
virtual void fun1() {
printf("CDerived::fun1()\r\n");
}
virtual void fun3() {
printf("CDerived::fun3()\r\n");
}
private:
int m_nMember;
};
int _tmain(int argc, _TCHAR* argv[]) {
CDerived derievd;
return 0;
}
构造函数最先调用的CBase1构造函数
调用完CBase1后 this+10h进行第二个构造函数CBase2调用
随后分别将两个虚表放在了首地址,首地址+10h处。
fun1 fun3派生类实现了虚函数,所以覆盖,fun2没实现,照搬。
0x06 虚表4
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
C代码
#include "stdafx.h"
class A {
public:
A() {
m_nMember = 1;
printf("A()\r\n");
}
~A() {
printf("~A()\r\n");
}
virtual void fun1() {
printf("A::fun1()\r\n");
}
private:
int m_nMember;
};
class B :virtual public A{
public:
B() {
m_nMember = 2;
printf("B()\r\n");
}
~B() {
printf("~B()\r\n");
}
virtual void fun2() {
printf("B::fun2()\r\n");
}
private:
int m_nMember;
};
class C :virtual public A{
public:
C() {
m_nMember = 3;
printf("C()\r\n");
}
~C() {
printf("~C()\r\n");
}
virtual void fun3() {
printf("C::fun3()\r\n");
}
private:
int m_nMember;
};
class BC :public B, public C {
public:
BC() {
m_nMember = 4;
printf("BC()\r\n");
}
~BC() {
printf("~BC()\r\n");
}
virtual void fun3() {
printf("BC::fun3()\r\n");
}
virtual void fun4() {
printf("BC::fun4()\r\n");
}
private:
int m_nMember;
};
int _tmain(int argc, _TCHAR* argv[]) {
BC theBC;
return 0;
}
多了一个参数表示是否调用基类构造函数
设置了b和c的虚基类偏移表指针,随后调用A的构造函数
调用完A的构造函数调用b的
C的过程和B差不多。
最后调用BC的构造函数
虚表
这里main首先调用了BC的构造函数,但是其要调用B和C的构造函数,都会调用A的构造函数,所以多传递一个参数,用来表示是否进行虚基类的构造,这里在开头传递1先构造了虚基类A。
初始化虚基类偏移表的操作时为了定位虚基类在对象内存中的位置,虚基类偏移表也在全局数据区。虚基类偏移表8字节,后四字节用于表示虚基类在当前虚基类偏移表中的偏移。
0x07 虚表5
实验环境
推荐使用的环境 | 备注 | |
---|---|---|
操作系统 | Windows 10 | |
编译器 | ||
编译选项 | ||
build版本 | ||
调试器 | IDA |
实验过程
C代码
class IBase {
public:
IBase() {
m_nMember = 1;
printf("IBase()\r\n");
}
virtual void fun1() = 0;
virtual void fun2() = 0;
private:
int m_nMember;
};
class CDerived :public IBase
{
public:
CDerived()
{
printf("CDerived()\r\n");
}
virtual void fun1(){};
virtual void fun2() {};
};
int _tmain(int argc, _TCHAR* argv[]) {
IBase *pBase = new CDerived();
delete pBase;
return 0;
}
抽象类与单重继承没什么区别,抽象类的虚表为两个purecall
purecall的功能是显示一个错误信息并退出程序。
文章评论