- JDK 23用 GraalVM (最新)
- JDK 24用 GraalVM (早期アクセス)
- JDK 21用 GraalVM
- JDK 17用 GraalVM
- アーカイブ
- 開発ビルド
デバッグ情報機能
目次 #
- はじめに
- ソースファイルキャッシング
- GDBからJavaをデバッグする際の特別な考慮事項
- ソースコードの場所の特定
- GNUデバッガでのソースパスの設定
- Linuxでのデバッグ情報の確認
- Isolateを使ったデバッグ
- デバッグヘルパーメソッド
- perfとvalgrindを使用する際の特別な考慮事項
はじめに #
デバッグ情報を含むネイティブ実行ファイルを作成するには、アプリケーションをコンパイルする際に`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 を使用する場合の特別な考慮事項 #
デバッグ情報には、トップレベルおよびインライン化されたコンパイル済みメソッドコードのアドレス範囲の詳細、およびコードアドレスから対応するソースファイルと行へのマッピングが含まれています。 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
のようなビューアと組み合わせて使用すると、ネイティブイメージの実行に関する貴重な情報を特定し、特定のソースコード行に関連付けることができます。
perf record
を使用したコールグラフの記録 #
通常、perf がスタックフレームの記録を行う場合 (--call-graph
が使用される場合)、個々のスタックフレームを認識するためにフレームポインタを使用します。これは、プロファイルされる実行可能ファイルが、関数が呼び出されるたびにフレームポインタを実際に保持することを前提としています。ネイティブイメージの場合、これはイメージビルド引数として -H:+PreserveFramePointer
を使用することで実現できます。
代替ソリューションは、perf に dwarf デバッグ情報 (具体的には debug_frame データ) を使用させてスタックフレームのアンワインドを支援することです。これが機能するには、イメージを -g
(デバッグ情報を生成するため) でビルドし、perf record
で引数 --call-graph dwarf
を使用して、スタックのアンワインドに dwarf デバッグ情報 (フレームポインタの代わりに) が使用されるようにする必要があります。