新聞中心
你好,我是雨樂!

在樊城等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供網(wǎng)站設(shè)計、成都做網(wǎng)站 網(wǎng)站設(shè)計制作按需求定制網(wǎng)站,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),高端網(wǎng)站設(shè)計,成都全網(wǎng)營銷推廣,外貿(mào)營銷網(wǎng)站建設(shè),樊城網(wǎng)站建設(shè)費用合理。
在上一篇文章C++:從技術(shù)角度聊聊RTTI中聊到了虛函數(shù)表,以及內(nèi)部的部分布局。對于c++對象的內(nèi)存布局一直處于似懂非懂似清非清的階段,沒有去深入了解過,所以借著這個機會,一并分析下。
多態(tài)在我們?nèi)粘9ぷ髦杏玫乃闶潜容^多的一種特性,業(yè)界編譯器往往是通過虛函數(shù)來實現(xiàn)運行時多態(tài),而涉及到虛函數(shù)的內(nèi)存布局往往是最麻煩且容易出錯的,本文從一個簡單的例子入手,借助gcc和gdb,對內(nèi)存布局進行分析,相信看完本文,對內(nèi)存布局會有一個清晰的認識。
多態(tài)
眾所周知,C++為了實現(xiàn)多態(tài)(運行期),引進了虛函數(shù)(語言標準支持的,其它實現(xiàn)方式不在本文討論范圍內(nèi)),而虛函數(shù)的實現(xiàn)機制則是通過虛函數(shù)表。這塊的知識點不算多,卻非常重要,因此往往是面試必問之一,當然,對于我也不例外。作為候選人,如果沒有把運行期多態(tài)的實現(xiàn)機制講清楚,那么此次面試基本涼涼~~
仍然以上一篇文章的代碼為例,代碼如下:
class Base1 {
public:
virtual void fun() {}
virtual void f1() {}
int a;
};
class Derived : public Base {
public:
void fun() {} // override Base::fun()
int b;
};
void call(Base *b) {
b->fun();
}在上述示例call()函數(shù)中,當b指向Base對象時候,call()函數(shù)實際調(diào)用的是Base::fun();當b指向Derived對象時候,call()函數(shù)實際調(diào)用的是Derived::fun()。之所以可以這么實現(xiàn),是因為虛函數(shù)后面的實現(xiàn)機制--虛函數(shù)表(后面稱為Vtable):
? 對于每個類(存在虛函數(shù),后面文中不再贅述),存在一個表,表的內(nèi)容包含虛函數(shù)等(不僅僅是虛函數(shù),在后面會有細講),類似于如下這種:
vtable_Base = {&Base::func, ...}
vtable_Derived = {&Derived::func, ...}? 在創(chuàng)建類對象時候,對象最前部會有一個指針(稱之為vptr),指向給類虛函數(shù)表的對應(yīng)位置。PS:(需要注意的是并不是指向Vtable的頭,這塊一定要注意)
那么,call()函數(shù)在運行的時候,因為不知道其參數(shù)b所指向具體類型是什么,所以只能通過其它方式進行調(diào)用。在前面的內(nèi)容中,有提到過每個對象會有一個指針指向其類的虛函數(shù)表,那么就可以通過該虛函數(shù)表進行相應(yīng)的調(diào)用。因此,call()函數(shù)中的b->fun()就類似于如下:
((Vtable*)b)[0]()
在現(xiàn)在編譯器對多態(tài)的實現(xiàn)中,原理與上述差不多,只是更為復(fù)雜。比如在在虛函數(shù)指針的索引(如上述例子中的index 0),這個index是根據(jù)函數(shù)的聲明順序而來,如果在Derived中再新增一個virtual函數(shù)fun2(),那么其在虛函數(shù)表中的index就是1。
實現(xiàn)
本節(jié)中以一個多繼承作為示例,代碼如下:
class Base1 {
public:
void f0() {}
virtual void f1() {}
int a;
};
class Base2 {
public:
virtual void f2() {}
int b;
};
class Derived : public Base1, public Base2 {
public:
void d() {}
void f2() {} // override Base2::f1()
int c;
};
int main() {
Base2 *b2 = new Base2;
Derived *d = new Derived;
}后面的內(nèi)容將分別從基類和派生類的角度進行分析。
基類
首先,我們通過g++的命令-fdump-class-hierarchy進行編譯,以便在布局上有一個宏觀的認識,然后通過gdb進行更加詳細的分析。
Base2內(nèi)存布局如下:
Vtable for Base2
Base2::_ZTV5Base2: 3u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5Base2)
16 (int (*)(...))Base2::f2
Class Base2
size=16 align=8
base size=12 base align=8
Base2 (0x0x7ff572e6b600) 0
vptr=((& Base2::_ZTV5Base2) + 16u)
在上述代碼中,Base2的Vtable名為 _ZTV5Base2 ,經(jīng)過c++filt處理之后,發(fā)現(xiàn)其為vtable for Base2。之所以是這種是因為被編譯器進行了mangled。其中,TV代表Table for Virtual,后面的數(shù)字5是類名的字符數(shù),Base2則是類名。
維基百科以g++3.4.6為示例,示例中之處Vtable應(yīng)該只包含指向Base2::f2 的指針,但在我的本地環(huán)境(g++5.4.0,布局如上述)中,B2::f2為第三行:首先是offset,其值為0;然后包含一個指向名為_ZTI5Base2的結(jié)構(gòu)的指針(這個在上節(jié)RTTI一文中有講,在本文后面也會涉及);最后是函數(shù)指針B2::f2。
g++ 3.4.6 from GCC produces the following 32-bit memory layout for the object b2:[nb 1]
b2: +0: pointer to virtual method table of Base2 +4: value of bvirtual method table of B2: +0: Base2::f2()
繼續(xù)看Class Base2部分,我們注意到有一句vptr=((& Base2::_ZTV5Base2) + 16u),通過這句可以知道,Base2類中其虛函數(shù)指針vptr指向其虛函數(shù)表的首位+16處。
在下面的內(nèi)容中,將通過gdb來分析其內(nèi)存布局。
(gdb) disas
Dump of assembler code for function main:
0x00000000004006f8 <+0>: push %rbp
0x00000000004006f9 <+1>: mov %rsp,%rbp
0x00000000004006fc <+4>: push %rbx
0x00000000004006fd <+5>: sub $0x18,%rsp
=> 0x0000000000400701 <+9>: mov $0x10,%edi
0x0000000000400706 <+14>: callq 0x400578 <_Znwm@plt>
0x000000000040070b <+19>: mov %rax,%rbx
0x000000000040070e <+22>: mov %rbx,%rdi
0x0000000000400711 <+25>: callq 0x40076a <_ZN5Base2C2Ev>
0x0000000000400716 <+30>: mov %rbx,-0x18(%rbp)
0x000000000040071a <+34>: mov $0x20,%edi
0x000000000040071f <+39>: callq 0x400578 <_Znwm@plt>
0x0000000000400724 <+44>: mov %rax,%rbx
0x0000000000400727 <+47>: mov %rbx,%rdi
0x000000000040072a <+50>: callq 0x40079a <_ZN7DerivedC2Ev>
0x000000000040072f <+55>: mov %rbx,-0x20(%rbp)
0x0000000000400733 <+59>: mov $0x0,%eax
0x0000000000400738 <+64>: add $0x18,%rsp
0x000000000040073c <+68>: pop %rbx
0x000000000040073d <+69>: pop %rbp
0x000000000040073e <+70>: retq
End of assembler dump.
(gdb) b *0x0000000000400716
Breakpoint 2 at 0x400716: file abc.cc, line 22.
(gdb) c
Continuing.
Breakpoint 2, 0x0000000000400716 in main () at abc.cc:22
22 Base2 *b2 = new Base2;
(gdb) disas
Dump of assembler code for function main:
0x00000000004006f8 <+0>: push %rbp
0x00000000004006f9 <+1>: mov %rsp,%rbp
0x00000000004006fc <+4>: push %rbx
0x00000000004006fd <+5>: sub $0x18,%rsp
0x0000000000400701 <+9>: mov $0x10,%edi
0x0000000000400706 <+14>: callq 0x400578 <_Znwm@plt>
0x000000000040070b <+19>: mov %rax,%rbx
0x000000000040070e <+22>: mov %rbx,%rdi
0x0000000000400711 <+25>: callq 0x40076a <_ZN5Base2C2Ev>
=> 0x0000000000400716 <+30>: mov %rbx,-0x18(%rbp)
0x000000000040071a <+34>: mov $0x20,%edi
0x000000000040071f <+39>: callq 0x400578 <_Znwm@plt>
0x0000000000400724 <+44>: mov %rax,%rbx
0x0000000000400727 <+47>: mov %rbx,%rdi
0x000000000040072a <+50>: callq 0x40079a <_ZN7DerivedC2Ev>
0x000000000040072f <+55>: mov %rbx,-0x20(%rbp)
0x0000000000400733 <+59>: mov $0x0,%eax
0x0000000000400738 <+64>: add $0x18,%rsp
0x000000000040073c <+68>: pop %rbx
0x000000000040073d <+69>: pop %rbp
0x000000000040073e <+70>: retq
End of assembler dump.
在上述匯編中<+14>處,調(diào)用了operator new進行內(nèi)存分配,然后將地址放于寄存器rax中,在<+25>處調(diào)用Base2構(gòu)造函數(shù),繼續(xù)分析:
(gdb) p/x $rax
$2 = 0x612c20
(gdb) x/2xg 0x612c20
0x612c20: 0x0000000000400918 0x0000000000000000
(gdb) p &(((Base2*)0)->b)
$3 = (int *) 0x8
首先通過p/x $rax獲取b2的地址0x612c20,然后通過x/4xg 0x612c20打印內(nèi)存地址,地址信息包含存儲的屬性;接著通過p &(((Base2*)0)->b)來獲取變量b的布局,其值為0x8,因此可以說明變量b在類Base2的第八字節(jié)處,即vptr之后,那么class base2的結(jié)構(gòu)布局如下:
在上述x/2xg 0x612c20的輸出中,有個地址0x0000000000400918,其指向Base2類的虛函數(shù)表,這個可以通過如下方式進行驗證:
(gdb) p *((Base2*)0x612c20)
$6 = {_vptr.Base2 = 0x400918, b = 0}
但是需要注意的是,其并不是指向虛函數(shù)表的首位,而是指向Vtable + 0x10處,下面是類Base2虛函數(shù)表的內(nèi)容:
(gdb) x/4xg 0x0000000000400918-0x10
0x400908 <_ZTV5Base2>: 0x0000000000000000 0x0000000000400980
0x400918 <_ZTV5Base2+16>: 0x000000000040074c 0x0000000000000000
(gdb) x/2i 0x000000000040074c
0x40074c <_ZN5Base22f2Ev>: push %rbp
0x40074d <_ZN5Base22f2Ev+1>: mov %rsp,%rbp
其中,0代表offset,第三項0x400918值與_vptr.Base2一致,其中的內(nèi)容通過x/2i 0x000000000040074c分析可以看出為Base2::f2()函數(shù)地址。那么第二項又代表什么呢?
還記得上篇文章中的RTTI信息么?對!第二項就是指向RTTI信息的地址,可以通過如下命令:
(gdb) x/2xg 0x0000000000400980
0x400980 <_ZTI5Base2>: 0x0000000000600da0 0x0000000000400990
(gdb) x/s 0x0000000000400990
0x400990 <_ZTS5Base2>: "5Base2"
其中,_ZTI5Base2代表typeinfo for Base2,其指向的地址有兩個內(nèi)容,分別是0x0000000000600da0和0x0000000000400990,其中0x400990存儲的是類名,可以通過x/s來證明。
然后接著分析0x0000000000600da0存儲的內(nèi)容,如下:
(gdb) x/2xg 0x0000000000600da0
0x600da0 <_ZTVN10__cxxabiv117__class_type_infoE@@CXXABI_1.3+16>: 0x0000003e9628b210 0x0000003e9628b230
_ZTVN10__cxxabiv117__class_type_infoE解析之后為vtable for __cxxabiv1::__class_type_info。
綜上,類Base2的內(nèi)存布局如下圖所示:
多重繼承
跟上節(jié)一樣,仍然通過 -fdump-class-hierarchy 參數(shù)獲取Derived類的詳細信息,如下:
Vtable for Derived
Derived::_ZTV7Derived: 7u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Base1::f1
24 (int (*)(...))Derived::f2
32 (int (*)(...))-16
40 (int (*)(...))(& _ZTI7Derived)
48 (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev
Class Derived
size=32 align=8
base size=32 base align=8
Derived (0x0x7f2708268af0) 0
vptr=((& Derived::_ZTV7Derived) + 16u)
Base1 (0x0x7f2708127840) 0
primary-for Derived (0x0x7f2708268af0)
Base2 (0x0x7f27081278a0) 16
vptr=((& Derived::_ZTV7Derived) + 48u)
接著繼續(xù)使用gdb進行分析:
(gdb) disas
Dump of assembler code for function main:
0x00000000004006f8 <+0>: push %rbp
0x00000000004006f9 <+1>: mov %rsp,%rbp
0x00000000004006fc <+4>: push %rbx
0x00000000004006fd <+5>: sub $0x18,%rsp
0x0000000000400701 <+9>: mov $0x10,%edi
0x0000000000400706 <+14>: callq 0x400578 <_Znwm@plt>
0x000000000040070b <+19>: mov %rax,%rbx
0x000000000040070e <+22>: mov %rbx,%rdi
0x0000000000400711 <+25>: callq 0x40076a <_ZN5Base2C2Ev>
0x0000000000400716 <+30>: mov %rbx,-0x18(%rbp)
0x000000000040071a <+34>: mov $0x20,%edi
0x000000000040071f <+39>: callq 0x400578 <_Znwm@plt>
0x0000000000400724 <+44>: mov %rax,%rbx
0x0000000000400727 <+47>: mov %rbx,%rdi
0x000000000040072a <+50>: callq 0x40079a <_ZN7DerivedC2Ev>
=> 0x000000000040072f <+55>: mov %rbx,-0x20(%rbp)
0x0000000000400733 <+59>: mov $0x0,%eax
0x0000000000400738 <+64>: add $0x18,%rsp
0x000000000040073c <+68>: pop %rbx
0x000000000040073d <+69>: pop %rbp
0x000000000040073e <+70>: retq
End of assembler dump.
(gdb) p/x $rax
$8 = 0x612c40
(gdb) p sizeof(Derived)
$9 = 32
(gdb) x/6xg 0x612c40
0x612c40: 0x00000000004008e0 0x0000000000000000
0x612c50: 0x0000000000400900 0x0000000000000000
0x612c60: 0x0000000000000000 0x00000000000203a1
(gdb) p &(((Derived*)0)->a)
$15 = (int *) 0x8
(gdb) p &(((Derived*)0)->b)
$16 = (int *) 0x18
(gdb) p &(((Derived*)0)->c)
$17 = (int *) 0x1c
p *((Derived*)0x612c40)
$13 = {= {_vptr.Base1 = 0x4008e0, a = 0}, = {_vptr.Base2 = 0x400900, b = 0}, c = 0}
從上述代碼可以看出,Derived的結(jié)構(gòu)布局如下:
接著,我們分析類Derived的虛函數(shù)表:
(gdb) x/7xg 0x00000000004008e0 - 0x10
0x4008d0 <_ZTV7Derived>: 0x0000000000000000 0x0000000000400938
0x4008e0 <_ZTV7Derived+16>: 0x0000000000400740 0x0000000000400758
0x4008f0 <_ZTV7Derived+32>: 0xfffffffffffffff0 0x0000000000400938
0x400900 <_ZTV7Derived+48>: 0x0000000000400763
其對應(yīng)如下:
Vtable for Derived
Derived::_ZTV7Derived: 7u entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI7Derived)
16 (int (*)(...))Base1::f1
24 (int (*)(...))Derived::f2
32 (int (*)(...))-16
40 (int (*)(...))(& _ZTI7Derived)
48 (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev
為了驗證如上,繼續(xù)使用gdb進行操作:
(gdb) x/2xg 0x0000000000400938
0x400938 <_ZTI7Derived>: 0x0000000000600df8 0x0000000000400970
(gdb) x/4xi 0x0000000000400740
0x400740 <_ZN5Base12f1Ev>: push %rbp
0x400741 <_ZN5Base12f1Ev+1>: mov %rsp,%rbp
0x400744 <_ZN5Base12f1Ev+4>: mov %rdi,-0x8(%rbp)
0x400748 <_ZN5Base12f1Ev+8>: nop
(gdb) x/2xi 0x0000000000400740
0x400740 <_ZN5Base12f1Ev>: push %rbp
0x400741 <_ZN5Base12f1Ev+1>: mov %rsp,%rbp
(gdb) x/2xi 0x0000000000400758
0x400758 <_ZN7Derived2f2Ev>: push %rbp
0x400759 <_ZN7Derived2f2Ev+1>: mov %rsp,%rbp
(gdb) x/4xi 0x0000000000400763
0x400763 <_ZThn16_N7Derived2f2Ev>: sub $0x10,%rdi
0x400767 <_ZThn16_N7Derived2f2Ev+4>: jmp 0x400758 <_ZN7Derived2f2Ev>
0x400769: nop
0x40076a <_ZN5Base2C2Ev>: push %rbp
在上面的內(nèi)存布局中,_ZThn16_N7Derived2f2Ev在上篇文章中沒有進行分析,那么這個標記代表什么意思么,其作用又是什么呢?
通過c++filt將其demanged之后,non-virtual thunk to Derived::f2()。那么這個thunk的目的或者意義在哪呢?
我們看下如下代碼:
Derived *d = new Derived;
Base1 *b1 = (Base1*)d;
Base2 *b2 = (Base2*)d;
std::cout << d << " " << b1 << " " << b2 << std::endl;
((Base2*)d)->f2();
輸出如下:
0x1cc0c20 0x1cc0c20 0x1cc0c30
可以看出,同樣是一個地址,使用Base1轉(zhuǎn)換的地址和使用Base2轉(zhuǎn)換的地址不同,這是因為在轉(zhuǎn)換的時候,對指針進行了偏移,即加上了sizeof(Base1)。
好了,言歸正傳。
分析下如下情況:
Base1* b1 = new Derived();
b1->f1();
其正常工作,不需要移動任何指針,這是因為b1指向Derived對象的首地址。
那么如下是下面這種情況呢?
Base2* b2 = new Derived();
// 相當于 Derived *d = new Derived;
// Base2* b2 = d + sizeof(Base1);
b2->f2();
對于創(chuàng)建對象操作,在上述代碼中有大致解釋,那么對于b2->f2()操作,編譯器又是如何實現(xiàn)的呢?
其必須將b2所指向的指針調(diào)整為具體的Derived對象的其實指針,這樣才能正確的調(diào)用f2。此操作可以在運行時完成,即在運行時候通過調(diào)整指針指向進行操作,但這樣效率明顯不高。所以為了解決效率問題,編譯器引入了thunk,即在編譯階段進行生成。那么針對上面的b2->f2()操作,編譯器會進行如下:
void thunk_to_Derived_f2(Base2* this) {
this -= sizeof(Base1);
Derived::f2(this);
}我們?nèi)匀煌ㄟ^gdb來驗證這一點,如下:
(gdb) x/2i 0x0000000000400763
0x400763 <_ZThn16_N7Derived2f2Ev>: sub $0x10,%rdi
0x400767 <_ZThn16_N7Derived2f2Ev+4>: jmp 0x400758 <_ZN7Derived2f2Ev>
其中,寄存器rdi中存儲的是this指針,對this指針進行-16操作,然后進行調(diào)用 Derived::f2(this) 。
繼續(xù)分析虛函數(shù)表的內(nèi)容,其第二項為TypeInfo信息:
(gdb) x/2xg 0x0000000000400938
0x400938 <_ZTI7Derived>: 0x0000000000600df8 0x0000000000400970
(gdb) x/2xg 0x0000000000600df8
0x600df8 <_ZTVN10__cxxabiv121__vmi_class_type_infoE@@CXXABI_1.3+16>: 0x0000003e9628df70 0x0000003e9628df90
(gdb) x/s 0x0000000000400970
0x400970 <_ZTS7Derived>: "7Derived"
所以,綜合以上內(nèi)容,class Derived的內(nèi)存布局如下圖所示:
通過上圖,可以看出class Derived對象有兩個vptr,那么有沒有可能將這倆vptr合并成一個呢?
答案是不行。這是因為與單繼承不同,在多繼承中,class Base1和class Base2相互獨立,它們的虛函數(shù)沒有順序關(guān)系,即f1和f2有著相同對虛表起始位置的偏移量,所以不可以按照偏移量的順序排布;并且class Base1和class Base2中的成員變量也是無關(guān)的,因此基類間也不具有包含關(guān)系;這使得class Base1和class Base2在class Derived中必須要處于兩個不相交的區(qū)域中,同時需要有兩個虛指針分別對它們虛函數(shù)表索引。
偏移(offset)
在前面的內(nèi)容中,我們多次提到了top offset,在上節(jié)Derived的虛函數(shù)表中,有兩個top offset,其值分別為0和-16,那么這個offset起什么作用呢?
在此,先給出結(jié)論:將對象從當前這個類型轉(zhuǎn)換為該對象的實際類型的地址偏移量。
仍然以前面的class Derived為例,其虛函數(shù)表布局如下:
Vtable for Derived
Derived::_ZTV7Derived: 7u entries
0<
分享標題:再議內(nèi)存布局,你學(xué)會了嗎?
URL網(wǎng)址:http://fisionsoft.com.cn/article/dphoeig.html


咨詢
建站咨詢
