LLVMコードカバレッジマッピングフォーマット

はじめに

LLVMのコードカバレッジマッピングフォーマットは、LLVMとClangのインストルメンテーションベースのプロファイリング(Clangの-fprofile-instr-generateオプション)を使用したコードカバレッジ分析を提供するために使用されます。

このドキュメントは、LLVMのコードカバレッジマッピングが内部的にどのように動作するかを知りたいと考えている方を対象としています。Clangのプロファイルガイド付き最適化の動作に関する予備知識があると役立ちますが、必須ではありません。独自のプログラムのコードカバレッジ分析にLLVMを使用することに関心のある方は、Clangドキュメント <https://clang.llvm.org/docs/SourceBasedCodeCoverage.html>を参照してください。

まず、LLVMのコードカバレッジマッピングフォーマットと、ClangとLLVMのコードカバレッジツールがこのフォーマットをどのように使用するかについて簡単に説明します。基本事項を説明した後、データ構造、LLVM IR表現、バイナリエンコーディングなど、カバレッジマッピングフォーマットのより高度な機能について説明します。

概要

LLVMのコードカバレッジマッピングフォーマットは、LLVM IRとオブジェクトファイルに埋め込むことができる自己完結型のデータフォーマットとして設計されています。これは、コードカバレッジツールがファイル内の特定のソース範囲と、インストルメント化されたプログラムを実行した後に取得された実行回数との間のマッピングを行うために必要なデータを格納することを目的としているため、このドキュメントではマッピングフォーマットとして記述されています。

マッピングデータは、コードカバレッジプロセスで2つの場所で使用されます。

  1. clangが-fcoverage-mappingを使用してソースファイルをコンパイルすると、ソース範囲とプロファイリングインストルメンテーションカウンタ間のマッピングを記述するマッピング情報が生成されます。この情報はLLVM IRに埋め込まれ、プログラムがリンクされると最終的な実行ファイルに便利に含まれます。

  2. また、llvm-covによっても使用されます。マッピング情報はオブジェクトファイルから抽出され、実行回数(プロファイルインストルメンテーションカウンタの値)とファイル内のソース範囲を関連付けるために使用されます。その後、ツールはプログラムの様々なコードカバレッジレポートを生成できます。

カバレッジマッピングフォーマットは、Clangだけでなく、任意のフロントエンドで使用できる「ユニバーサルフォーマット」を目指しています。また、フロントエンドがIRとオブジェクトファイルのサイズを削減するために、最小限のカバレッジマッピングデータを生成することを可能にすることも目的としています。たとえば、関数の各ステートメントについてマッピング情報を生成する代わりに、フロントエンドは同じ実行回数のステートメントをコードの領域にグループ化し、これらの領域についてのみマッピング情報を生成できます。

高度な概念

このガイドの残りの部分は、カバレッジマッピングフォーマットの動作方法に関する洞察を提供することを目的としています。

カバレッジマッピングフォーマットは、プロファイルインストルメンテーションカウンタが特定の関数に関連付けられているため、関数レベルで動作します。コードカバレッジを必要とする各関数について、フロントエンドは、ソースコード範囲と、その関数のプロファイルインストルメンテーションカウンタとの間のマッピングを行うことができるカバレッジマッピングデータを作成する必要があります。

マッピング領域

関数のカバレッジマッピングデータには、マッピング領域の配列が含まれています。マッピング領域は、この領域でカバーされているソースコード範囲ファイルIDカバレッジマッピングカウンタ、および領域の種類を格納します。マッピング領域にはいくつかの種類があります。

  • コード領域は、ソースコードの一部とカバレッジマッピングカウンタを関連付けます。これらはマッピング領域の大部分を占めています。コードカバレッジツールはこれらを使用して、行の実行回数を計算し、実行されなかったコードの領域を強調表示し、関数の様々なコードカバレッジ統計を取得します。例:

    int main(int argc, const char *argv[]) {     // Code Region from 1:40 to 9:2
                                                
      if (argc > 1) {                            // Code Region from 3:17 to 5:4
        printf("%s\n", argv[1]);              
      } else {                                   // Code Region from 5:10 to 7:4
        printf("\n");                         
      }                                         
      return 0;                                 
    }
    

  • スキップされた領域は、Clangのプリプロセッサによってスキップされたソース範囲を表すために使用されます。フロントエンドはこれらが実行されないことを知っているため、カバレッジマッピングカウンタとは関連付けられていません。コードカバレッジツールはこれらを使用して、関数内のスキップされた行を実行回数を持たない非コード行としてマークします。例:

    int main() {                // Code Region from 1:12 to 6:2
    #ifdef DEBUG                // Skipped Region from 2:1 to 4:2
      printf("Hello world"); 
    #endif                     
      return 0;                
    }
    

  • 展開領域は、Clangのマクロ展開を表すために使用されます。これには、追加のプロパティである展開されたファイルIDがあります。このプロパティは、コードカバレッジツールが、ファイルIDが展開されたファイルIDと一致するかどうかを確認することにより、このマクロ展開の結果として作成されたマッピング領域を見つけるために使用できます。コードカバレッジツールは、対応するファイルIDを持つ最初の領域の実行回数を調べることで、この領域の実行回数を決定できるため、カバレッジマッピングカウンタとは関連付けられていません。例:

    int func(int x) {                             
      #define MAX(x,y) ((x) > (y)? (x) : (y))     
      return MAX(x, 42);                           // Expansion Region from 3:10 to 3:13
    }
    

  • 分岐領域は、ソースコード内のインストルメント化可能な分岐条件をカバレッジマッピングカウンタと関連付け、個々の条件が「true」と評価された回数を追跡するカウンタと、「false」と評価された回数を追跡するカウンタを備えています。インストルメンテーション可能な分岐条件には、ブール論理演算子を使用するより大きなブール式が含まれる場合があります。「true」と「false」のケースは、ソースコードにトレースバックできる一意の分岐パスを表します。例:

    int func(int x, int y) {
      if ((x > 1) || (y > 3)) {  // Branch Region from 3:6 to 3:12
                                 // Branch Region from 3:17 to 3:23
        printf("%d\n", x);              
      } else {                                
        printf("\n");                         
      }
      return 0;                                 
    }
    

  • 決定領域は、ソースコード内のブール式と複数の分岐領域を関連付けます。この情報には、式のテストベクトルの実行を表すために必要なビットマップビット数、および式を構成するインストルメント化可能な分岐条件の総数も含まれます。決定領域は、各ブール式についてllvm-covで修正条件/決定カバレッジ(MC/DC)を視覚化するために使用されます。決定領域が使用されると、制御フローIDが関連付けられた各分岐領域に割り当てられます。1つのIDは現在の分岐条件を表し、さらに2つのIDは、それぞれtrueまたはfalseの評価が与えられた場合の制御フローにおける次の分岐条件を表します。これにより、llvm-covは条件の周りの制御フローを再構築して、実行可能なテストベクトルの完全なリストを理解することができます。

ソース範囲:

ソース範囲レコードには、特定のマッピング領域の開始位置と終了位置が含まれています。両方の位置には、行番号と列番号が含まれています。

ファイルID:

ファイルIDは、この領域がどのソースファイルまたはマクロ展開に存在するかを示す整数値です。これにより、Clangは次の例に示すように、マクロ内で定義されたコードのマッピング情報を生成できます。

void func(const char *str) {         // Code Region from 1:28 to 6:2 with file id 0
  #define PUT printf("%s\n", str)    // 2 Code Regions from 2:15 to 2:34 with file ids 1 and 2
  if(*str)                          
    PUT;                             // Expansion Region from 4:5 to 4:8 with file id 0 that expands a macro with file id 1
  PUT;                               // Expansion Region from 5:3 to 5:6 with file id 0 that expands a macro with file id 2
}

カウンタ:

カバレッジマッピングカウンタは、プロファイルインストルメンテーションカウンタへの参照を表すことができます。このようなカウンタを持つ領域の実行回数は、対応するプロファイルインストルメンテーションカウンタの値を調べることで決定されます。

また、カバレッジマッピングカウンタまたは他の式を操作するバイナリ算術式を表すこともできます。式カウンタを持つ領域の実行回数は、式の引数を評価してから、それらを合計するか、互いに減算することで決定されます。次の例では、減算式を使用して、`else`キーワードに続く複合ステートメントの実行回数を計算しています。

int main(int argc, const char *argv[]) {    // Region's counter is a reference to the profile counter #0
                                           
  if (argc > 1) {                           // Region's counter is a reference to the profile counter #1
    printf("%s\n", argv[1]);                
  } else {                                  // Region's counter is an expression (reference to the profile counter #0 - reference to the profile counter #1)
    printf("\n");                        
  }                                        
  return 0;                                
}

最後に、カバレッジマッピングカウンタは、0の実行回数を表すこともできます。ゼロカウンタは、次の例のように、到達不能なステートメントや式のカバレッジマッピングを提供するために使用されます。

int main() {                  
  return 0;                   
  printf("Hello world!\n");    // Unreachable region's counter is zero
}

ゼロカウンタにより、コードカバレッジツールは到達不能な行について適切な行実行回数を表示し、到達不能なコードを強調表示できます。これがないと、ツールはフロントエンドの知識を持たないため、これらの行と領域はまだ実行されたと考えてしまいます。

分岐領域は、ソースコード内の分岐条件を追跡し、2つのカバレッジマッピングカウンタを参照して作成されることに注意してください。1つは分岐条件が「true」と評価された回数を追跡し、もう1つは分岐条件が「false」と評価された回数を追跡します。

LLVM IR表現

カバレッジマッピングデータは、`IPSK_covmap`セクション指定子(つまり、Windowsでは`.lcovmap$M`、それ以外の場合は`__llvm_covmap`)を持つ`__llvm_coverage_mapping`というグローバル定数構造体変数を使用してLLVM IRに格納されます。

例として、CファイルとそのLLVMへのコンパイル方法を考えてみましょう。

int foo() {
  return 42;
}
int bar() {
  return 13;
}

Clangによって生成されたカバレッジマッピング変数には、2つのフィールドがあります。

  • カバレッジマッピングヘッダー。

  • 翻訳単位に存在するファイル名のオプションで圧縮されたリスト。

変数は8バイトアラインメントになっています。これは、ld64が常に異なるオブジェクトファイルからのシンボルを密にパックできるとは限らないためです(ワードレベルのアラインメントの仮定は深く組み込まれています)。

@__llvm_coverage_mapping = internal constant { { i32, i32, i32, i32 }, [32 x i8] }
{
  { i32, i32, i32, i32 } ; Coverage map header
  {
    i32 0,  ; Always 0. In prior versions, the number of affixed function records
    i32 32, ; The length of the string that contains the encoded translation unit filenames
    i32 0,  ; Always 0. In prior versions, the length of the affixed string that contains the encoded coverage mapping data
    i32 3,  ; Coverage mapping format version
  },
 [32 x i8] c"..." ; Encoded data (dissected later)
}, section "__llvm_covmap", align 8

現在のフォーマットのバージョンはバージョン6です。

バージョン6と5の間には1つの違いがあります。

  • ファイル名リストの最初のエントリはコンパイルディレクトリです。ファイル名が相対的な場合、コンパイルディレクトリは相対パスと組み合わされて絶対パスになります。これにより、ファイル名で重複するプレフィックスを省略することでサイズを削減できます。

バージョン5と4の間には1つの違いがあります。

  • 分岐領域の概念が対応する領域の種類とともに導入されました。分岐領域は2つのカウンタをエンコードします。1つは「true」分岐条件が選択された回数を追跡し、もう1つは「false」分岐条件が選択された回数を追跡します。

バージョン4と3の間には2つの違いがあります。

  • 関数レコードは、現在シンボル名で表され、linkonce_odr とマークされています。これにより、リンカは重複する関数レコードをマージできます。重複するダミーレコード(翻訳単位で含まれるが使用されない関数に対して出力される)のマージにより、カバレッジマッピングデータのサイズ肥大化を削減します。この変更の一環として、関数のリージョンマッピング情報は、カバレッジヘッダーに付加されるのではなく、関数レコード内に含まれるようになりました。

  • 翻訳単位のファイル名リストは、オプションでzlib圧縮できます。

バージョン3とバージョン2の唯一の違いは、ギャップ領域を示すために列の終了位置の特別なエンコーディングが導入されたことです。

バージョン1では、foo の関数レコードは次のように定義されていました。

{ i8*, i32, i32, i64 } { i8* getelementptr inbounds ([3 x i8]* @__profn_foo, i32 0, i32 0), ; Function's name
  i32 3, ; Function's name length
  i32 9, ; Function's encoded coverage mapping data string length
  i64 0  ; Function's structural hash
}

バージョン2では、foo の関数レコードは次のように定義されていました。

{ i64, i32, i64 } {
  i64 0x5cf8c24cdb18bdac, ; Function's name MD5
  i32 9, ; Function's encoded coverage mapping data string length
  i64 0  ; Function's structural hash

カバレッジマッピングヘッダー:

上記のように、カバレッジマッピングヘッダーには次のフィールドがあります。

  • カバレッジヘッダーに付加された関数レコードの数。常に0ですが、下位互換性のために存在します。

  • __llvm_coverage_mapping の第3フィールドにある、エンコードされた翻訳単位ファイル名を含む文字列の長さ。

  • __llvm_coverage_mapping の第3フィールドにある、カバレッジヘッダーに付加されたエンコードされたカバレッジマッピングデータを含む文字列の長さ。常に0ですが、下位互換性のために存在します。

  • フォーマットバージョン。現在のバージョンは6です(5としてエンコードされます)。

関数レコード:

関数レコードは、次の型の構造体です。

{ i64, i32, i64, i64, [? x i8] }

関数名のMD5、その関数のエンコードされたマッピングデータの長さ、関数の構造ハッシュ値、関数の翻訳単位内のファイル名のハッシュ、およびエンコードされたマッピングデータが含まれています。

サンプルの解剖:

これは、前に示したカバレッジマッピングサンプルのIRに格納されていたエンコードデータの概要です。

  • IRには、サンプル翻訳単位のエンコードされたカバレッジマッピングデータを表す次の文字列定数が含まれています。

    c"\01\15\1Dx\DA\13\D1\0F-N-*\D6/+\CE\D6/\C9-\D0O\CB\CF\D7K\06\00N+\07]"
    
  • この文字列には、整数格納のために全体で使用されているLEB128形式でエンコードされた値が含まれています。圧縮されたペイロードも含まれています。

  • サンプルの先頭3つのLEB128エンコードされた数値は、ファイル名数、非圧縮ファイル名の長さ、および圧縮ペイロードの長さ(圧縮が無効になっている場合は0)を指定します。このサンプルでは、21バイトの長さ(非圧縮)のファイル名が1つあり、29バイト(圧縮)で格納されています。

  • 最初の関数レコードからのカバレッジマッピングはこの文字列にエンコードされています。

    c"\01\00\00\01\01\01\0C\02\02"
    

    この文字列は、次のバイトで構成されています。

    0x01

    この関数で使用されているファイルIDの数。この関数内のマッピングデータで使用されているファイルIDは1つだけです。

    0x00

    ファイル“/Users/alex/test.c”に対応するファイル名配列のインデックス。

    0x00

    この関数で使用されているカウンタ式の数。この関数は式を使用していません。

    0x01

    関数のファイルID 0の配列に格納されているマッピング領域の数。

    0x01

    この関数の最初の領域のカバレッジマッピングカウンタ。値1は、インデックス0のプロファイル計測カウンタを参照するカバレッジマッピングカウンタであることを示しています。

    0x01

    この関数の最初のマッピング領域の開始行。

    0x0C

    この関数の最初のマッピング領域の開始列。

    0x02

    この関数の最初のマッピング領域の終了行。

    0x02

    この関数の最初のマッピング領域の終了列。

  • 2番目の関数レコードのエンコードされたカバレッジマッピングデータを含む部分文字列の長さも9です。最初の関数レコードのマッピングデータと同じ構造になっています。

  • 末尾の2バイトはゼロであり、8バイトアライメントにするためにカバレッジマッピングデータをパディングするために使用されます。

エンコーディング

関数ごとのカバレッジマッピングデータは、単純な構造を持つバイトストリームとしてエンコードされます。この構造は、(可変長の符号なし整数など)を使用してファイルIDマッピングカウンタ式、およびマッピング領域をエンコードするために使用されます。

構造のフォーマットは次のとおりです。

[ファイル ID マッピング、 カウンタ 式、 マッピング 領域]

翻訳単位のファイル名は、関数ごとのカバレッジマッピングデータと同じエンコーディングを使用して、次の構造でエンコードされます。

[numFilenames : LEB128、 filename0 : 文字列、 filename1 : 文字列、 ...]

このセクションでは、エンコーディングフォーマットで使用される基本的な型について説明します。これらは、[foo : type]の説明で:の後に表示されます。

LEB128

LEB128は、DWARFのLEB128エンコーディングを使用してエンコードされた符号なし整数値であり、値が小さい場合(128未満の値の場合は1バイト)に最適化されています。

文字列

[長さ : LEB128、 文字...]

文字列値は、文字列の長さに対するLEB値と、その文字のバイトシーケンスでエンコードされます。

ファイルIDマッピング

[numIndices : LEB128、 filenameIndex0 : LEB128、 filenameIndex1 : LEB128、 ...]

関数のカバレッジマッピングストリーム内のファイルIDマッピングには、翻訳単位のファイル名配列へのインデックスが含まれています。

カウンタ

[値 : LEB128]

カバレッジマッピングカウンタは、単一のLEB値に格納されます。これは、最下位2ビットに格納されているタグと、残りのビットに格納されているカウンタデータの2つの要素で構成されます。

タグ:

カウンタのタグは、カウンタの種類と、カウンタが式である場合はその式の種類をエンコードします。可能なタグ値は次のとおりです。

  • 0 - カウンタはゼロです。

  • 1 - カウンタはプロファイル計測カウンタへの参照です。

  • 2 - カウンタは減算式です。

  • 3 - カウンタは加算式です。

データ:

カウンタのデータは、次のように解釈されます。

  • カウンタがプロファイル計測カウンタへの参照である場合、カウンタのデータはプロファイルカウンタのIDです。

  • カウンタが式である場合、カウンタのデータはカウンタ式の配列へのインデックスです。

カウンタ式

[numExpressions : LEB128、 expr0LHS : LEB128、 expr0RHS : LEB128、 expr1LHS : LEB128、 expr1RHS : LEB128、 ...]

カウンタ式は2つのカウンタで構成され、2項算術演算を表します。式の種別は、この式を参照するカウンタのタグから決定されます。

マッピング領域

[numRegionArrays : LEB128、 regionsForFile0、 regionsForFile1、 ...]

マッピング領域は、特定のサブ配列内のすべての領域が同じファイルIDを持つサブ配列の配列に格納されます。

領域のサブ配列のファイルIDは、メイン配列におけるそのサブ配列のインデックスです。例:最初のサブ配列のファイルIDは0になります。

領域のサブ配列

[numRegions : LEB128、 region0、 region1、 ...]

特定のファイルIDのマッピング領域は、領域の開始位置で昇順にソートされた配列に格納されます。

マッピング領域

[ヘッダー、 ソース 範囲]

マッピング領域レコードには、カウンタや領域の種類を格納するヘッダーと、この領域の開始位置と終了位置を含むソース範囲の2つのサブレコードが含まれています。

ソース範囲

[deltaLineStart : LEB128, columnStart : LEB128, numLines : LEB128, columnEnd : LEB128]

ソース範囲レコードには、次のフィールドが含まれています。

  • deltaLineStart:現在のマッピング領域の開始行と、前のマッピング領域の開始行との差。

    現在のマッピング領域が現在のサブ配列の最初の領域である場合、その領域の開始行が格納されます。

  • columnStart:マッピング領域の開始列。

  • numLines:現在のマッピング領域の終了行と開始行との差。

  • columnEnd:マッピング領域の終了列。最上位ビットが設定されている場合、現在のマッピング領域はギャップ領域です。ギャップ領域のカウントは、行に他の領域がない場合にのみ、行実行カウントとして使用されます。

テスト形式

警告

このセクションは、llvm-covに取り組んでいるLLVM開発者のみを対象としています。

llvm-covは、テスト目的で特別なファイル形式(以下では.covmappingと呼びます)を使用します。この形式は非公開であり、一般ユーザーには使用しないでください。llvm-covconvert-for-testingサブコマンドを使用すると、開発者はこのようなファイルを取得できます。

.covmappingファイルの構造は次のとおりです。

[magicNumber : u64, version : u64, profileNames, coverageMapping, coverageRecords]

マジックナンバーとバージョン

マジックナンバーは0x6d766f636d766c6cで、リトルエンディアンのASCII文字列llvmcovmです。

現時点では2つのバージョンがあります。

  • バージョン1は0x6174616474736574(ASCII文字列testdata)としてエンコードされます。

  • バージョン2は1としてエンコードされます。

バージョン1とバージョン2の唯一の違いは、coverageMappingフィールドのエンコーディングであり、後ほど説明します。

プロファイル名

profileNamescoverageMappingcoverageRecordsは、元のバイナリファイルから抽出された3つのセクションです。

profileNamesは、セクションのサイズ、アドレス、および生データをエンコードします。

[profileNamesSize : LEB128, profileNamesAddr : LEB128, profileNamesData : bytes]

カバレッジマッピング

このフィールドは、8バイトアラインメントにするためにゼロバイトでパディングされます。

coverageMappingには、ソースファイルのレコードが含まれています。バージョン1では、1つのレコードのみが格納されます。

[padding : bytes, coverageMappingData : bytes]

バージョン2では、coverageMappingDataのサイズをデータの前にLEB128数値としてエンコードすることで、この制限が緩和されています。

[coverageMappingSize : LEB128, padding : bytes, coverageMappingData : bytes]

現在のバージョンは2です。

カバレッジレコード

このフィールドは、8バイトアラインメントにするためにゼロバイトでパディングされます。

coverageRecordsは次のようにエンコードされます。

[padding : bytes, coverageRecordsData : bytes]

ファイル内の残りのデータはcoverageRecordsDataとみなされます。