从逆向分析角度看C++虚函数

谈到虚函数,我想很多朋友都应该知道虚函数表指针VPTR和虚函数表VTABLE,如果不清楚的朋友,建议先看看侯捷先生翻译的《深度探索C++对象模型》:)

刚开始的时候,我仅仅知道虚函数的多态机制是通过VPTR和VTABLE操控的,完全地相信书上所描述的,并没有亲自去证明过,或许是因为那时候我还没有接触到逆向分析吧:)

 

提几个问题:

1.      VPTR的大小如何确定?

2.      VPTR在类实例中的偏移值是多少?

3.      VPTR是如何索引到需要调用的函数的?

4.      VTABLE中函数的顺序是如何确定的?

 

了解上述问题的朋友,可以关闭这个窗口了:)


考虑如下代码:

#include <cstdio>

class Base
{
public:
	int i;
	char c;
	double d;

	Base()
	{
		i  = 4;
		c  = 'A';
		d  = 2.0;
	}
	
	virtual void Virtual_Func_A()
	{
		printf("Virtual_Func_A()\n");
	}
	virtual void Virtual_Func_B()
	{
		printf("Virtual_Func_B()\n");
	}
};

int main(void)
{
	Base* b = new Base;
	b->Virtual_Func_A();
	b->Virtual_Func_A();

	return 0;
}

分析工具:VC 6.0

分析过程:

在解答这个问题前,读者有必要了解一下“数据对齐”的概念,如果不了解的读者,可以参考下:)

http://blog.csdn.net/yeweiouyang/article/details/8636458


注意,Inter cpu 采用的是小端法

VPTR对程序员来说是隐形的,在Win32平台下,不考虑数据对齐的情况,VPTR占用4个字节,用于保存VTABLE的地址

 考虑如下程序:

#include <cstdio>

class Base
{
public:
	virtual void Common_Func()
	{
		printf("Base::Common_Func()\n");
	}
	virtual void Base_Func()
	{
		printf("Base::Func()\n");
	}
};

class Derived : public Base
{
public:
	virtual void Common_Func()
	{
		printf("Derived::Common_Func()\n");
	}
	virtual void Derived_Func()
	{
		printf("Derived::Func()\n");
	}
};

int main(void)
{
	Base* b = new Base;
	b->Common_Func();
	b->Base_Func();

	Derived* d = new Derived;
	d->Common_Func();
	d->Base_Func();
	d->Derived_Func();

	return 0;
}

分析工具:IDA Pro

静态反汇编:

.text:00401000    push    esi
.text:00401001    push    4               ; 为 Base::VPTR 申请 4 个字节的栈空间
.text:00401003    call    ??2@YAPAXI@Z    ; operator new(uint)
.text:00401008    add     esp, 4
.text:0040100B    test    eax, eax
.text:0040100D    jz      short loc_401019
.text:0040100F    mov     dword ptr [eax], offset Base_VPTR ; *b 的堆空间放入 Base::VPTR
.text:00401015    mov     esi, eax
.text:00401017    jmp     short loc_40101B

对应的offset Base_VPTR:

.rdata:004060BC Base_VPTR       dd offset Base_Common_Func ; DATA XREF: _main+Fo
.rdata:004060C0                 dd offset Base_Func
.rdata:004060C4                 align 8

不难发现Base_VPTR正是VTABLE的首地址,即Base::VPTR所指向之处

由这一句:

.text:0040100F    mov    dword ptr [eax], offset Base_VPTR ;

不难发现,VPTR存放的位置是Base类实例b的开始处,即偏移值为0


.text:00401019
.text:00401019 loc_401019:                              ; CODE XREF: _main+Dj
.text:00401019    xor     esi, esi
.text:0040101B
.text:0040101B loc_40101B:                              ; CODE XREF: _main+17j
.text:0040101B    mov     eax, [esi]                    ; eax = Base::VTABLE
.text:0040101D    mov     ecx, esi
.text:0040101F    call    dword ptr [eax]               ; call Base::Common_Func()
.text:00401021    mov     edx, [esi]                    ; edx = Base::VTABLE
.text:00401023    mov     ecx, esi
.text:00401025    call    dword ptr [edx+4]             ; call Base::Base_Func()
.text:00401028    push    4                             ; 为 Derived::VPTR 申请 4 个字节的栈空间
.text:0040102A    call    ??2@YAPAXI@Z                  ; operator new(uint)
.text:0040102F    add     esp, 4
.text:00401032    test    eax, eax
.text:00401034    jz      short loc_401040
.text:00401036    mov     dword ptr [eax], offset Derived_VPTR ; *d 的堆空间放入 Derived_VPTR
.text:0040103C    mov     esi, eax
.text:0040103E    jmp     short loc_401042
.text:00401040 ; ---------------------------------------------------------------------------
.text:00401040
.text:00401040 loc_401040:                              ; CODE XREF: _main+34j
.text:00401040    xor     esi, esi
.text:00401042
.text:00401042 loc_401042:                              ; CODE XREF: _main+3Ej
.text:00401042    mov     eax, [esi]                    ; eax = Derived::VTABLE
.text:00401044    mov     ecx, esi
.text:00401046    call    dword ptr [eax]               ; call Derived::Common_Func()
.text:00401048    mov     edx, [esi]                    ; edx = Derived::VTABLE
.text:0040104A    mov     ecx, esi
.text:0040104C    call    dword ptr [edx+4]             ; call Base_Func(),Base_Func()是从Base类中继承而来的
.text:0040104F    mov     eax, [esi]                    ; eax = Derived::VTABLE
.text:00401051    mov     ecx, esi
.text:00401053    call    dword ptr [eax+8]             ; call Derived::Derived_Func()
.text:00401056    xor     eax, eax
.text:00401058    pop     esi
.text:00401059    retn
.text:00401059 _main           endp

对应的Derived_VPTR:

.rdata:004060B0 Derived_VPTR    dd offset Derived_Common_Func ; DATA XREF: _main+36o
……
(IDA 并没有将所有函数指针显示出来)

经过分析,Base和Derived的VTABLE结构大致如下:


在VTABLE中保存有函数的地址(即函数指针),而非函数本身或函数名,在Win32平台下,函数指针占用4个字节,当通过类实例调用虚函数时,VPTR就会在VTABLE中进行索引,若找到对应的虚函数,就会取出函数指针并进行调用

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yeweiouyang/article/details/9527057
所属专栏: C++逆向分析
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭