vptr是一个相对容易思考的问题,因为vptr明确的属于一个实例,所以vptr的赋值理应放在类的构造函数中。 g++为每个有虚函数的类在构造函数末尾中隐式的添加了为vptr赋值的操作 。
同样通过生成的汇编代码验证:
Assembly (x86)
class Derived : public Base1, public Base2 { 80488dc: 55 push %ebp 80488dd: 89 e5 mov %esp,%ebp 80488df: 83 ec 18 sub $0x18,%esp 80488e2: 8b 45 08 mov 0x8(%ebp),%eax 80488e5: 89 04 24 mov %eax,(%esp) 80488e8: e8 d3 ff ff ff call 80488c0 <_ZN5Base1C1Ev> 80488ed: 8b 45 08 mov 0x8(%ebp),%eax 80488f0: 83 c0 04 add $0x4,%eax 80488f3: 89 04 24 mov %eax,(%esp) 80488f6: e8 d3 ff ff ff call 80488ce <_ZN5Base2C1Ev> 80488fb: 8b 45 08 mov 0x8(%ebp),%eax 80488fe: c7 00 48 8a 04 08 movl $0x8048a48,(%eax) 8048904: 8b 45 08 mov 0x8(%ebp),%eax 8048907: c7 40 04 5c 8a 04 08 movl $0x8048a5c,0x4(%eax) 804890e: c9 leave 804890f: c3 ret可以看到在代码中,Derived类的构造函数为实例的两个vptr赋初值,可是,这两个初值居然是立即数!立即数!立即数! 这说明了vtbl的生成并不是运行时的,而是在编译期就已经确定了存放在这两个地址上的 !
这个地址不出意料的属于.rodata(只读数据段),使用 objdump -s -j .rodata 提取出对应的内存观察:
80489e0 03000000 01000200 00000000 42617365 ............Base 80489f0 313a3a66 28290042 61736532 3a3a6728 1::f().Base2::g( 8048a00 29004465 72697665 643a3a66 28290044 ).Derived::f().D 8048a10 65726976 65643a3a 67282900 44657269 erived::g().Deri 8048a20 7665643a 3a682829 00000000 00000000 ved::h()........ 8048a30 00000000 00000000 00000000 00000000 ................ 8048a40 00000000 a08a0408 34880408 68880408 ........4...h... 8048a50 94880408 fcffffff a08a0408 60880408 ............`... 8048a60 00000000 c88a0408 08880408 00000000 ................ 8048a70 00000000 d88a0408 dc870408 37446572 ............7Der 8048a80 69766564 00000000 00000000 00000000 ived............ 8048a90 00000000 00000000 00000000 00000000 ................ 8048aa0 889f0408 7c8a0408 00000000 02000000 ....|........... 8048ab0 d88a0408 02000000 c88a0408 02040000 ................ 8048ac0 35426173 65320000 a89e0408 c08a0408 5Base2.......... 8048ad0 35426173 65310000 a89e0408 d08a0408 5Base1..........
由于程序运行的机器是小端机,经过简单的转换就可以得到第一个vptr所指向的内存中的第一条数据为0x80488834,如果把这个数据解释为函数地址到汇编文件中查找,会得到:
Assembly (x86)
08048834 <_ZN7Derived1fEv>: }; class Derived : public Base1, public Base2 { public: virtual void f() { 8048834: 55 push %ebp 8048835: 89 e5 mov %esp,%ebp 8048837: 83 ec 18 sub $0x18,%espBingo! g++在编译期就为每个类确定了vtbl的内容,并且在构造函数中添加相应代码使vptr能够指向已经填好的vtbl的地址 。
这也同时为我们解答了第三个问题:
在Linux中运行的C++程序虚拟存储器中,vptr、vtbl存放在虚拟存储的什么位置?
直接看图:
虚函数在虚拟存储器中的位置