型メタデータ

型メタデータは、IRモジュールが、与えられたグローバル変数のセット内のアドレスに対応するポインタセットを協調的に構築できるようにするメカニズムです。LLVMの制御フローの整合性の実装では、このメタデータを使用して、(各呼び出しサイトで)特定のアドレスが、特定のクラスまたは関数型の有効なvtableまたは関数ポインタのいずれかに対応していることを効率的にチェックします。また、そのプログラム全体の非仮想化パスでは、このメタデータを使用して、特定の仮想呼び出しの潜在的な呼び出し先を特定します。

このメカニズムを使用するには、クライアントは次の2つの要素を持つメタデータノードを作成します。

  1. グローバル変数へのバイトオフセット(通常、関数ではゼロ)

  2. 型識別子を表すメタデータオブジェクト

これらのメタデータノードは、!typeメタデータ種類を持つグローバルオブジェクトメタデータアタッチメントを使用してグローバル変数に関連付けられます。

各型識別子は、グローバル変数または関数のいずれかを排他的に識別する必要があります。

制限事項

現在の実装では、x86-32およびx86-64アーキテクチャでの関数へのメタデータの添付のみをサポートしています。

組込み関数llvm.type.testを使用して、特定のポインタが型識別子に関連付けられているかどうかをテストします。

型メタデータを使用した型情報の表現

このセクションでは、Clangが型メタデータを使用して仮想テーブルに関連付けられたC++型情報をどのように表現するかについて説明します。

次の継承階層について考えてみましょう

struct A {
  virtual void f();
};

struct B : A {
  virtual void f();
  virtual void g();
};

struct C {
  virtual void h();
};

struct D : A, C {
  virtual void f();
  virtual void h();
};

A、B、C、Dの仮想テーブルオブジェクトは、次のようになります(Itanium ABIの下で)。

表2 A、B、C、Dの仮想テーブルレイアウト

クラス

0

1

2

3

4

5

6

A

A::offset-to-top

&A::rtti

&A::f

B

B::offset-to-top

&B::rtti

&B::f

&B::g

C

C::offset-to-top

&C::rtti

&C::h

D

D::offset-to-top

&D::rtti

&D::f

&D::h

D::offset-to-top

&D::rtti

&D::hのサンク

型Aのオブジェクトが構築されると、Aの仮想テーブルオブジェクト内の&A::fのアドレスがオブジェクトのvtableポインタに格納されます。ABI用語では、このアドレスはアドレスポイントとして知られています。同様に、型Bのオブジェクトが構築されると、&B::fのアドレスがvtableポインタに格納されます。このように、Bの仮想テーブルオブジェクト内のvtableは、Aのvtableと互換性があります。

Dは、多重継承を使用しているため、もう少し複雑です。その仮想テーブルオブジェクトには、Aのvtableと互換性のある1つと、Cのvtableと互換性のあるもう1つの2つのvtableが含まれています。型Dのオブジェクトには、Aサブオブジェクトに属し、Aのvtableと互換性のあるvtableのアドレスを含む1つと、Cサブオブジェクトに属し、Cのvtableと互換性のあるvtableのアドレスを含む1つの2つの仮想ポインタが含まれています。

上記のクラス階層の互換性情報の完全なセットを以下に示します。次の表は、クラスの名前、そのクラスのvtable内のアドレスポイントのオフセット、およびそのアドレスポイントと互換性のあるクラスの1つの名前を示しています。

表3 A、B、C、Dの型オフセット

vtableの

オフセット

互換性のあるクラス

A

16

A

B

16

A

B

C

16

C

D

16

A

D

48

C

次のステップは、この互換性情報をIRにエンコードすることです。これを行う方法は、互換性のある各クラスの名前で型メタデータを作成し、それを使用して各vtable内の互換性のある各アドレスポイントを関連付けることです。たとえば、これらの型メタデータエントリは、上記の階層の互換性情報をエンコードします。

@_ZTV1A = constant [...], !type !0
@_ZTV1B = constant [...], !type !0, !type !1
@_ZTV1C = constant [...], !type !2
@_ZTV1D = constant [...], !type !0, !type !3, !type !4

!0 = !{i64 16, !"_ZTS1A"}
!1 = !{i64 16, !"_ZTS1B"}
!2 = !{i64 16, !"_ZTS1C"}
!3 = !{i64 16, !"_ZTS1D"}
!4 = !{i64 48, !"_ZTS1C"}

この型メタデータを使用すると、組込み関数llvm.type.testを使用して、特定のポインタが型識別子と互換性があるかどうかをテストできるようになります。逆方向に考えて、特定のポインタに対してllvm.type.testがtrueを返す場合、特定の仮想呼び出しが呼び出す可能性のある仮想関数の識別子も静的に判断できます。たとえば、プログラムがポインタを!"_ZST1A"のメンバーであると想定する場合、アドレスは_ZTV1A+16_ZTV1B+16、または_ZTV1D+16(つまり、それぞれA、B、Dのvtableのアドレスポイント)のいずれかであることしかわかりません。次に、そのポインタからアドレスをロードすると、アドレスは&A::f&B::f、または&D::fのいずれかであることしかわかりません。

型メンバーシップのアドレスのテスト

プログラムがllvm.type.testを使用してアドレスをテストする場合、これにより、リンク時最適化パスLowerTypeTestsが、この組込み関数への呼び出しを、型メンバーテストを実行するための効率的なコードに置き換えることになります。高レベルでは、このパスは、参照されるグローバル変数をオブジェクトファイル内の連続したメモリ領域にレイアウトし、そのメモリ領域にマップするビットベクターを構築し、各llvm.type.test呼び出しサイトで、それらのビットベクターに対してポインタをテストするコードを生成します。レイアウト操作のため、グローバル変数の定義はLTO時に使用可能である必要があります。詳細については、制御フロー整合性設計ドキュメントを参照してください。

関数を識別する型識別子は、ジャンプテーブルに変換されます。これは、型識別子に関連付けられた各関数のブランチ命令で構成されるコードブロックであり、ターゲット関数に分岐します。パスは、取得された関数アドレスを対応するジャンプテーブルエントリにリダイレクトします。オブジェクトファイルのシンボルテーブルでは、ジャンプテーブルエントリは元の関数の識別子を取得するため、モジュール外で取得されたアドレスはモジュール内で実行される検証に合格します。

ジャンプテーブルは外部関数を呼び出す可能性があるため、その定義はLTO時に使用可能である必要はありません。外部で定義された関数が型識別子に関連付けられている場合、モジュール内のその識別子がモジュール外の識別子と同じであるという保証はありません。前者が必要な場合はジャンプテーブルエントリになるためです。

GlobalLayoutBuilderクラスは、基礎となるビットセットのサイズを最小限に抑えるために、グローバル変数を効率的にレイアウトする役割を担います。

target datalayout = "e-p:32:32"

@a = internal global i32 0, !type !0
@b = internal global i32 0, !type !0, !type !1
@c = internal global i32 0, !type !1
@d = internal global [2 x i32] [i32 0, i32 0], !type !2

define void @e() !type !3 {
  ret void
}

define void @f() {
  ret void
}

declare void @g() !type !3

!0 = !{i32 0, !"typeid1"}
!1 = !{i32 0, !"typeid2"}
!2 = !{i32 4, !"typeid2"}
!3 = !{i32 0, !"typeid3"}

declare i1 @llvm.type.test(i8* %ptr, metadata %typeid) nounwind readnone

define i1 @foo(i32* %p) {
  %pi8 = bitcast i32* %p to i8*
  %x = call i1 @llvm.type.test(i8* %pi8, metadata !"typeid1")
  ret i1 %x
}

define i1 @bar(i32* %p) {
  %pi8 = bitcast i32* %p to i8*
  %x = call i1 @llvm.type.test(i8* %pi8, metadata !"typeid2")
  ret i1 %x
}

define i1 @baz(void ()* %p) {
  %pi8 = bitcast void ()* %p to i8*
  %x = call i1 @llvm.type.test(i8* %pi8, metadata !"typeid3")
  ret i1 %x
}

define void @main() {
  %a1 = call i1 @foo(i32* @a) ; returns 1
  %b1 = call i1 @foo(i32* @b) ; returns 1
  %c1 = call i1 @foo(i32* @c) ; returns 0
  %a2 = call i1 @bar(i32* @a) ; returns 0
  %b2 = call i1 @bar(i32* @b) ; returns 1
  %c2 = call i1 @bar(i32* @c) ; returns 1
  %d02 = call i1 @bar(i32* getelementptr ([2 x i32]* @d, i32 0, i32 0)) ; returns 0
  %d12 = call i1 @bar(i32* getelementptr ([2 x i32]* @d, i32 0, i32 1)) ; returns 1
  %e = call i1 @baz(void ()* @e) ; returns 1
  %f = call i1 @baz(void ()* @f) ; returns 0
  %g = call i1 @baz(void ()* @g) ; returns 1
  ret void
}

!vcall_visibilityメタデータ

vtableから未使用の関数ポインタを削除できるようにするには、それを使用できるすべての仮想呼び出しがコンパイラーに認識されているか、別の翻訳単位がvtableを介してより多くの呼び出しを導入する可能性があるかどうかを知る必要があります。これはvtableのリンケージと同じではありません。呼び出しサイトが、より広く可視な基本クラスのポインタを使用している可能性があるためです。たとえば、次のコードを考えてみましょう。

__attribute__((visibility("default")))
struct A {
  virtual void f();
};

__attribute__((visibility("hidden")))
struct B : A {
  virtual void f();
};

LTOでは、Bの宣言を確認できるすべてのコードが私たちに表示されることがわかっています。ただし、BへのポインタはA*にキャストされ、別のリンケージユニットに渡される可能性があり、そのユニットはfを呼び出す可能性があります。この呼び出しは、Bのvtableからロード(オブジェクトポインタを使用)し、次にB::fを呼び出します。これは、BのvtableまたはB::fの実装から関数ポインタを削除できないことを意味します。ただし、動的な基本クラスに関するすべてのコードを確認できる場合(Bが非表示の可視性を持つクラスからのみ継承される場合)、この最適化は有効になります。

この概念は、vtableオブジェクトに添付された!vcall_visibilityメタデータでIRに表され、次の値があります。

動作

0(または省略)

パブリック

このvtableを使用する仮想関数呼び出しは、外部コードから行うことができます。

1

リンケージユニット

このvtableを使用する可能性のあるすべての仮想関数呼び出しは、現在のLTOユニットにあります。つまり、LTOリンクが実行されると、現在のモジュールに存在します。

2

翻訳ユニット

このvtableを使用する可能性のあるすべての仮想関数呼び出しは、現在のモジュールにあります。

さらに、!vcall_visibilityメタデータ(ゼロ以外の値を使用)でマークされたvtableからのすべての関数ポインタのロードは、llvm.type.checked.load組込み関数を使用して行う必要があります。これにより、仮想呼び出しサイトを、ロードする可能性のあるvtableと関連付けることができます。vtableの他の部分(RTTI、offset-to-topなど)は、通常のロードでアクセスできます。