デバッグ情報機能
生成されたネイティブ・イメージにデバッグ情報を追加するには、native-image
ビルダーに-g
オプションを指定します:
native-image -g Hello
-g
フラグは、デバッグ情報を生成するようにnative-image
に指示します。結果のイメージには、GNUデバッガ(GDB)が認識する形式のデバッグ・レコードが含まれます。また、-O0
をビルダーに渡して、コンパイラの最適化を実行しないことを指定できます。すべての最適化の無効化は必須ではありませんが、一般的にはデバッグ・エクスペリエンスが向上します。
デバッグ情報は、デバッガだけの役に立つものではありません。また、Linuxパフォーマンス・プロファイリング・ツールperf
およびvalgrind
で使用して、CPU使用率やキャッシュ失敗数などの実行統計を特定の名前のJavaメソッドに関連付けることができ、元のJavaソース・ファイル内の個々のJavaコード行にリンクすることさえ可能です。
デフォルトでは、デバッグ情報にはパラメータおよびローカル変数の値の詳細のみが含まれます。つまり、デバッガは多くのパラメータおよびローカル変数を未定義として報告します。-O0
をビルダーに渡すと、完全なデバッグ情報が含まれます。より高いレベルの最適化(-O1
またはデフォルトの-O2
)を採用するときに、さらに多くのパラメータおよびローカル変数情報を含める場合は、追加のコマンドライン・フラグをnative-image
コマンドに渡す必要があります
native-image -g -H:+SourceLevelDebug Hello
フラグ-g
を使用してデバッグ情報を有効にしても、生成されたネイティブ・イメージのコンパイル方法に違いはありません。また、実行される速度や実行時に使用されるメモリー量には影響しません。ただし、ディスク上の生成されたイメージのサイズを大幅に増やすことができます。フラグ-H:+SourceLevelDebug
を渡すことによってパラメータおよびローカル変数情報を完全に有効にすると、プログラムがわずかに異なる方法でコンパイルされ、一部のアプリケーションでは実行速度が低下する可能性があります。
基本的なperf report
コマンドでは、各Javaメソッドでの実行時間の割合を示すヒストグラムが表示されますが、必要なのは-g
および-H:+SourceLevelDebug
のフラグをnative-image
コマンドに渡すことだけです。ただし、perf
(すなわちperf annotate
)やvalgrind
のより高度な使用方法では、コンパイルされたJavaメソッドを識別するリンケージ・シンボルでデバッグ情報をを補足する必要があります。Javaメソッドのシンボルは、デフォルトでは生成されるネイティブ・イメージから省かれますが、native-image
コマンドに1つの追加フラグを渡すことで保持できます
native-image -g -H:+SourceLevelDebug -H:-DeleteLocalSymbols Hello
このフラグを使用すると、生成されるイメージ・ファイルのサイズがわずかに増加します。
ノート: ネイティブ・イメージのデバッグは現在、macOSの初期サポートでLinuxで機能します。この機能は試験段階です。
ノート: Linuxでの
perf
およびvalgrind
のデバッグ情報のサポートは、実験的な機能です。
目次
- ソース・ファイルのキャッシュ
- GDBからJavaをデバッグする場合の特別な考慮事項
- ソース・コードの場所の識別
- GNUデバッガでのソース・パスの構成
- Linuxでのデバッグ情報のチェック
- 分離を使用したデバッグ
- デバッグ・ヘルパー・メソッド
- perfおよびvalgrindの使用に関する特別な考慮事項
ソース・ファイルのキャッシュ
-g
オプションを使用すると、ネイティブ実行可能ファイルの生成時に特定できるすべてのJDKランタイム・クラス、GraalVMクラスおよびアプリケーション・クラスのソースのキャッシュも有効になります。デフォルトでは、キャッシュは、生成されたバイナリとともにsources
という名前のサブディレクトリに作成されます。オプション-H:Path=...
を使用してネイティブ実行可能ファイルのターゲット・ディレクトリを指定すると、キャッシュもその同じターゲットの下に再配置されます。コマンドライン・オプションを使用して、sources
への代替パスを指定し、デバッガのソース・ファイルの検索パス・ルートを構成します。キャッシュ内のファイルは、ネイティブ実行可能ファイルのデバッグ・レコードに含まれるファイル・パス情報と一致するディレクトリ階層に配置されます。ソース・キャッシュには、生成されるバイナリのデバッグに必要なすべてのファイルが含められ、それ以外は含められません。このローカル・キャッシュは、ネイティブ実行可能ファイルをデバッグするときに、必要なソースのみをデバッガまたはIDEで使用できるようにするために便利です。
実装によって、ソース・ファイルをスマートに特定することが試みられます。JDKランタイム・ソースを検索するとき、JDK src.zipを特定するために、現在のJAVA_HOME
が使用されます。また、クラスパスのエントリに基づいて、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:-SpawnIsolates \
-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:-SpawnIsolates \
-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
オプションは実際には冗長です。1つ目のケースでは、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++モデルでは、基礎となるC++ (クラス/構造体)レイアウト型のラベル付けにJava名が使用され、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つのフィールドを含む_objhdr
という特別な構造体クラスから標準のoop (通常のオブジェクト・ポインタ)ヘッダー・フィールドを継承します。最初のフィールドはhub
と呼ばれ、その型はjava.lang.Class *
です。つまり、これはオブジェクトのクラスへのポインタです。2番目のフィールドはidHash
と呼ばれ、型はint
です。オブジェクトのアイデンティティ・ハッシュコードを格納します。
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データ値の静的タイプの識別にも使用できます。現在のセッション例は、単純なhello worldプログラム用です。メイン・メソッド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
オブジェクトの下位にあるクラスはいずれも共通の自動生成ヘッダー型_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
内の値は、明らかに文字列配列への参照です。確かに、これは偶然ではありません。例のセッションは、Hello.main
のエントリに配置されたブレーク・ポイントで停止し、その時点で、String[]
パラメータargs
の値がレジスタrdi
に配置されます。後ろを見ると、rdi
の値がコマンドprint args
で出力された値と同じであることがわかります。
hub
オブジェクトの名前のみを出力できる単純なコマンドを次に示します:
(gdb) x/s $7->hub->name->value->data
798: "[Ljava.lang.String;"
実際には、gdb
コマンドhubname_raw
を定義して、任意の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
として取得された文字列配列の型は既知です。
(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;"
インタフェース・レイアウトは、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;
}
インタフェースに型指定された参照が指定された場合は、関連する共用体要素を介して表示することで、関連するクラス型に解決できます。
引数配列の最初の文字列を取得する場合は、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では、クラス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ファイルを解凍するか、追加のアプリケーション・ソース・ツリーをキャッシュにコピーすることです。別の方法は、追加のソース検索パスを構成することです。
GNUデバッガでのソース・パスの構成
GDBでは、デフォルトでは、ローカル・ディレクトリ・ルートsources
を使用して、アプリケーション・クラス、GraalVMクラスおよびJDKランタイム・クラスのソース・ファイルが検索されます。GDBを実行するディレクトリにソース・キャッシュがない場合は、次のコマンドを使用して必要なパスを構成できます:
(gdb) set directories /path/to/sources/
set directoryコマンドの引数は、ソース・キャッシュの場所をgdb
セッションの作業ディレクトリの絶対パスまたは相対パスとして識別する必要があります。
現在の実装では、org.graalvm.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ファイル(コンパイル・ユニット)およびメソッドを記述する情報セクションのレコードのレイアウトが定義されています。
rangesセクションには、メソッド・コード・セグメントの開始アドレスと終了アドレスの詳細が示されます。
decodedlineセクションでは、メソッド・コード範囲セグメントのサブフラグメントがファイルおよび行番号にマップされています。このマッピングには、インライン化されたメソッドのファイルおよび行番号のエントリが含まれています。
rawlineセグメントには、ファイル、行およびアドレス遷移をエンコードするDWARF状態マシン命令を使用して、行の表がどのように生成されたかに関する詳細が示されます。
locセクションには、情報セクションで宣言されたパラメータ変数とローカル変数が決定値を持つことがわかっているアドレス範囲の詳細が表示されます。詳細は、値がマシン・レジスタ、スタック、またはメモリ内の特定のアドレスのどこにあるかを特定します。
strセクションには、infoセクションのレコードから参照される文字列のルックアップ表が示されます。
framesセクションには、(固定サイズの)スタック・フレームがプッシュまたはポップされるコンパイル済メソッド内の遷移ポイントがリストされます。これにより、デバッガは、各フレームの現在および以前のスタック・ポインタとその戻りアドレスを識別できます。
デバッグ・レコードに埋め込まれている内容の一部はCコンパイラによって生成され、Javaメソッド・コードにバンドルされているライブラリまたはC libブートストラップ・コードのいずれかにあるコードに属していることに注意してください。
現在サポートされているターゲット
プロトタイプは、現在、Linux上のGNUデバッガに対してのみ実装されています:
-
Linux/x86_64サポートはテスト済であり、正しく動作します
-
Linux/AArch64サポートは存在しますが、まだ完全には検証されていません(ブレーク・ポイントは問題なく機能しますが、スタック・バックトレースが正しくない可能性があります)
Windowsサポートはまだ開発中です。
分離を使用したデバッグ
native-image
ビルダーにコマンドライン・オプション-H:-SpawnIsolates
を渡すことによって、分離の使用を有効にすると、通常のオブジェクト・ポインタ(oops)がエンコードされる方法に影響します。つまり、デバッグ情報ジェネレータは、エンコードされたoopをオブジェクト・データが格納されているメモリー内のアドレスに変換する方法に関する情報をgdb
に渡す必要があります。このため、エンコードされたoopとデコードされたrawアドレスの処理をgdb
に対して要求するときに注意が必要になる場合があります。
分離が無効になっている場合、oopは、基本的に、オブジェクトの内容を直接指すrawアドレスになります。このことは、通常、oopが静的/インスタンス・フィールドに埋め込まれているか、レジスタにあるローカル変数またはパラメータ変数から参照されているか、スタックに保存されているかに関係なく、同じです。一部のoopの下位3ビットはオブジェクトの特定の一時プロパティを記録するタグの保持に使用できるため、これはそれほど単純なことではありません。ただし、gdb
にデバッグ情報が渡されるということは、oopをアドレスとして間接参照する前に、これらのタグ・ビットが削除されることを意味します。
一方、分離が有効になっている場合、静的またはインスタンス・フィールドに格納されているoops参照は、実際には直接アドレスではなく、専用のヒープ・ベース・レジスタ(x86_64ではr14、AArch64ではr29)からのオフセットを示す相対アドレスになります(いくつかの特別なケースでは、オフセットにタグの下位ビットも設定されることがあります)。この種の間接oopが実行中にロードされると、ほとんどの場合、ヒープ・ベース・レジスタ値にオフセットを追加することによって、常に即座にrawアドレスに変換されます。そのため、ローカル変数またはパラメータ変数の値として発生するoopは実際にはrawアドレスになります。
分離を有効にする一部のオペレーティング・システムでは、
gdb
リリース・バージョン10以前の使用時にオブジェクトの出力で問題が発生することに注意してください。オペレーティング・システムにこれらの以前のリリースのいずれかが含まれている場合は、デバッグ情報の生成時にコマンドライン・オプション-H:-SpawnIsolates
を渡すことで、分離の使用を無効にすることをお薦めします。または、デバッガを新しいバージョンにアップグレードすることもできます。
gdb
では、分離が有効になっている場合にイメージにエンコードされるDWARF情報に従って、間接的なoopがリベースされます。これは、基礎となるオブジェクト・データにアクセスするためにoopの間接参照が試行されるたびに行われます。通常、これは自動的かつ透過的ですが、オブジェクトの型を要求したときに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を出力するよう求められた場合は、それがrawアドレスではなくオフセットであることは明白です。
(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の使用に関する特別な考慮事項
デバッグ情報に含まれるのは、コンパイル済メソッドの最上位コードとインライン化コードのアドレス範囲の詳細と、コード・アドレスと対応するソース・ファイル/行のマッピングです。perf
とvalgrind
は、記録処理やレポート処理においてこの情報を使用できます。たとえば、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-DeleteLocalSymbols
をnative-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名の両方でラベル付けされています。ここでは、ブランチ・ターゲットは、その開始シンボルからのオフセットを使用して出力されます。
残念ながら、perf
およびvalgrind
は、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
でも、高品質の出力を提供するためにローカル・シンボルを保持する必要があります。callgrind
をkcachegrind
などのビューアと組み合せて使用すると、ネイティブ・イメージの実行に関する非常に多くの貴重な情報を識別して、それらを特定のソース・コードに関連付けることができます。