没脚的雀

C++对象内存布局

C++对象内存布局–gdb初探

继承、多态是OOP中两大特性。C++号称是面向对象的语言,自然也要支持上面的两种设计特性。这里主要探究g++中对这两大特性的支持——vtables。大部分内容参考自 C++ vtables

多态与继承–vtable的作用

在本小节的讨论中, 我们使用如下的继承体系:

1549804115977

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

using namespace std;

class Person {
public:
Person():iPerson(1){}
int iPerson;
virtual void helloPerson() { cout << "Person::helloPerson()" << endl; }
};
class Father: public Person {
public:
Father():iFather(2){}
int iFather;
virtual void helloFather() { cout << "Father::helloFather()" << endl; }
};
class Mother: public Person {
public:
Mother():iMother(3){}
int iMother;
virtual void helloMother() { cout << "Mother::helloMother()" << endl; }
};
class Child: public Father, public Mother {
public:
Child():iChild(4){}
int iChild;
virtual void helloChild() { cout << "Child::helloChild()" << endl; }
};

int main() {
Person person;
person.helloPerson();
Father father;
father.helloFather();
Mother mother;
mother.helloMother();
Child child;
child.helloChild();
// child.helloPerson(); // compile error: request for member "helloPerson()" is ambiguous;
}

gdb设置

使用g++对上面的代码进行编译,以及gdb设置断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ g++ -g multi-inher.cpp -o multi-inher
$ gdb multi-inher
(gdb) set p obj on
(gdb) set p pretty on
(gdb) b 38
Breakpoint 1 at 0x4009e9: file mutli-inher.cpp, line 38.
(gdb) r
Starting program: /software/home/recsys/BookCode/cppObj/multi-inher
Person::helloPerson()
Father::helloFather()
Mother::helloMother()

Breakpoint 1, main () at mutli-inher.cpp:38
warning: Source file is more recent than executable.
38 child.helloChild();

child变量内存布局

查看child变量的内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(gdb) call sizeof(Child)
$1 = 40
(gdb) x/48xb &child
0x7fffffffe1c0: 0xd0 0x0c 0x40 0x00 0x00 0x00 0x00 0x00
0x7fffffffe1c8: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00
0x7fffffffe1d0: 0xf8 0x0c 0x40 0x00 0x00 0x00 0x00 0x00
0x7fffffffe1d8: 0x01 0x00 0x00 0x00 0x03 0x00 0x00 0x00
0x7fffffffe1e0: 0x04 0x00 0x00 0x00 0xff 0x7f 0x00 0x00
0x7fffffffe1e8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(gdb) p child
$2 = (Child) {
<Father> = {
<Person> = {
_vptr.Person = 0x400cd0 <vtable for Child+16>,
iPerson = 1
},
members of Father:
iFather = 2
},
<Mother> = {
<Person> = {
_vptr.Person = 0x400cf8 <vtable for Child+56>,
iPerson = 1
},
members of Mother:
iMother = 3
},
members of Child:
iChild = 4
}

x/48xb &child的输出,我们可以观察到Child的大小为40, child变量的内存地址在: 0x0x7fffffffe1c0,而且貌似这些内存位置上面对应的值是一些地址,看看这些地址对应的symbol是什么。

1
2
3
4
(gdb) info symbol 0x400cd0
vtable for Child + 16 in section .rodata of /software/home/recsys/BookCode/cppObj/multi-inher
(gdb) info symbol 0x400cf8
vtable for Child + 56 in section .rodata of /software/home/recsys/BookCode/cppObj/multi-inher

整理如下,可以看到child中包含了两个指针,指向了Child类的vtable的不同偏移位置,这两个指针就是虚指针了。

地址 十六进制值 对应的symbol
0x7fffffffe1c0 0x400cd0 vtable for Child + 16
0x7fffffffe1c8 0x0000000100000002 Father::iPerson, Father::iFather
0x7fffffffe1d0 0x400cf8 vtable for Child + 56
0x7fffffffe1d8 0x0000000100000003 Mother::iPerson, Mother::iMother
0x7fffffffe1e0 0x0000000400000000 Child::iChild, padding

Child的vtable的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) x/80xb 0x400cd0 - 16
0x400cc0 <_ZTV5Child>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400cc8 <_ZTV5Child+8>: 0x80 0x0d 0x40 0x00 0x00 0x00 0x00 0x00
0x400cd0 <_ZTV5Child+16>: 0x6e 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400cd8 <_ZTV5Child+24>: 0xc8 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400ce0 <_ZTV5Child+32>: 0x98 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400ce8 <_ZTV5Child+40>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0x400cf0 <_ZTV5Child+48>: 0x80 0x0d 0x40 0x00 0x00 0x00 0x00 0x00
0x400cf8 <_ZTV5Child+56>: 0x6e 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400d00 <_ZTV5Child+64>: 0x22 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400d08: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
(gdb) info symbol 0x400d80
typeinfo for Child in section .rodata of /software/home/recsys/BookCode/cppObj/multi-inher
(gdb) info symbol 0x400a6e
Person::helloPerson() in section .text of /software/home/recsys/BookCode/cppObj/multi-inher
(gdb) info symbol 0x400ac8
Father::helloFather() in section .text of /software/home/recsys/BookCode/cppObj/multi-inher
(gdb) info symbol 0x400b98
Child::helloChild() in section .text of /software/home/recsys/BookCode/cppObj/multi-inher
(gdb) info symbol 0x400d80
typeinfo for Child in section .rodata of /software/home/recsys/BookCode/cppObj/multi-inher
(gdb) info symbol 0x400b22
Mother::helloMother() in section .text of /software/home/recsys/BookCode/cppObj/multi-inher

整理如下:

内存地址 十六进制值 symbol
0x400cc0 0x0000000 top-offset(稍后说明)
0x400cc8 0x400d80 typeinfo for Child
0x400cd0 0x400a6e Person::helloPerson()
0x400cd8 0x400ac8 Father::helloFather()
0x400ce0 0x400b98 Child::helloChild()
0x400ce8 -16 top-offset
0x400cf0 0x400d80 typeinfo for Child
0x400cf8 0x400a6e Person::helloPerson()
0x400d00 0x400b22 Mother::helloMother()

child的vtable包含的内容包括:

  • top-offset: 对象起始地址与当前vptr所属子对象起始地址的偏移

    1
    2
    3
    4
    5
    6
    7
    [cling]$ .L mutli-inher.cpp
    [cling]$ Child child
    (Child &) @0x7f963138b018
    [cling]$ Father * pF = &child
    (Father *) 0x7f963138b018
    [cling]$ Mother * pM = &child
    (Mother *) 0x7f963138b028

    从上面可以看到,当进行指针转换,将子类地址赋值给第二继承类的指针变量时,在这里则是将Child变量地址赋值给Mother指针变量时,需要用到vtable存储的top-offset, pM = &child - (-16) ;

    top-offset与vptr的关系: top-offset = vptr[-2]

  • typeinfo: 当前类信息编译器开了RTTI,将会在vptr[-1]位置存储当前类的typeinfo对象;

  • virtual function: 最后是一些虚函数对应的地址;

    • vptr[i]—当前类对应的第i个虚函数

child变量与Child类的vtable的关系如下:

1549810057207

因此,vtable的作用包括了一下两点:

  • 记录虚函数地址;
  • 记录当前类的类型信息;
  • 记录对象起始地址与子对象起始地址之间的偏移;

菱形继承–虚继承

在上面小节的继承体系下,child变量将会保存两份parent子对象,因而,导致了

1
child.helloParent();

编译错误。通常情况下,我们并不希望在child中保存两份子对象,OOP中通过虚继承解决上面的问题

1549810681372

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

using namespace std;

class Person {
public:
Person():iPerson(1){}
int iPerson;
virtual void helloPerson() { cout << "Person::helloPerson()" << endl; }
};
class Father: public virtual Person {
public:
Father():iFather(2){}
int iFather;
virtual void helloFather() { cout << "Father::helloFather()" << endl; }
};
class Mother: public virtual Person {
public:
Mother():iMother(3){}
int iMother;
virtual void helloMother() { cout << "Mother::helloMother()" << endl; }
};
class Child: public Father, public Mother {
public:
Child():iChild(4){}
int iChild;
virtual void helloChild() { cout << "Child::helloChild()" << endl; }
};

int main() {
Person person;
person.helloPerson();
Father father;
father.helloFather();
Mother mother;
mother.helloMother();
Child child;
child.helloChild();
}

child 变量内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(gdb) p child
$1 = (Child) {
<Father> = {
<Person> = {
_vptr.Person = 0x400da0 <vtable for Child+96>,
iPerson = 1
},
members of Father:
_vptr.Father = 0x400d58 <vtable for Child+24>,
iFather = 2
},
<Mother> = {
members of Mother:
_vptr.Mother = 0x400d80 <vtable for Child+64>,
iMother = 3
},
members of Child:
iChild = 4
}
(gdb) call sizeof(Child)
$2 = 48
(gdb) x/56xb &child
0x7fffffffe1b0: 0x58 0x0d 0x40 0x00 0x00 0x00 0x00 0x00
0x7fffffffe1b8: 0x02 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffe1c0: 0x80 0x0d 0x40 0x00 0x00 0x00 0x00 0x00
0x7fffffffe1c8: 0x03 0x00 0x00 0x00 0x04 0x00 0x00 0x00
0x7fffffffe1d0: 0xa0 0x0d 0x40 0x00 0x00 0x00 0x00 0x00
0x7fffffffe1d8: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffe1e0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

可以看到,在使用了虚继承之后,Child的大小变成了48,对比上小节中child变量的内存布局,可以发现,多出来的8字节来自于Father子对象数据iFather以及Parent子对象iParent的内存对齐

通过 info symbol 查看child内存上面各个地址的对象:

地址 十六进制值 对应的symbol
0x7fffffffe1b0 0x400d58 vtable for Child + 24
0x7fffffffe1b8 0x0000000000000002 Father::iFather, padding
0x7fffffffe1c0 0x400d80 vtable for Child + 64
0x7fffffffe1c8 0x0000000300000004 Mother::iMother, Child::iChild
0x7fffffffe1d0 0x400da0 vtable for Child+96
0x7fffffffe1d8 0x0000000100000000 Parent::iParent, padding

与前一节不同的是:

  • Parent子对象被放置在整个对象靠后的地址

虚继承下,Child类的vtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) x/112xb 0x400d58 - 24
0x400d40 <_ZTV5Child>: 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400d48 <_ZTV5Child+8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400d50 <_ZTV5Child+16>: 0x60 0x0f 0x40 0x00 0x00 0x00 0x00 0x00
0x400d58 <_ZTV5Child+24>: 0xcc 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400d60 <_ZTV5Child+32>: 0x36 0x0c 0x40 0x00 0x00 0x00 0x00 0x00
0x400d68 <_ZTV5Child+40>: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400d70 <_ZTV5Child+48>: 0xf0 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0x400d78 <_ZTV5Child+56>: 0x60 0x0f 0x40 0x00 0x00 0x00 0x00 0x00
0x400d80 <_ZTV5Child+64>: 0x8a 0x0b 0x40 0x00 0x00 0x00 0x00 0x00
0x400d88 <_ZTV5Child+72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x400d90 <_ZTV5Child+80>: 0xe0 0xff 0xff 0xff 0xff 0xff 0xff 0xff
0x400d98 <_ZTV5Child+88>: 0x60 0x0f 0x40 0x00 0x00 0x00 0x00 0x00
0x400da0 <_ZTV5Child+96>: 0x0e 0x0a 0x40 0x00 0x00 0x00 0x00 0x00
0x400da8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

查看vtable的内容:

内存地址 十六进制值 symbol
0x400d40 0x20 virtual-base offset
0x400d48 0x00 top-offset
0x400d50 0x400f60 typeinfo for Child
0x400d58 0x400acc Father::helloFather()
0x400d60 0x400c36 Child::helloChild()
0x400d68 0x10 virtual-base offset
0x400d70 -16 top-offset
0x400d78 0x400f60 typeinfo for Child
0x400d80 0x400b8a Mother::helloMother()
0x400d88 0x00 virtual-base offset
0x400d90 -32 top-offset
0x400d98 0x400f60 typeinfo for Child
0x400da0 0x400a0e Parent::helloParent()

虚继承情况下,Child的vtable与上一小节的vtable主要的不同在于:

  • 增加了一个virtual-base offset用于记录共享子对象起始地址与当前对象起始地址之间的偏差
  • 由于g++将共享子对象放置在整个对象靠后的地址,Parent与Child无法共享vptr,因此vtable增加了与Parent相关的信息

1549815053588

大佬给口饭吃咧