デバッグ情報機能

目次 #

はじめに #

デバッグ情報を含むネイティブ実行ファイルを作成するには、アプリケーションをコンパイルする際に`javac`に`-g`コマンドラインオプションを指定し、次に`native-image`ビルダに指定します。

javac -g Hello.java
native-image -g Hello

これにより、ソースレベルのデバッグが可能になり、デバッガ(GDB)はマシン命令をJavaファイルの特定のソース行と関連付けます。結果の 이미지 は、GNUデバッガ(GDB)が理解できる形式のデバッグレコードを含みます。さらに、ビルダに`-O0`を渡すことで、コンパイラの最適化を実行しないように指定できます。すべての最適化を無効にする必要はありませんが、一般的にデバッグエクスペリエンスが向上します。

デバッグ情報は、デバッガだけでなく、Linuxのパフォーマンスプロファイリングツール`perf`や`valgrind`でも、CPU使用率やキャッシュミスなどの実行統計を特定のJavaメソッドと関連付け、元のJavaソースファイルの個々のコード行にリンクするために使用できます。

デフォルトでは、デバッグ情報はパラメータとローカル変数の値の一部のみの詳細を含みます。これは、デバッガが多くのパラメータとローカル変数を未定義として報告することを意味します。ビルダに`-O0`を渡すと、完全なデバッグ情報が含まれます。より高いレベルの最適化(`-O1`またはデフォルトの`-O2`)を使用する場合に、より多くのパラメータとローカル変数の情報を含めるには、`native-image`コマンドに追加のコマンドラインフラグを渡す必要があります。

native-image -g -H:+SourceLevelDebug Hello

フラグ`-g`でdebuginfoを有効にしても、生成されたネイティブイメージのコンパイル方法に違いはなく、実行速度や実行時のメモリ使用量には影響しません。ただし、生成されるイメージのディスク上のサイズが大幅に増加する可能性があります。フラグ`-H:+SourceLevelDebug`を渡すことによって完全なパラメータとローカル変数の情報を有効にすると、プログラムのコンパイル方法がわずかに異なり、アプリケーションによっては実行速度が低下する可能性があります。

各Javaメソッドの実行時間の割合を示すヒストグラムを表示する基本的な`perf report`コマンドでは、`native-image`コマンドにフラグ`-g`と`-H:+SourceLevelDebug`を渡すだけで済みます。ただし、`perf`のより高度な使用方法(たとえば、`perf annotate`)や`valgrind`の使用には、コンパイルされたJavaメソッドを識別するリンケージシンボルでデバッグ情報を補足する必要があります。Javaメソッドシンボルは、デフォルトでは生成されたネイティブイメージから省略されますが、`native-image`コマンドに1つの追加フラグを渡すことで保持できます。

native-image -g -H:+SourceLevelDebug -H:-DeleteLocalSymbols Hello

このフラグを使用すると、結果のイメージファイルのサイズがわずかに増加します。

注:ネイティブイメージのデバッグは現在、macOSの初期サポートを含むLinuxで動作します。この機能は実験的なものです。

注:Linuxでの`perf`と`valgrind`のデバッグ情報のサポートは実験的な機能です。

ソースファイルキャッシング #

`-g`オプションは、ネイティブ実行ファイルの生成時に見つけることができるJDKランタイムクラス、GraalVMクラス、およびアプリケーションクラスのソースのキャッシングも有効にします。デフォルトでは、キャッシュは生成されたバイナリの横に`sources`という名前のサブディレクトリに作成されます。`-H:Path=...`オプションを使用してネイティブ実行ファイルのターゲットディレクトリが指定されている場合、キャッシュも同じターゲットの下に再配置されます。コマンドラインオプションを使用して、`sources`への代替パスを提供し、デバッガのソースファイル検索パスルートを設定します。キャッシュ内のファイルは、ネイティブ実行ファイルのデバッグレコードに含まれるファイルパス情報と一致するディレクトリ階層にあります。ソースキャッシュには、生成されたバイナリをデバッグするために必要なすべてのファイルが含まれており、それ以外は何も含まれていません。このローカルキャッシュは、ネイティブ実行ファイルをデバッグするときに、必要なソースだけをデバッガまたはIDEで使用できるようにする便利な方法を提供します。

実装は、ソースファイルの場所を特定する際にスマートになるように努めています。JDKランタイムソースを検索する際には、現在の`JAVA_HOME`を使用してJDK src.zipを見つけます。また、クラスパスエントリを使用して、GraalVMソースファイルとアプリケーションソースファイルの場所を提案します(ソースの場所を特定するために使用されるスキームの正確な詳細については、以下を参照してください)。ただし、ソースレイアウトはさまざまであり、すべてのソースを見つけることができない場合があります。したがって、ユーザーは`DebugInfoSourceSearchPath`オプションを使用して、コマンドラインでソースファイルの場所を明示的に指定できます。

javac --source-path apps/greeter/src \
    -d apps/greeter/classes org/my/greeter/*Greeter.java
javac -cp apps/greeter/classes \
    --source-path apps/hello/src \
    -d apps/hello/classes org/my/hello/Hello.java
native-image -g \
    -H:DebugInfoSourceSearchPath=apps/hello/src \
    -H:DebugInfoSourceSearchPath=apps/greeter/src \
    -cp apps/hello/classes:apps/greeter/classes org.my.hello.Hello

`DebugInfoSourceSearchPath`オプションは、すべてのターゲットソースの場所を通知するために必要な回数だけ繰り返すことができます。このオプションに渡される値は、絶対パスまたは相対パスのいずれかです。ディレクトリ、ソースJARファイル、またはソースZIPファイルのいずれかを識別できます。カンマ区切り文字を使用して、一度に複数のソースルートを指定することもできます。

native-image -g \
    -H:DebugInfoSourceSearchPath=apps/hello/target/hello-sources.jar,apps/greeter/target/greeter-sources.jar \
    -cp apps/target/hello.jar:apps/target/greeter.jar \
    org.my.Hello

デフォルトでは、アプリケーション、GraalVM、およびJDKソースのキャッシュは`sources`という名前のディレクトリに作成されます。`DebugInfoSourceCacheRoot`オプションを使用して、代替パスを指定できます。これは絶対パスまたは相対パスです。後者の場合、パスは`-H:Path`オプション(デフォルトは現在の作業ディレクトリ)で指定された生成された実行ファイルのターゲットディレクトリを基準として解釈されます。例として、前のコマンドの次のバリアントは、現在のプロセス`id`を使用して構築された絶対一時ディレクトリパスを指定します。

SOURCE_CACHE_ROOT=/tmp/$$/sources
native-image -g \
    -H:DebugInfoSourceCacheRoot=$SOURCE_CACHE_ROOT \
    -H:DebugInfoSourceSearchPath=apps/hello/target/hello-sources.jar,apps/greeter/target/greeter-sources.jar \
    -cp apps/target/hello.jar:apps/target/greeter.jar \
    org.my.Hello

結果のキャッシュディレクトリは`/tmp/1272696/sources`のようになります。

ソースキャッシュパスにまだ存在しないディレクトリが含まれている場合、キャッシュの作成中に作成されます。

上記のすべての例では、`DebugInfoSourceSearchPath`オプションは実際には冗長であることに注意してください。最初のケースでは、_apps/hello/classes/_と_apps/greeter/classes/_のクラスパスエントリを使用して、デフォルトの検索ルート_apps/hello/src/_と_apps/greeter/src/_が導出されます。2番目のケースでは、_apps/target/hello.jar_と_apps/target/greeter.jar_のクラスパスエントリを使用して、デフォルトの検索ルート_apps/target/hello-sources.jar_と_apps/target/greeter-sources.jar_が導出されます。

サポートされている機能 #

現在サポートされている機能は次のとおりです。

  • ファイルと行、またはメソッド名で設定されたブレークポイント
  • 関数呼び出しへのステップインとステップオーバーを含む、行ごとのシングルステップ
  • スタックバックトレース(インラインコードの詳細を含むフレームは含まれません)
  • プリミティブ値の出力
  • Javaオブジェクトの構造化(フィールドごと)出力
  • さまざまなレベルの汎用性でのオブジェクトのキャスト/出力
  • パス式によるオブジェクトネットワークへのアクセス
  • メソッドと静的フィールドデータへの名前による参照
  • パラメータとローカル変数にバインドされた値への名前による参照
  • クラス定数への名前による参照

コンパイルされたメソッド内のシングルステップには、インライン化されたGraalVMメソッドを含む、インラインコードのファイルと行番号情報が含まれていることに注意してください。そのため、GDBは同じコンパイル済みメソッド内にいてもファイルを 切り替える 場合があります。

GDBからJavaをデバッグする際の特別な考慮事項 #

GDBは現在、Javaデバッグのサポートを含んでいません。結果として、Javaプログラムを同等のC ++プログラムとしてモデル化するデバッグ情報を生成することにより、デバッグ機能が実装されました。Javaクラス、配列、およびインターフェースの参照は、実際には関連するフィールド/配列データを含むレコードへのポインタです。対応するC ++モデルでは、Java名が基になるC ++(クラス/構造体)レイアウトタイプにラベルを付け、Java参照はポインタとして表示されます。

そのため、たとえば、DWARFデバッグ情報モデルでは、`java.lang.String`はC ++クラスを識別します。このクラスレイアウトタイプは、`int`型の`hash`や`byte []`型の`value`などの予期されるフィールド、および`String(byte []`)、`charAt(int)`などのメソッドを宣言します。ただし、Javaに`String(String)`として表示されるコピーコンストラクタは、`gdb`に`String(java.lang.String *)`シグネチャで表示されます。

C ++レイアウトクラスは、C ++パブリック継承を使用して、クラス(レイアウト)タイプ`java.lang.Object`からフィールドとメソッドを継承します。後者は、最大2つのフィールド(VM構成に応じて)を含む`_objhdr`という名前の特別な構造体クラスから標準のoop(通常のオブジェクトポインタ)ヘッダーフィールドを継承します。最初のフィールドは`hub`と呼ばれ、そのタイプは`java.lang.Class *`です。つまり、オブジェクトのクラスへのポインタです。2番目のフィールド(オプション)は`idHash`と呼ばれ、`int`型です。オブジェクトのIDハッシュコードを格納します。

`ptype`コマンドを使用して、特定のタイプの詳細を出力できます。埋め込まれた`.`文字をエスケープするために、Javaタイプの名前を引用符で囲む必要があることに注意してください。

(gdb) ptype 'java.lang.String'
type = class java.lang.String : public java.lang.Object {
  private:
    byte [] *value;
    int hash;
    byte coder;

  public:
    void String(byte [] *);
    void String(char [] *);
    void String(byte [] *, java.lang.String *);
    . . .
    char charAt(int);
    . . .
    java.lang.String * concat(java.lang.String *);
    . . .
}

`ptype`コマンドを使用して、Javaデータ値の静的タイプを識別することもできます。現在の exemplesession は、単純なhello worldプログラム用です。Mainメソッド`Hello.main`には、Javaタイプが`String []`である単一のパラメータ`args`が渡されます。デバッガが`main`のエントリで停止している場合、`ptype`を使用して`args`のタイプを出力できます。

(gdb) ptype args
type = class java.lang.String[] : public java.lang.Object {
  public:
    int len;
    java.lang.String *data[0];
} *

ここで強調する価値のある詳細がいくつかあります。まず、デバッガはJava配列参照をポインタ型として認識します。これはすべてのJavaオブジェクト参照と同様です。

次に、ポインタは構造体、実際にはC ++クラスを指し、整数長のフィールドと、配列オブジェクトをモデル化するメモリブロックに埋め込まれたC ++配列型のデータフィールドを使用してJava配列のレイアウトをモデル化します。

配列データフィールドの要素は基本タイプへの参照です。この場合は`java.lang.String`へのポインタです。データ配列の名目上の長さは0です。ただし、`String []`オブジェクトに割り当てられたメモリブロックには、実際にはフィールド`len`の値によって決定されるポインタ数を保持するのに十分なスペースが含まれています。

最後に、C ++クラス`java.lang.String []`はC ++クラス`java.lang.Object`から継承することに注意してください。したがって、配列は依然としてオブジェクトです。特に、オブジェクトの内容を出力するときにわかるように、これはすべての配列にすべてのJavaオブジェクトが共有するオブジェクトヘッダーフィールドも含まれていることを意味します。

`print`コマンドを使用して、オブジェクト参照をメモリアドレスとして表示できます。

(gdb) print args
$1 = (java.lang.String[] *) 0x7ffff7c01130

また、オブジェクトの内容をフィールドごとに表示するためにも使用できます。これは、`*`演算子を使用してポインタを逆参照することで実現されます。

(gdb) print *args
$2 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0xaa90f0,
      idHash = 0
    }, <No data fields>}, 
  members of java.lang.String[]:
  len = 1,
  data = 0x7ffff7c01140
}

配列オブジェクトは、親クラス Object を介して _objhdr クラスから継承された埋め込みフィールドを含みます。 _objhdr は、すべてのオブジェクトの先頭に存在するフィールドをモデル化するためにデバッグ情報に追加された合成型です。これらには、オブジェクトのクラスへの参照である hub と、一意の数値ハッシュコードである hashId が含まれます。

明らかに、デバッガはローカル変数 args の型 (java.lang.String[]) とメモリ内の位置 (0x7ffff7c010b8) を認識しています。また、参照されるオブジェクトに埋め込まれたフィールドのレイアウトについても認識しています。これは、デバッガコマンドで C++ の . 演算子と -> 演算子を使用して、基になるオブジェクトデータ構造をトラバースできることを意味します。

(gdb) print args->data[0]
$3 = (java.lang.String *) 0x7ffff7c01160
(gdb) print *args->data[0]
$4 = {
   <java.lang.Object> = {
     <_objhdr> = {
      hub = 0xaa3350
     }, <No data fields>},
   members of java.lang.String:
   value = 0x7ffff7c01180,
   hash = 0,
   coder = 0 '\000'
 }
(gdb) print *args->data[0]->value
$5 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0xaa3068,
      idHash = 0
    }, <No data fields>}, 
  members of byte []:
  len = 6,
  data = 0x7ffff7c01190 "Andrew"
}

オブジェクトヘッダーの hub フィールドに戻ると、前述のように、これは実際にはオブジェクトのクラスへの参照です。これは実際には Java 型 java.lang.Class のインスタンスです。 gdb は、基になる C++ クラス (レイアウト) 型へのポインタを使用してフィールドの型を指定することに注意してください。

(gdb) print args->hub
$6 = (java.lang.Class *) 0xaa90f0

Object から下位方向のすべてのクラスは、共通の自動生成ヘッダー型 _objhdr を継承します。 hub フィールドを含むのはこのヘッダー型です。

(gdb) ptype _objhdr
type = struct _objhdr {
    java.lang.Class *hub;
    int idHash;
}

(gdb) ptype 'java.lang.Object'
type = class java.lang.Object : public _objhdr {
  public:
    void Object(void);
    . . .

すべてのオブジェクトにクラスを指す共通ヘッダーがあるため、アドレスがオブジェクト参照であるかどうか、そしてもしそうならオブジェクトのクラスが何かを判断するための簡単なテストを実行できます。有効なオブジェクト参照があれば、常に hub の名前フィールドから参照される String の内容を出力できます。

結果として、デバッガによって観測されるすべてのオブジェクトをその動的型にダウンキャストできるようになります。つまり、デバッガが (例えば) java.nio.file.Path の静的型しか認識していない場合でも、jdk.nio.zipfs.ZipPath などのサブタイプである動的型に簡単にダウンキャストできるため、静的型だけでは観測できないフィールドを検査できます。最初に値はオブジェクト参照にキャストされます。次に、パス式を使用して、hub フィールドと hub の名前フィールドを介して、名前 String にある byte[] 値配列を逆参照します。

(gdb) print/x ((_objhdr *)$rdi)
$7 = (_objhdr *) 0x7ffff7c01130
(gdb) print *$7->hub->name->value
$8 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0xaa3068,
      idHash = 178613527
    }, <No data fields>}, 
   members of byte []:
   len = 19,
  data = 0x8779c8 "[Ljava.lang.String;"
 }

レジスタ rdi の値は、明らかに String 配列への参照です。実際、これは偶然ではありません。サンプルセッションは Hello.main のエントリに配置されたブレークポイントで停止し、その時点で String[] パラメータ args の値はレジスタ rdi に配置されます。振り返ってみると、rdi の値はコマンド print args によって出力された値と同じであることがわかります。

hub オブジェクトの名前だけを出力できる、より単純なコマンドは次のとおりです。

(gdb) x/s $7->hub->name->value->data
798:	"[Ljava.lang.String;"

実際、任意の生のメモリアドレスでこの操作を実行する gdb コマンド hubname_raw を定義すると便利です。

define hubname_raw
  x/s (('java.lang.Object' *)($arg0))->hub->name->value->data
end

(gdb) hubname_raw $rdi
0x8779c8:	"[Ljava.lang.String;"

無効な参照に対してハブ名を出力しようとすると、安全に失敗し、エラーメッセージが出力されます。

(gdb) p/x $rdx
$5 = 0x2
(gdb) hubname $rdx
Cannot access memory at address 0x2

gdb が既に参照の Java 型を認識している場合、hubname コマンドのより単純なバージョンを使用して、キャストせずにそれを出力できます。たとえば、上記で $1 として取得した String 配列には、既知の型があります。

(gdb) ptype $1
type = class java.lang.String[] : public java.lang.Object {
    int len;
    java.lang.String *data[0];
} *

define hubname
  x/s (($arg0))->hub->name->value->data
end

(gdb) hubname $1
0x8779c8:	"[Ljava.lang.String;"

ネイティブイメージヒープには、イメージに含まれるすべての Java 型に対して一意のハブオブジェクト (java.lang.Class のインスタンス) が含まれています。標準の Java クラスリテラル構文を使用して、これらのクラス定数を参照できます。

(gdb) print 'Hello.class'
$6 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0xaabd00,
      idHash = 1589947226
    }, <No data fields>}, 
  members of java.lang.Class:
  typeCheckStart = 13,
  name = 0xbd57f0,
  ...

残念ながら、埋め込まれた . 文字をフィールドアクセスとして gdb が解釈しないように、クラス定数リテラルを引用符で囲む必要があります。

クラス定数リテラルの型は、java.lang.Class * ではなく java.lang.Class であることに注意してください。

クラス定数は、Java インスタンスクラス、インターフェース、配列クラス、およびプリミティブ配列を含む配列に存在します。

(gdb)  print 'java.util.List.class'.name
$7 = (java.lang.String *) 0xb1f698
(gdb) print 'java.lang.String[].class'.name->value->data
$8 = 0x8e6d78 "[Ljava.lang.String;"
(gdb) print 'long.class'.name->value->data
$9 = 0xc87b78 "long"
(gdb) x/s  'byte[].class'.name->value->data
0x925a00:	"[B"
(gdb) 

インターフェースレイアウトは、C++ 共用体型としてモデル化されます。共用体のメンバーには、インターフェースを実装するすべての Java クラスの C++ レイアウト型が含まれます。

(gdb) ptype 'java.lang.CharSequence'
type = union java.lang.CharSequence {
    java.nio.CharBuffer _java.nio.CharBuffer;
    java.lang.AbstractStringBuilder _java.lang.AbstractStringBuilder;
    java.lang.String _java.lang.String;
    java.lang.StringBuilder _java.lang.StringBuilder;
    java.lang.StringBuffer _java.lang.StringBuffer;
}

インターフェースに型指定された参照がある場合、関連する共用体要素を通して表示することにより、関連するクラス型に解決できます。

args 配列の最初の String を使用すると、gdb にインターフェース CharSequence にキャストするように要求できます。

(gdb) print args->data[0]
$10 = (java.lang.String *) 0x7ffff7c01160
(gdb) print ('java.lang.CharSequence' *)$10
$11 = (java.lang.CharSequence *) 0x7ffff7c01160

hub フィールドを含むのは共用体要素のオブジェクトだけなので、hubname コマンドはこの共用体型では機能しません。

(gdb) hubname $11
There is no member named hub.

ただし、すべての要素に同じヘッダーが含まれているため、実際の型を識別するために、それらのいずれかを hubname に渡すことができます。これにより、正しい共用体要素を選択できます。

(gdb) hubname $11->'_java.nio.CharBuffer'
0x95cc58:	"java.lang.String`\302\236"
(gdb) print $11->'_java.lang.String'
$12 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0xaa3350,
      idHash = 0
    }, <No data fields>},
  members of java.lang.String:
  hash = 0,
  value = 0x7ffff7c01180,
  coder = 0 '\000'
}

出力された hub のクラス名に、末尾の文字が含まれていることに注意してください。これは、Java 文字列テキストを格納するデータ配列がゼロ終端されるとは限らないためです。

デバッガは、ローカル変数とパラメータ変数の名前と型だけを理解するわけではありません。メソッド名と静的フィールド名についても認識しています。

次のコマンドは、クラス Hello のメインエントリポイントにブレークポイントを配置します。 GDB はこれを C++ メソッドと見なしているため、:: セパレータを使用してメソッド名をクラス名から分離することに注意してください。

(gdb) info func ::main
All functions matching regular expression "::main":

File Hello.java:
	void Hello::main(java.lang.String[] *);
(gdb) x/4i Hello::main
=> 0x4065a0 <Hello::main(java.lang.String[] *)>:	sub    $0x8,%rsp
   0x4065a4 <Hello::main(java.lang.String[] *)+4>:	cmp    0x8(%r15),%rsp
   0x4065a8 <Hello::main(java.lang.String[] *)+8>:	jbe    0x4065fd <Hello::main(java.lang.String[] *)+93>
   0x4065ae <Hello::main(java.lang.String[] *)+14>:	callq  0x406050 <Hello$Greeter::greeter(java.lang.String[] *)>
(gdb) b Hello::main
Breakpoint 1 at 0x4065a0: file Hello.java, line 43.

オブジェクトデータを含む静的フィールドの例は、クラス BigInteger の静的フィールド powerCache によって提供されます。

(gdb) ptype 'java.math.BigInteger'
type = class _java.math.BigInteger : public _java.lang.Number {
  public:
    int [] mag;
    int signum;
  private:
    int bitLengthPlusOne;
    int lowestSetBitPlusTwo;
    int firstNonzeroIntNumPlusTwo;
    static java.math.BigInteger[][] powerCache;
    . . .
  public:
    void BigInteger(byte [] *);
    void BigInteger(java.lang.String *, int);
    . . .
}
(gdb) info var powerCache
All variables matching regular expression "powerCache":

File java/math/BigInteger.java:
	java.math.BigInteger[][] *java.math.BigInteger::powerCache;

静的変数名を使用して、このフィールドに格納されている値を参照できます。また、アドレス演算子を使用して、ヒープ内のフィールドの位置 (アドレス) を識別することもできます。

(gdb) p 'java.math.BigInteger'::powerCache
$13 = (java.math.BigInteger[][] *) 0xced5f8
(gdb) p &'java.math.BigInteger'::powerCache
$14 = (java.math.BigInteger[][] **) 0xced3f0

デバッガは、静的フィールドのシンボリック名を通じて逆参照して、フィールドに格納されているプリミティブ値またはオブジェクトにアクセスします。

(gdb) p *'java.math.BigInteger'::powerCache
$15 = {
  <java.lang.Object> = {
    <_objhdr> = {
    hub = 0xb8dc70,
    idHash = 1669655018
    }, <No data fields>},
  members of _java.math.BigInteger[][]:
  len = 37,
  data = 0xced608
}
(gdb) p 'java.math.BigInteger'::powerCache->data[0]@4
$16 = {0x0, 0x0, 0xed5780, 0xed5768}
(gdb) p *'java.math.BigInteger'::powerCache->data[2]
$17 = {
  <java.lang.Object> = {
    <_objhdr> = {
    hub = 0xabea50,
    idHash = 289329064
    }, <No data fields>},
  members of java.math.BigInteger[]:
  len = 1,
  data = 0xed5790
}
(gdb) p *'java.math.BigInteger'::powerCache->data[2]->data[0]
$18 = {
  <java.lang.Number> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0xabed80
      }, <No data fields>}, <No data fields>},
  members of java.math.BigInteger:
  mag = 0xcbc648,
  signum = 1,
  bitLengthPlusOne = 0,
  lowestSetBitPlusTwo = 0,
  firstNonzeroIntNumPlusTwo = 0
}

ソースコードの場所の特定 #

実装の目標の 1 つは、プログラムの実行中に停止したときに関連するソースファイルを識別できるように、デバッガを簡単に構成できるようにすることです。 native-image ツールは、適切に構造化されたファイルキャッシュに関連するソースを蓄積することによって、これを達成しようとします。

native-image ツールは、JDK ランタイムクラス、GraalVM クラス、およびアプリケーションソースクラスのソースファイルをローカルソースキャッシュに含めるために、異なる戦略を使用して検索します。クラスのパッケージ名に基づいて、使用する戦略を識別します。そのため、たとえば、java.* または jdk.* で始まるパッケージは JDK クラスです。 org.graal.* または com.oracle.svm.* で始まるパッケージは GraalVM クラスです。他のパッケージはすべてアプリケーションクラスと見なされます。

JDK ランタイムクラスのソースは、ネイティブイメージ生成プロセスを実行するために使用される JDK リリースにある *src.zip* から取得されます。取得されたファイルは、サブディレクトリ *sources* の下にキャッシュされ、関連付けられたクラスのモジュール名 (JDK11 の場合) とパッケージ名を使用して、ソースが配置されるディレクトリ階層を定義します。

たとえば、Linux では、class java.util.HashMap のソースはファイル *sources/java.base/java/util/HashMap.java* にキャッシュされます。このクラスとそのメソッドのデバッグ情報レコードは、相対ディレクトリパス *java.base/java/util* とファイル名 *HashMap.java* を使用して、このソースファイルを識別します。 Windows では、ファイルセパレータとして / ではなく \ を使用することを除いて、同じです。

GraalVM クラスのソースは、クラスパスにあるエントリから派生した ZIP ファイルまたはソースディレクトリから取得されます。取得されたファイルは、サブディレクトリ *sources* の下にキャッシュされ、関連付けられたクラスのパッケージ名を使用して、ソースが配置されるディレクトリ階層を定義します (たとえば、クラス com.oracle.svm.core.VM のソースファイルは sources/com/oracle/svm/core/VM.java にキャッシュされます)。

キャッシュされた GraalVM ソースのルックスキームは、各クラスパスエントリで見つかった内容によって異なります。 * /path/to/foo.jar* のような JAR ファイルエントリが指定されている場合、対応するファイル */path/to/foo.src.zip* は、ソースファイルを抽出できる候補 ZIP ファイルシステムと見なされます。エントリが */path/to/bar* のようなディレクトリを指定する場合、ディレクトリ */path/to/bar/src* と */path/to/bar/src_gen* が候補と見なされます。 ZIP ファイルまたはソースディレクトリが存在しない場合、または予想される GraalVM パッケージ階層のいずれかと一致するサブディレクトリ階層が少なくとも 1 つ含まれていない場合、候補はスキップされます。

アプリケーションクラスのソースは、クラスパスにあるエントリから派生したソース JAR ファイルまたはソースディレクトリから取得されます。取得されたファイルは、サブディレクトリ *sources* の下にキャッシュされ、関連付けられたクラスのパッケージ名を使用して、ソースが配置されるディレクトリ階層を定義します (たとえば、クラス `org.my.foo.Foo` のソースファイルは *sources/org/my/foo/Foo.java* としてキャッシュされます)。

キャッシュされたアプリケーションソースのルックスキームは、各クラスパスエントリで見つかった内容によって異なります。*/path/to/foo.jar* のような JAR ファイルエントリが指定されている場合、対応する JAR ファイル */path/to/foo-sources.jar* は、ソースファイルを抽出できる候補 ZIP ファイルシステムと見なされます。エントリが */path/to/bar/classes/* または */path/to/bar/target/classes/* のようなディレクトリを指定する場合、ディレクトリ */path/to/bar/src/main/java/*、*/path/to/bar/src/java/*、または */path/to/bar/src/* のいずれかが候補として選択されます (この優先順位で)。最後に、ネイティブ実行可能ファイルが実行されている現在のディレクトリも候補と見なされます。

これらのルックアップ戦略は暫定的なものであり、将来的に拡張が必要になる場合があります。ただし、不足しているソースを他の方法で利用できるようにすることは可能です。 1 つのオプションは、追加のアプリソース JAR ファイルを解凍するか、追加のアプリソースツリーをキャッシュにコピーすることです。もう 1 つは、追加のソース検索パスを構成することです。

GNU デバッガでのソースパスの構成 #

デフォルトでは、GDB はローカルディレクトリルート `sources` を使用して、アプリケーションクラス、GraalVM クラス、および JDK ランタイムクラスのソースファイルを検索します。ソースキャッシュが GDB を実行するディレクトリにない場合は、次のコマンドを使用して必要なパスを構成できます。

(gdb) set directories /path/to/sources/

set directories コマンドへの引数は、ソースキャッシュの場所を絶対パスまたは `gdb` セッションの作業ディレクトリからの相対パスとして識別する必要があります。

現在の実装では、*jdk.graal.compiler** パッケージサブスペースにある GraalVM JIT コンパイラの一部のソースがまだ見つからないことに注意してください。

アプリケーションソース JAR ファイルを解凍するか、アプリケーションソースツリーをキャッシュにコピーすることにより、`sources` にキャッシュされたファイルを補うことができます。 `sources` に追加する新しいサブディレクトリは、ソースが含まれているクラスのトップレベルパッケージに対応していることを確認する必要があります。

検索パスにディレクトリを追加するには、set directories コマンドを使用します。

(gdb) set directories /path/to/my/sources/:/path/to/my/other/sources

GNU デバッガは ZIP 形式のファイルシステムを認識しないため、追加するエントリは、関連するソースを含むディレクトリツリーを指定する必要があります。繰り返しますが、検索パスに追加されたディレクトリのトップレベルエントリは、ソースが含まれているクラスのトップレベルパッケージに対応している必要があります。

Linux でのデバッグ情報の確認 #

これは、デバッグ情報の実装方法を理解したい場合、またはデバッグ中に発生したデバッグ情報のエンコーディングに関連する問題のトラブルシューティングを行いたい場合にのみ関係します。

objdump コマンドを使用して、ネイティブ実行ファイルに埋め込まれたデバッグ情報を表示できます。次のコマンド(すべてターゲットバイナリが hello と呼ばれていると想定)を使用して、生成されたすべてのコンテンツを表示できます。

objdump --dwarf=info hello > info
objdump --dwarf=abbrev hello > abbrev
objdump --dwarf=ranges hello > ranges
objdump --dwarf=decodedline hello > decodedline
objdump --dwarf=rawline hello > rawline
objdump --dwarf=str hello > str
objdump --dwarf=loc hello > loc
objdump --dwarf=frames hello > frames

info セクションには、コンパイルされたすべての Java メソッドの詳細が含まれています。

abbrev セクションは、Java ファイル(コンパイル単位)とメソッドを記述する info セクションのレコードのレイアウトを定義します。

ranges セクションは、メソッドコードセグメントの開始アドレスと終了アドレスを詳細に示します。

decodedline セクションは、メソッドコード範囲セグメントのサブセグメントをファイルと行番号にマッピングします。このマッピングには、インライン化されたメソッドのファイルと行番号のエントリが含まれます。

rawline セグメントは、ファイル、行、およびアドレスの遷移をエンコードする DWARF ステートマシン命令を使用して、行テーブルがどのように生成されるかの詳細を提供します。

loc セクションは、info セクションで宣言されたパラメータとローカル変数が決定値を持つことがわかっているアドレス範囲の詳細を提供します。詳細は、値がマシンレジスタ、スタック、またはメモリ内の特定のアドレスのどこに配置されているかを識別します。

str セクションは、info セクションのレコードから参照される文字列のルックアップテーブルを提供します。

frames セクションは、コンパイルされたメソッド内の遷移ポイントをリストします。遷移ポイントでは、(固定サイズの)スタックフレームがプッシュまたはポップされ、デバッガは各フレームの現在および以前のスタックポインタとそのリターンアドレスを識別できます。

デバッグレコードに埋め込まれているコンテンツの一部は、C コンパイラによって生成され、ライブラリ内にあるコード、または Java メソッドコードにバンドルされている C lib ブートストラップコードに属することに注意してください。

現在サポートされているターゲット #

プロトタイプは現在、Linux 上の GNU デバッガに対してのみ実装されています。

  • Linux/x86_64 のサポートはテスト済みであり、正しく動作するはずです。

  • Linux/AArch64 のサポートは存在しますが、まだ完全には検証されていません(ブレークポイントは正常に機能するはずですが、スタックバックトレースは正しくない可能性があります)。

Windows のサポートはまだ開発中です。

アイソレートを使用したデバッグ #

ネイティブイメージでのアイソレートの使用は、通常のオブジェクトポインタ(oops)のエンコード方法に影響します。つまり、デバッグ情報ジェネレータは、エンコードされた oop をオブジェクトデータが格納されているメモリ内のアドレスに変換する方法に関する情報を gdb に提供する必要があります。そのため、gdb にエンコードされた oop とデコードされた生のアドレスを処理するように依頼する場合、注意が必要になることがあります。

アイソレートが無効になっている場合、oops は基本的にオブジェクトの内容を直接指す生のアドレスになります。これは一般的に、oop が静的/インスタンスフィールドに埋め込まれているか、レジスタに配置されているかスタックに保存されているローカル変数またはパラメータ変数から参照されているかに関係なく同じです。一部の oop の下位 3 ビットを使用して、オブジェクトの一時的なプロパティを記録する「タグ」を保持する場合があるため、それほど単純ではありません。ただし、gdb に提供されるデバッグ情報により、アドレスとして oop を逆参照する前に、これらのタグビットが削除されます。

アイソレートを使用すると、静的フィールドまたはインスタンスフィールドに格納されている oops 参照は、実際には直接アドレスではなく、専用のヒープベースレジスタ(x86_64 では r14、AArch64 では r29)からの相対アドレス、オフセットになります(まれに、オフセットにもいくつかの低いタグビットが設定されている場合があります)。この種の「間接」oop が実行中にロードされると、ほとんどの場合、すぐにオフセットをヒープベースレジスタ値に追加することにより「生の」アドレスに変換されます。そのため、ローカル変数またはパラメータ変数の値として発生する oops は、実際には生のアドレスです。

一部のオペレーティングシステムでは、アイソレートを有効にすると、gdb リリースバージョン 10 以前を使用する場合にオブジェクトの印刷に問題が発生することに注意してください。デバッガを新しいバージョンにアップグレードすることを強くお勧めします。

アイソレートが有効になっている場合、イメージにエンコードされた DWARF 情報は、基になるオブジェクトデータにアクセスするために逆参照しようとするときはいつでも、間接 oop をリベースするように gdb に指示します。これは通常自動的かつ透過的ですが、オブジェクトの型を要求したときに gdb が表示する基になる型モデルに表示されます。

たとえば、上記で遭遇した静的フィールドを考えてみましょう。アイソレートを使用するイメージでその型を印刷すると、この静的フィールドの型が予期された型とは異なることがわかります。

(gdb) ptype 'java.math.BigInteger'::powerCache
type = class _z_.java.math.BigInteger[][] : public java.math.BigInteger[][] {
} *

フィールドは _z_.java.math.BigInteger[][] として型指定されます。これは、予期される型 java.math.BigInteger[][] から継承する空のラッパークラスです。このラッパー型は基本的に元の型と同じですが、それを定義する DWARF 情報レコードには、gdb にこの型へのポインタを変換する方法を指示する情報が含まれています。

gdb がこのフィールドに格納されている oop を印刷するように求められると、それが生のアドレスではなくオフセットであることが明らかになります。

(gdb) p/x 'java.math.BigInteger'::powerCache
$1 = 0x286c08
(gdb) x/x 0x286c08
0x286c08:	Cannot access memory at address 0x286c08

ただし、gdb がフィールドを介して逆参照するように求められると、必要なアドレス変換を oop に適用し、正しいデータを取得します。

(gdb) p/x *'java.math.BigInteger'::powerCache
$2 = {
  <java.math.BigInteger[][]> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0x1ec0e2,
        idHash = 0x2f462321
      }, <No data fields>},
    members of java.math.BigInteger[][]:
    len = 0x25,
    data = 0x7ffff7a86c18
  }, <No data fields>}

hub フィールドまたはデータ配列の型を印刷すると、それらも間接型を使用してモデル化されていることがわかります。

(gdb) ptype $1->hub
type = class _z_.java.lang.Class : public java.lang.Class {
} *
(gdb) ptype $2->data
type = class _z_.java.math.BigInteger[] : public java.math.BigInteger[] {
} *[0]

デバッガは、これらの oop を逆参照する方法をまだ知っています。

(gdb) p $1->hub
$3 = (_z_.java.lang.Class *) 0x1ec0e2
(gdb) x/x $1->hub
0x1ec0e2:	Cannot access memory at address 0x1ec0e2
(gdb) p *$1->hub
$4 = {
  <java.lang.Class> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0x1dc860,
        idHash = 1530752816
      }, <No data fields>},
    members of java.lang.Class:
    name = 0x171af8,
    . . .
  }, <No data fields>}

間接型は対応する raw 型から継承するため、raw 型ポインタを識別する式が機能するほとんどすべての場合において、間接型ポインタを識別する式を使用できます。注意が必要になる唯一のケースは、表示された数値フィールド値または表示されたレジスタ値をキャストする場合です。

たとえば、上記で印刷された間接 hub oop が hubname_raw に渡されると、そのコマンド内部の型 Object へのキャストは、必要な間接 oops 変換を強制できません。結果としてメモリ アクセスは失敗します。

(gdb) hubname_raw 0x1dc860
Cannot access memory at address 0x1dc860

この場合、引数を間接ポインタ型にキャストするわずかに異なるコマンドを使用する必要があります。

(gdb) define hubname_indirect
 x/s (('_z_.java.lang.Object' *)($arg0))->hub->name->value->data
end
(gdb) hubname_indirect 0x1dc860
0x7ffff78a52f0:	"java.lang.Class"

デバッグヘルパーメソッド #

デバッグ情報が完全にサポートされていないプラットフォーム、または複雑な問題をデバッグする場合、ネイティブイメージ実行状態に関する高レベル情報を印刷またはクエリすると役立つことがあります。このようなシナリオの場合、ネイティブイメージは、ビルド時オプション -H:+IncludeDebugHelperMethods を指定することでネイティブ実行ファイルに埋め込むことができるデバッグヘルパーメソッドを提供します。デバッグ中は、通常の C メソッドと同様に、これらのデバッグヘルパーメソッドを呼び出すことができます。この機能は、ほとんどすべてのデバッガと互換性があります。

gdb でデバッグしている間、次のコマンドを使用して、ネイティブイメージに埋め込まれているすべてのデバッグヘルパーメソッドをリストできます。

(gdb) info functions svm_dbg_

メソッドを呼び出す前に、Java クラス DebugHelper のソースコードを直接調べて、各メソッドが予期する引数を判断することをお勧めします。たとえば、以下のメソッドを呼び出すと、致命的なエラーに対して出力される内容と同様の、ネイティブイメージ実行状態に関する高レベル情報が出力されます。

(gdb) call svm_dbg_print_fatalErrorDiagnostics($r15, $rsp, $rip)

perf および valgrind を使用する場合の特別な考慮事項 #

デバッグ情報には、トップレベルおよびインライン化されたコンパイル済みメソッドコードのアドレス範囲の詳細、およびコードアドレスから対応するソースファイルと行へのマッピングが含まれています。 perfvalgrind は、この情報を記録およびレポート操作の一部に使用できます。たとえば、perf report は、perf record セッション中にサンプリングされたコードアドレスを Java メソッドに関連付け、出力ヒストグラムにメソッドの DWARF 派生メソッド名を出力できます。

    . . .
    68.18%     0.00%  dirtest          dirtest               [.] _start
            |
            ---_start
               __libc_start_main_alias_2 (inlined)
               |          
               |--65.21%--__libc_start_call_main
               |          com.oracle.svm.core.code.IsolateEnterStub::JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b (inlined)
               |          com.oracle.svm.core.JavaMainWrapper::run (inlined)
               |          |          
               |          |--55.84%--com.oracle.svm.core.JavaMainWrapper::runCore (inlined)
               |          |          com.oracle.svm.core.JavaMainWrapper::runCore0 (inlined)
               |          |          |          
               |          |          |--55.25%--DirTest::main (inlined)
               |          |          |          |          
               |          |          |           --54.91%--DirTest::listAll (inlined)
               . . .

残念ながら、他の操作では、コンパイルされたメソッドコードの開始を特定する ELF(ローカル)関数シンボルテーブルエントリによって Java メソッドを識別する必要があります。特に、両方のツールによって提供されるアセンブリコードダンプは、最も近いシンボルからのオフセットを使用して分岐ターゲットと呼び出しターゲットを識別します。Java メソッドシンボルを省略すると、オフセットは一般に、無関係なグローバルシンボル(通常は C コードによる呼び出しのためにエクスポートされたメソッドのエントリポイント)を基準にして表示されます。

問題の例として、perf annotate から抜粋した次の出力は、メソッド java.lang.String::String() のコンパイル済みコードの最初のいくつかの注釈付き命令を表示しています。

    . . .
         : 501    java.lang.String::String():
         : 521    public String(byte[] bytes, int offset, int length, Charset charset) {
    0.00 :   519d50: sub    $0x68,%rsp
    0.00 :   519d54: mov    %rdi,0x38(%rsp)
    0.00 :   519d59: mov    %rsi,0x30(%rsp)
    0.00 :   519d5e: mov    %edx,0x64(%rsp)
    0.00 :   519d62: mov    %ecx,0x60(%rsp)
    0.00 :   519d66: mov    %r8,0x28(%rsp)
    0.00 :   519d6b: cmp    0x8(%r15),%rsp
    0.00 :   519d6f: jbe    51ae1a <graal_vm_locator_symbol+0xe26ba>
    0.00 :   519d75: nop
    0.00 :   519d76: nop
         : 522    Objects.requireNonNull(charset);
    0.00 :   519d77: nop
         : 524    java.util.Objects::requireNonNull():
         : 207    if (obj == null)
    0.00 :   519d78: nop
    0.00 :   519d79: nop
         : 209    return obj;
    . . .

左端の列は、perf record 実行中に取得されたサンプルの各命令で記録された時間の割合を示しています。各命令の前に、プログラムのコードセクション内のアドレスが付けられています。逆アセンブリは、コードが派生したソース行をインターリーブします。トップレベルコードの場合は 521〜524、Objects.requireNonNull() からインライン化されたコードの場合は 207〜209 です。また、メソッドの開始には、DWARF デバッグ情報で定義されている名前 java.lang.String::String() のラベルが付けられています。ただし、アドレス 0x519d6f の分岐命令 jbe は、graal_vm_locator_symbol から非常に大きなオフセットを使用しています。出力されたオフセットは、シンボルの位置に対する正しいアドレスを識別します。ただし、これでは、ターゲットアドレスが実際にはメソッド String::String() のコンパイル済みコード範囲内にあること、つまりこれがメソッドローカル分岐であることが明確になりません。

オプション -H-DeleteLocalSymbolsnative-image コマンドに渡されると、ツールの出力の可読性が大幅に向上します。このオプションを有効にした場合の同等の perf annotate 出力は次のとおりです。

    . . .
         : 5      000000000051aac0 <String_constructor_f60263d569497f1facccd5467ef60532e990f75d>:
         : 6      java.lang.String::String():
         : 521    *          {@code offset} is greater than {@code bytes.length - length}
         : 522    *
         : 523    * @since  1.6
         : 524    */
         : 525    @SuppressWarnings("removal")
         : 526    public String(byte[] bytes, int offset, int length, Charset charset) {
    0.00 :   51aac0: sub    $0x68,%rsp
    0.00 :   51aac4: mov    %rdi,0x38(%rsp)
    0.00 :   51aac9: mov    %rsi,0x30(%rsp)
    0.00 :   51aace: mov    %edx,0x64(%rsp)
    0.00 :   51aad2: mov    %ecx,0x60(%rsp)
    0.00 :   51aad6: mov    %r8,0x28(%rsp)
    0.00 :   51aadb: cmp    0x8(%r15),%rsp
    0.00 :   51aadf: jbe    51bbc1 <String_constructor_f60263d569497f1facccd5467ef60532e990f75d+0x1101>
    0.00 :   51aae5: nop
    0.00 :   51aae6: nop
         : 522    Objects.requireNonNull(charset);
    0.00 :   51aae7: nop
         : 524    java.util.Objects::requireNonNull():
         : 207    * @param <T> the type of the reference
         : 208    * @return {@code obj} if not {@code null}
         : 209    * @throws NullPointerException if {@code obj} is {@code null}
         : 210    */
         : 211    public static <T> T requireNonNull(T obj) {
         : 212    if (obj == null)
    0.00 :   51aae8: nop
    0.00 :   51aae9: nop
         : 209    throw new NullPointerException();
         : 210    return obj;
    . . .

このバージョンでは、メソッドの開始アドレスに、マングルされたシンボル名 String_constructor_f60263d569497f1facccd5467ef60532e990f75d と DWARF 名のラベルが付けられています。分岐ターゲットは、その開始シンボルからのオフセットを使用して出力されるようになりました。

残念ながら、perfvalgrind は、GraalVM で使用されているマングルアルゴリズムを正しく理解しておらず、シンボルと DWARF 関数データの両方が同じアドレスで開始するコードを識別することがわかっている場合でも、逆アセンブリでマングルされた名前を DWARF 名に置き換えることができません。そのため、分岐命令はまだシンボルとオフセットを使用してターゲットを出力しますが、少なくとも今回はメソッドシンボルを使用しています。

また、アドレス 51aac0 はメソッドの開始として認識されるようになったため、perf はメソッドの最初の行の前に 5 つのコンテキスト行を配置しました。これは、メソッドの javadoc コメントの末尾をリストしています。残念ながら、perf はこれらの行に誤って番号を付けており、最初のコメントに 516 ではなく 521 のラベルを付けています。

perf annotate コマンドを実行すると、イメージ内のすべてのメソッドと C 関数の逆アセンブリリストが表示されます。 perf annotate コマンドに引数としてメソッド名を渡すことで、特定のメソッドに注釈を付けることができます。ただし、perf は DWARF 名ではなく、マングルされたシンボル名を引数として必要とすることに注意してください。そのため、java.lang.String::String() メソッドに注釈を付けるには、perf annotate String_constructor_f60263d569497f1facccd5467ef60532e990f75d コマンドを実行する必要があります。

valgrind ツールの callgrind も、高品質の出力を提供するためにローカルシンボルを保持する必要があります。 callgrindkcachegrind のようなビューアと組み合わせて使用​​すると、ネイティブイメージの実行に関する貴重な情報を特定し、特定のソースコード行に関連付けることができます。

perf record を使用したコールグラフの記録 #

通常、perf がスタックフレームの記録を行う場合 (--call-graph が使用される場合)、個々のスタックフレームを認識するためにフレームポインタを使用します。これは、プロファイルされる実行可能ファイルが、関数が呼び出されるたびにフレームポインタを実際に保持することを前提としています。ネイティブイメージの場合、これはイメージビルド引数として -H:+PreserveFramePointer を使用することで実現できます。

代替ソリューションは、perf に dwarf デバッグ情報 (具体的には debug_frame データ) を使用させてスタックフレームのアンワインドを支援することです。これが機能するには、イメージを -g (デバッグ情報を生成するため) でビルドし、perf record で引数 --call-graph dwarf を使用して、スタックのアンワインドに dwarf デバッグ情報 (フレームポインタの代わりに) が使用されるようにする必要があります。

お問い合わせ