文章目录
  1. 1. 多继承
    1. 1.1. 无虚函数
    2. 1.2. 有虚函数
    3. 1.3. 对数据成员的寻址
  2. 2. 小结

LLVM IR

前面的文章简单介绍了在LLVM IR的世界里C++对象模型是什么样子,本文继续介绍在多继承情况下的对象模型。

多继承

无虚函数

首先看看没有虚函数的情况下多继承的情况,示例代码如下:

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
class Genius {
public:
void Think() { puts("thinking"); }

int iq_;
};

class Dog {
public:
void Run() { puts("running"); }

double speed_;
};

class Scooby : public Genius, public Dog {
public:
void Doo() { puts("dooing"); }

short tail_;
};

int main() {
Genius *g = new Scooby();
Dog *d = reinterpret_cast<Dog*>(g);
Scooby *s = static_cast<Scooby*>(g);
g->Think();
d->Run();
s->Doo();
return 0;
}

Scooby 继承Genius和Dog,并含有自己的数据成员。编译一下得到IR:

1
2
3
%class.Genius = type { i32 }
%class.Dog = type { double }
%class.Scooby = type <{ %class.Genius, [4 x i8], %class.Dog, i16, [6 x i8] }>

内存布局很简单,在Scooby对象中直接包含了Genius和Dog对象,并按照声明顺序排列,当然,在最后为了字节对齐,还加上了一段padding。

而成员函数也只是稀松平常,直接由对象指针调用:

1
2
3
4
5
call void @_ZN6Genius5ThinkEv(%class.Genius* %10)
%11 = load %class.Dog*, %class.Dog** %d, align 8
call void @_ZN3Dog3RunEv(%class.Dog* %11)
%12 = load %class.Scooby*, %class.Scooby** %s, align 8
call void @_ZN6Scooby3DooEv(%class.Scooby* %12)

在没有虚函数的情况下,没有什么奇怪的事情发生。

有虚函数

示例代码如下:

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
class Genius {
public:
virtual void Think() { puts("thinking"); }

long long iq_;
};

class Dog {
public:
virtual void Run() { puts("running"); }

double speed_;
};

class Scooby : public Genius, public Dog {
public:
virtual void Think() { puts("scooby is thinking"); }

virtual void Run() { puts("scooby is running"); }

short tail_;
};

int main() {
Genius *g = new Scooby();
Dog *d = static_cast<Scooby*>(g);
g->Think();
d->Run();
return 0;
}

在多继承的情况下,对象还是需要和类信息绑定。Genius类有一个虚函数,Dog类也有一个虚函数,因此它们都会有一个虚函数表,而Scooby继承了它们,则需要有两个虚函数表,因此对象中也需要有两个虚表指针指向两个虚函数表。

1
2
3
%class.Genius = type { i32 (...)**, i64 }
%class.Dog = type { i32 (...)**, double }
%class.Scooby = type <{ %class.Genius, %class.Dog, i16, [6 x i8] }>

内存布局没什么特别,还是Genius和Dog两个对象,下面来看看虚函数表的初始化:

1
2
3
@_ZTV6Scooby = linkonce_odr unnamed_addr constant [7 x i8*] [i8* null, i8* null, i8* bitcast (void (%class.Scooby*)* @_ZN6Scooby5ThinkEv to i8*), i8* bitcast (void (%class.Scooby*)* @_ZN6Scooby3RunEv to i8*), i8* inttoptr (i64 -16 to i8*), i8* null, i8* bitcast (void (%class.Scooby*)* @_ZThn16_N6Scooby3RunEv to i8*)], comdat, align 8
@_ZTV6Genius = linkonce_odr unnamed_addr constant [3 x i8*] [i8* null, i8* null, i8* bitcast (void (%class.Genius*)* @_ZN6Genius5ThinkEv to i8*)], comdat, align 8
@_ZTV3Dog = linkonce_odr unnamed_addr constant [3 x i8*] [i8* null, i8* null, i8* bitcast (void (%class.Dog*)* @_ZN3Dog3RunEv to i8*)], comdat, align 8

比较意外的是,Scooby并没有采用两个虚函数表,事实上它把两个虚函数表拼在了一起,Genuis的部分在前面,Dog的部分在后面。这里还用了两个Run函数,之后再去考证。

看看Scooby的构造函数中对虚表指针的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
define linkonce_odr void @_ZN6ScoobyC2Ev(%class.Scooby* %this) unnamed_addr #3 comdat align 2 {
%1 = alloca %class.Scooby*, align 8
store %class.Scooby* %this, %class.Scooby** %1, align 8
%2 = load %class.Scooby*, %class.Scooby** %1
%3 = bitcast %class.Scooby* %2 to %class.Genius*
call void @_ZN6GeniusC2Ev(%class.Genius* %3) #2

%4 = bitcast %class.Scooby* %2 to i8*
%5 = getelementptr inbounds i8, i8* %4, i64 16
%6 = bitcast i8* %5 to %class.Dog*
call void @_ZN3DogC2Ev(%class.Dog* %6) #2

%7 = bitcast %class.Scooby* %2 to i32 (...)***
store i32 (...)** bitcast (i8** getelementptr inbounds ([7 x i8*], [7 x i8*]* @_ZTV6Scooby, i64 0, i64 2) to i32 (...)**), i32 (...)*** %7

%8 = bitcast %class.Scooby* %2 to i8*
%9 = getelementptr inbounds i8, i8* %8, i64 16
%10 = bitcast i8* %9 to i32 (...)***
store i32 (...)** bitcast (i8** getelementptr inbounds ([7 x i8*], [7 x i8*]* @_ZTV6Scooby, i64 0, i64 6) to i32 (...)**), i32 (...)*** %10
ret void
}

Scooby的子对象Genius和Dog的虚表指针分别指向了Scooby虚函数表的第二项和第六项,即Think函数和Run函数。它们的相对索引和Genius虚函数表相同。

接着来看看对Scooby函数的调用:

1
2
3
4
5
6
%15 = load %class.Genius*, %class.Genius** %g, align 8
%16 = bitcast %class.Genius* %15 to void (%class.Genius*)***
%17 = load void (%class.Genius*)**, void (%class.Genius*)*** %16
%18 = getelementptr inbounds void (%class.Genius*)*, void (%class.Genius*)** %17, i64 0
%19 = load void (%class.Genius*)*, void (%class.Genius*)** %18
call void %19(%class.Genius* %15)

对Scooby::Think函数的调用很简单,基于虚表指针寻址,找到Genuis虚函数表的第0项,就是Scooby::Think函数了。

而对Scooby::Run函数的寻址则要复杂一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  %6 = load %class.Genius*, %class.Genius** %g, align 8
%7 = bitcast %class.Genius* %6 to %class.Scooby*
%8 = icmp eq %class.Scooby* %7, null
br i1 %8, label %13, label %9

; <label>:9 ; preds = %0
%10 = bitcast %class.Scooby* %7 to i8*
%11 = getelementptr inbounds i8, i8* %10, i64 16
%12 = bitcast i8* %11 to %class.Dog*
br label %13

; <label>:13 ; preds = %9, %0
%14 = phi %class.Dog* [ %12, %9 ], [ null, %0 ]
store %class.Dog* %14, %class.Dog** %d, align 8

首先需要调整this指针,让它指向Scooby的Dog子对象,然后查找Scooby::Run函数:

1
2
3
4
5
6
%20 = load %class.Dog*, %class.Dog** %d, align 8
%21 = bitcast %class.Dog* %20 to void (%class.Dog*)***
%22 = load void (%class.Dog*)**, void (%class.Dog*)*** %21
%23 = getelementptr inbounds void (%class.Dog*)*, void (%class.Dog*)** %22, i64 0
%24 = load void (%class.Dog*)*, void (%class.Dog*)** %23
call void %24(%class.Dog* %20)

寻址方式不变,不过这时候找到的其实是@_ZThn16_N6Scooby3RunEv,这是它的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define linkonce_odr void @_ZN6Scooby3RunEv(%class.Scooby* %this) unnamed_addr #0 comdat align 2 {
%1 = alloca %class.Scooby*, align 8
store %class.Scooby* %this, %class.Scooby** %1, align 8
%2 = load %class.Scooby*, %class.Scooby** %1
%3 = call i32 @puts(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.3, i32 0, i32 0))
ret void
}

; Function Attrs: nounwind uwtable
define linkonce_odr void @_ZThn16_N6Scooby3RunEv(%class.Scooby* %this) unnamed_addr #0 comdat align 2 {
%1 = alloca %class.Scooby*, align 8
store %class.Scooby* %this, %class.Scooby** %1, align 8
%2 = load %class.Scooby*, %class.Scooby** %1
%3 = bitcast %class.Scooby* %2 to i8*
%4 = getelementptr inbounds i8, i8* %3, i64 -16
%5 = bitcast i8* %4 to %class.Scooby*
call void @_ZN6Scooby3RunEv(%class.Scooby* %5)
ret void
}

@_ZThn16_N6Scooby3RunEv函数的功能是,把this指针移回Scooby对象的基址,然后调用真实的Scooby::Run函数。这里饶了一大圈,还是回到了Scooby对象基址,何苦呢?因为在对Scooby::Run函数寻址的时候,需要Dog子对象的虚表指针,所以要先把Scooby对象指针调整到Dog子对象,之后调用Scooby::Run,在这个函数里面,整个Scooby对象的数据成员都是可见的,所以必须把this指针指向Scooby对象的基址。

对数据成员的寻址

我们可以顺便看一下Clang是如何对数据成员进行寻址的,在Scooby::Run函数中加入以下几行代码:

1
2
3
printf("%d\n", iq_);
printf("%d\n", speed_);
printf("%d\n", tail_);

编译得到:

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
define linkonce_odr void @_ZN6Scooby3RunEv(%class.Scooby* %this) unnamed_addr #0 comdat align 2 {
%1 = alloca %class.Scooby*, align 8
store %class.Scooby* %this, %class.Scooby** %1, align 8
%2 = load %class.Scooby*, %class.Scooby** %1

; Genius::iq_
%3 = call i32 @puts(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.3, i32 0, i32 0))
%4 = bitcast %class.Scooby* %2 to %class.Genius*
%5 = getelementptr inbounds %class.Genius, %class.Genius* %4, i32 0, i32 1
%6 = load i64, i64* %5, align 8
%7 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.4, i32 0, i32 0), i64 %6)

; Dog::speed_
%8 = bitcast %class.Scooby* %2 to i8*
%9 = getelementptr inbounds i8, i8* %8, i64 16
%10 = bitcast i8* %9 to %class.Dog*
%11 = getelementptr inbounds %class.Dog, %class.Dog* %10, i32 0, i32 1
%12 = load double, double* %11, align 8
%13 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.4, i32 0, i32 0), double %12)

; Scooby::tail_
%14 = getelementptr inbounds %class.Scooby, %class.Scooby* %2, i32 0, i32 2
%15 = load i16, i16* %14, align 2
%16 = sext i16 %15 to i32
%17 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str.4, i32 0, i32 0), i32 %16)
ret void
}

逻辑还是比较清晰的:查找Genius::iq,地址不变,把this指针转为Genius*类型;查找Dog::speed对象,首先把this指针指向Dog子对象,然后指针转为Dog*类型;查找Scooby::tail_,直接查找成员变量就好了。

小结

多继承场景下的对象模型相对复杂,不过捋清楚之后还是比较清晰的。比较麻烦的是,每家编译器对多继承的实现都不太一样,Clang的实现已经和《深度探索C++对象模型》里的实现有了一些出入,所以就不能照本宣科了。j

文章目录
  1. 1. 多继承
    1. 1.1. 无虚函数
    2. 1.2. 有虚函数
    3. 1.3. 对数据成员的寻址
  2. 2. 小结