从汇编层面深度剖析C++虚函数 .
2013-03-08 16:52
344 查看
摘自 /article/1394261.html
虚函数是C++语言实现运行时多态的唯一手段,因此掌握C++虚函数也成为C++程序员是否合格的试金石。csdn网友所发的一篇博文《VC虚函数布局引发的问题》 从汇编角度分析了对象虚函数表的构,以及C++指针或者引用是如何利用这个表来实现运行时多态。
诚然,C++虚函数的结构会因编译器不同而异,但所使用的原理是一样的。为此,本文使用linux平台下的g++编译器,试图从汇编的层面上分析虚函数表的结构,以及如何利用它来实现运行时多态。
汇编语言是难读的,特别是对一些没有汇编基础的朋友,因此,本文将汇编翻译成相应的C语言,以方便读者分析问题。
1. 代码
为了方便表述问题,本文选取只有虚函数的两个类,当然,还有它的构造函数,如下:
[cpp]
view plaincopyprint?
class Base
{
public:
virtual void f() { }
virtual void g() { }
};
class Derive : public Base
{
public:
virtual void f() {}
};
int main()
{
Derive d;
Base *pb;
pb = &d;
pb->f();
return 0;
}
_ZTV4Base是一个数据符号,它的命名规则是根据g++的内部规则来命名的,如果你想查看它真正表示C++的符号名,可使用c++filt命令来转换,例如:
[lyt@t468 ~]$ c++filt _ZTV4Base
vtable for Base
_ZTV4Base符号(或者变量)可看作为一个数组,它的第一项是0,第二项_ZIT4Base是关于Base的类型信息,这与typeid有关。为方便讨论,我们略去此二项数据。 因此Base类的vtable的结构,翻译成相应的C语言定义如下:
[cpp]
view plaincopyprint?
unsigned long Base_vtable[] = {
&Base::f(),
&Base::g(),
};
相应的C语言定义如下:
[cpp]
view plaincopyprint?
unsigned long Derive_vtable[] = {
&Derive::f(),
&Base::g(),
};
ZN4BaseC1Ev这个符号是C++函数Base::Base() 的内部符号名,可使用c++flit将它还原。C++里的class,可以定义数据成员,函数成员两种。但转化到汇编层面时,每个对象里面真正存放的是数据成员,以及虚函数表。
在上面的Base类中,由于没有数据成员,因此它只有一个vtable指针。故Base类的定义,可以写成如下相应的C代码:
[cpp]
view plaincopyprint?
struct Base {
unsigned long **vtable;
}
同样地,Derive类的构造函数如下:
[cpp]
view plaincopyprint?
struct Derive {
unsigned long **vtable;
};
void Derive::Derive(struct Derive *this)
{
this->vtable = &Derive_vtable;
}
andl $-16, %esp
subl $32, %esp
这两句是为局部变量d和bp在堆栈上分配空间,也即如下的语句:
Derive d;
Base *pb;
leal 24(%esp), %eax
movl %eax, (%esp)
call _ZN6DeriveC1Ev
esp+24是变量d的首地址,先将它压到堆栈上,然后调用d的构造函数,相应翻译成C语言则如下:
Derive::Dervice(&d);
leal 24(%esp), %eax
movl %eax, 28(%esp)
这里其实是将&d的值赋给pb,也即:
pb = &d;
最关键的代码是下面这一段:
[cpp]
view plaincopyprint?
movl 28(%esp), %eax
movl (%eax), %eax
movl (%eax), %edx
movl 28(%esp), %eax
movl %eax, (%esp)
call *%edx
movl 28(%esp), %eax
movl (%eax), %eax
movl (%eax), %edx
movl 28(%esp), %eax
movl %eax, (%esp)
call *%edx
翻译成C语言也就传神的那句:
pb->vtable[0](bp);
编译器会记住f虚函数放在vtable的第0项,这是编译时信息。
5. 小结
这里省略了很多关于编译器和C++的细枝未节,是出于讨论方便用的需要。从上面的编译代码可以看到以下信息:
1.每个类都有各有的vtable结构,编译会正确填写它们的虚函数表
2. 对象在构造函数时,设置vtable值为该类的虚函数表
3.在指针或者引用时调用虚函数,是通过object->vtable加上虚函数的offset来实现的。
当然这仅仅是g++的实现方式,它和VC++的略有不同,但原理是一样的。
虚函数是C++语言实现运行时多态的唯一手段,因此掌握C++虚函数也成为C++程序员是否合格的试金石。csdn网友所发的一篇博文《VC虚函数布局引发的问题》 从汇编角度分析了对象虚函数表的构,以及C++指针或者引用是如何利用这个表来实现运行时多态。
诚然,C++虚函数的结构会因编译器不同而异,但所使用的原理是一样的。为此,本文使用linux平台下的g++编译器,试图从汇编的层面上分析虚函数表的结构,以及如何利用它来实现运行时多态。
汇编语言是难读的,特别是对一些没有汇编基础的朋友,因此,本文将汇编翻译成相应的C语言,以方便读者分析问题。
1. 代码
为了方便表述问题,本文选取只有虚函数的两个类,当然,还有它的构造函数,如下:
[cpp]
view plaincopyprint?
class Base
{
public:
virtual void f() { }
virtual void g() { }
};
class Derive : public Base
{
public:
virtual void f() {}
};
int main()
{
Derive d;
Base *pb;
pb = &d;
pb->f();
return 0;
}
[cpp] view plaincopyprint? _ZTV4Base: .long 0 .long _ZTI4Base .long _ZN4Base1fEv .long _ZN4Base1gEv .weak _ZTS6Derive .section .rodata._ZTS6Derive,"aG",@progbits,_ZTS6Derive,comdat .type _ZTS6Derive, @object .size _ZTS6Derive, 8 _ZTV4Base: .long 0 .long _ZTI4Base .long _ZN4Base1fEv .long _ZN4Base1gEv .weak _ZTS6Derive .section .rodata._ZTS6Derive,"aG",@progbits,_ZTS6Derive,comdat .type _ZTS6Derive, @object .size _ZTS6Derive, 8
_ZTV4Base是一个数据符号,它的命名规则是根据g++的内部规则来命名的,如果你想查看它真正表示C++的符号名,可使用c++filt命令来转换,例如:
[lyt@t468 ~]$ c++filt _ZTV4Base
vtable for Base
_ZTV4Base符号(或者变量)可看作为一个数组,它的第一项是0,第二项_ZIT4Base是关于Base的类型信息,这与typeid有关。为方便讨论,我们略去此二项数据。 因此Base类的vtable的结构,翻译成相应的C语言定义如下:
[cpp]
view plaincopyprint?
unsigned long Base_vtable[] = {
&Base::f(),
&Base::g(),
};
[cpp] view plaincopyprint? _ZTV6Derive: .long 0 .long _ZTI6Derive .long _ZN6Derive1fEv .long _ZN4Base1gEv .weak _ZTV4Base .section .rodata._ZTV4Base,"aG",@progbits,_ZTV4Base,comdat .align 8 .type _ZTV4Base, @object .size _ZTV4Base, 16 _ZTV6Derive: .long 0 .long _ZTI6Derive .long _ZN6Derive1fEv .long _ZN4Base1gEv .weak _ZTV4Base .section .rodata._ZTV4Base,"aG",@progbits,_ZTV4Base,comdat .align 8 .type _ZTV4Base, @object .size _ZTV4Base, 16
相应的C语言定义如下:
[cpp]
view plaincopyprint?
unsigned long Derive_vtable[] = {
&Derive::f(),
&Base::g(),
};
[cpp] view plaincopyprint? _ZN4BaseC1Ev: .LFB6: .cfi_startproc .cfi_personality 0x0,__gxx_personality_v0 pushl %ebp .cfi_def_cfa_offset 8 movl %esp, %ebp .cfi_offset 5, -8 .cfi_def_cfa_register 5 movl 8(%ebp), %eax movl $_ZTV4Base+8, (%eax) popl %ebp ret .cfi_endproc _ZN4BaseC1Ev: .LFB6: .cfi_startproc .cfi_personality 0x0,__gxx_personality_v0 pushl %ebp .cfi_def_cfa_offset 8 movl %esp, %ebp .cfi_offset 5, -8 .cfi_def_cfa_register 5 movl 8(%ebp), %eax movl $_ZTV4Base+8, (%eax) popl %ebp ret .cfi_endproc
ZN4BaseC1Ev这个符号是C++函数Base::Base() 的内部符号名,可使用c++flit将它还原。C++里的class,可以定义数据成员,函数成员两种。但转化到汇编层面时,每个对象里面真正存放的是数据成员,以及虚函数表。
在上面的Base类中,由于没有数据成员,因此它只有一个vtable指针。故Base类的定义,可以写成如下相应的C代码:
[cpp]
view plaincopyprint?
struct Base {
unsigned long **vtable;
}
[cpp] view plaincopyprint? void Base::Base(struct Base *this) { this->vtable = &Base_vtable; } void Base::Base(struct Base *this) { this->vtable = &Base_vtable; }
同样地,Derive类的构造函数如下:
[cpp]
view plaincopyprint?
struct Derive {
unsigned long **vtable;
};
void Derive::Derive(struct Derive *this)
{
this->vtable = &Derive_vtable;
}
[cpp] view plaincopyprint? .globl main .type main, @function main: .LFB3: .cfi_startproc .cfi_personality 0x0,__gxx_personality_v0 pushl %ebp .cfi_def_cfa_offset 8 movl %esp, %ebp .cfi_offset 5, -8 .cfi_def_cfa_register 5 andl $-16, %esp subl $32, %esp leal 24(%esp), %eax movl %eax, (%esp) call _ZN6DeriveC1Ev leal 24(%esp), %eax movl %eax, 28(%esp) movl 28(%esp), %eax movl (%eax), %eax movl (%eax), %edx movl 28(%esp), %eax movl %eax, (%esp) call *%edx movl $0, %eax leave ret .cfi_endproc .globl main .type main, @function main: .LFB3: .cfi_startproc .cfi_personality 0x0,__gxx_personality_v0 pushl %ebp .cfi_def_cfa_offset 8 movl %esp, %ebp .cfi_offset 5, -8 .cfi_def_cfa_register 5 andl $-16, %esp subl $32, %esp leal 24(%esp), %eax movl %eax, (%esp) call _ZN6DeriveC1Ev leal 24(%esp), %eax movl %eax, 28(%esp) movl 28(%esp), %eax movl (%eax), %eax movl (%eax), %edx movl 28(%esp), %eax movl %eax, (%esp) call *%edx movl $0, %eax leave ret .cfi_endproc
andl $-16, %esp
subl $32, %esp
这两句是为局部变量d和bp在堆栈上分配空间,也即如下的语句:
Derive d;
Base *pb;
leal 24(%esp), %eax
movl %eax, (%esp)
call _ZN6DeriveC1Ev
esp+24是变量d的首地址,先将它压到堆栈上,然后调用d的构造函数,相应翻译成C语言则如下:
Derive::Dervice(&d);
leal 24(%esp), %eax
movl %eax, 28(%esp)
这里其实是将&d的值赋给pb,也即:
pb = &d;
最关键的代码是下面这一段:
[cpp]
view plaincopyprint?
movl 28(%esp), %eax
movl (%eax), %eax
movl (%eax), %edx
movl 28(%esp), %eax
movl %eax, (%esp)
call *%edx
movl 28(%esp), %eax
movl (%eax), %eax
movl (%eax), %edx
movl 28(%esp), %eax
movl %eax, (%esp)
call *%edx
翻译成C语言也就传神的那句:
pb->vtable[0](bp);
编译器会记住f虚函数放在vtable的第0项,这是编译时信息。
5. 小结
这里省略了很多关于编译器和C++的细枝未节,是出于讨论方便用的需要。从上面的编译代码可以看到以下信息:
1.每个类都有各有的vtable结构,编译会正确填写它们的虚函数表
2. 对象在构造函数时,设置vtable值为该类的虚函数表
3.在指针或者引用时调用虚函数,是通过object->vtable加上虚函数的offset来实现的。
当然这仅仅是g++的实现方式,它和VC++的略有不同,但原理是一样的。
相关文章推荐
- 基于LINUX平台G++编译器从汇编层面深度剖析C++虚函数
- 从汇编层面深度剖析c++虚函数
- 从汇编层面深度剖析C++虚函数
- 从汇编层面深度剖析C++虚函数
- 从汇编层面深度剖析C++虚函数
- 从汇编层面深度剖析 C++ 虚函数
- 从汇编层面深度剖析C++虚函数
- 从汇编层面深度剖析C++虚函数
- 从汇编层面深度剖析C++虚函数
- 从汇编层面深度剖析C++基本对象布局
- 从汇编层面深度剖析C++基本对象布局 .
- 从汇编层面深度剖析C++基本对象布局
- 从汇编层面深度剖析C++虚函数
- 深度剖析C++虚函数
- [c++深度剖析】继承和虚函数(一)
- 《 C++深度剖析》学习日志十八——神秘的临时对象
- 用汇编的眼光看C++(之虚函数)12
- C/C++ Volatile关键词深度剖析
- C/C++ Volatile关键词深度剖析
- 用汇编的眼光看C++(之虚函数)