デバッグ情報機能
生成されたネイティブ・イメージにデバッグ情報を追加するには、native-image
ビルダーに-g
オプションを指定します:
native-image -g Hello
-g
フラグは、デバッグ情報を生成するようにnative-image
に指示します。結果のイメージには、GNUデバッガ(GDB)が認識する形式のデバッグ・レコードが含まれます。また、-O0
をビルダーに渡して、コンパイラの最適化を実行しないことを指定できます。すべての最適化の無効化は必須ではありませんが、一般的にはデバッグ・エクスペリエンスが向上します。
ノート: ネイティブ・イメージのデバッグは現在、macOSの初期サポートでLinuxで機能します。この機能は試験段階です。
ソース・ファイルのキャッシュ
GenerateDebugInfo
オプションを使用すると、ネイティブ・イメージの生成中に特定できるすべての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
からフィールドおよびメソッドを継承します。さらに、後者は、_objhdr
という特別な構造体クラスから標準のoop(通常のオブジェクト・ポインタ)ヘッダー・フィールドを継承します。この構造体クラスには、型がjava.lang.Class *
,のhub
という単一フィールドが含まれています。つまり、これはオブジェクトのクラスへのポインタです。
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 *);
. . .
}
print
コマンドを使用すると、参照オブジェクトの内容をフィールドごとに出力できます。キャストを使用してRAWメモリー・アドレスを特定のJava型の参照に変換する方法に注目してください。
(gdb) print *('java.lang.String' *) 0x7ffff7c01060
$1 = {
<java.lang.Object> = {
<_objhdr> = {
hub = 0x90cb58
}, <No data fields>},
members of java.lang.String:
value = 0x7ffff7c011a0,
hash = 0,
coder = 0 '\000'
}
オブジェクト・ヘッダーのhub
フィールドは、実際にはJava型java.lang.Class
の参照です。gdb
でのフィールドの型指定は、基礎となるC++クラス(レイアウト)型へのポインタを使用して行われます。
オブジェクトの下位にあるクラスはいずれも共通の自動生成ヘッダー型_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
フィールドおよびhub
の名前フィールドを介して、名前String
にあるbyte[]
値配列を間接参照します。
(gdb) print/x ((_objhdr *)$rdi)
$2 = 0x7ffff7c01028
(gdb) print *$2->hub->name->value
$3 = {
<java.lang.Object> = {
<_objhdr> = {
hub = 0x942d40,
idHash = 1806863149
}, <No data fields>},
members of byte []:
len = 19,
data = 0x923a90 "[Ljava.lang.String;"
}
レジスタrdx
内の値は、明らかに文字列配列への参照です。この型にキャストすると、長さが1であることがわかります。
(gdb) print *('java.lang.String[]' *)$rdi
$4 = {
<java.lang.Object> = {
<_objhdr> = {
hub = 0x925be8,
idHash = 0
}, <No data fields>},
members of java.lang.String[]:
len = 1,
data = 0x7ffff7c01038
}
hub
オブジェクトの名前のみを出力できる単純なコマンドを次に示します:
(gdb) x/s $2->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
0x904798: "[Ljava.lang.String;"
無効な参照のハブ名を出力しようとすると、フェイル・セーフが適用され、エラー・メッセージが出力されます。
(gdb) p/x $rdx
$5 = 0x2
(gdb) hubname $rdx
Cannot access memory at address 0x2
配列型のレイアウトは、C++クラス型としてモデル化されます。これはクラスObjectを継承するため、_objhdr
で定義されたハブおよびidHashヘッダー・フィールドが含まれています。さらに、Java配列の要素型(プリミティブ値またはオブジェクト参照)から型指定される要素を持つ長さフィールドおよび埋込み(C++)データ配列が追加されます。
(gdb) ptype 'java.lang.String[]'
type = class java.lang.String[] : public java.lang.Object {
int len;
java.lang.String *data[0];
}
埋込み配列のサイズは、通常、長さが0です。ただし、Java配列インスタンスが割り当てられると、データ配列に長さフィールドに定義された数の項目を格納できるだけの十分な領域が確保されます。
この場合、データ配列に格納される値の型はjava.lang.String *
になります。C++配列には、C++モデルに関するかぎり、アドレスなどのJavaオブジェクト参照が格納されます。
gdb
によってすでに参照用のJava型が認識されている場合は、より単純なバージョンのhubnameコマンドを使用してキャストなしで出力できます。たとえば、上で$4
として取得された文字列配列の型は既知です。
(gdb) ptype $4
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 $4
0x923b68: "[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 (('java.lang.String[]' *)$rdi)->data[0]
$5 = (java.lang.String *) 0x7ffff7c01060
(gdb) print ('java.lang.CharSequence' *)$5
$6 = (java.lang.CharSequence *) 0x7ffff7c01060
この共用体型はhub
フィールドを含む共用体要素のオブジェクトにすぎないため、hubname
コマンドは機能しません:
(gdb) hubname $6
There is no member named hub.
ただし、すべての要素に同じヘッダーが含まれているため、実際の型を識別するために、いずれかの要素をhubnameに渡すことができます。これにより、適切な共用体要素を選択できます:
(gdb) hubname $6->'_java.nio.CharBuffer'
0x7d96d8: "java.lang.String\270", <incomplete sequence \344\220>
(gdb) print $6->'_java.lang.String'
$18 = {
<java.lang.Object> = {
<_objhdr> = {
hub = 0x90cb58
}, <No data fields>},
members of java.lang.String:
value = 0x7ffff7c011a0,
hash = 0,
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
$8 = (java.math.BigInteger[][] *) 0xa6fd98
(gdb) p &'java.math.BigInteger'::powerCache
$9 = (java.math.BigInteger[][] **) 0xa6fbd8
デバッガでは、シンボリック名を介して静的フィールドを間接参照し、フィールドに格納されているプリミティブ値またはオブジェクトにアクセスします。
(gdb) p *'java.math.BigInteger'::powerCache
$10 = {
<java.lang.Object> = {
<_objhdr> = {
hub = 0x9ab3d0,
idHash = 489620191
}, <No data fields>},
members of _java.math.BigInteger[][]:
len = 37,
data = 0xa6fda8
}
(gdb) p 'java.math.BigInteger'::powerCache->data[0]@4
$11 = {0x0, 0x0, 0xc09378, 0xc09360}
(gdb) p *'java.math.BigInteger'::powerCache->data[2]
$12 = {
<java.lang.Object> = {
<_objhdr> = {
hub = 0x919898,
idHash = 1796421813
}, <No data fields>},
members of java.math.BigInteger[]:
len = 1,
data = 0xc09388
}
(gdb) p *'java.math.BigInteger'::powerCache->data[2]->data[0]
$14 = {
<java.lang.Number> = {
<java.lang.Object> = {
<_objhdr> = {
hub = 0x919bc8
}, <No data fields>}, <No data fields>},
members of java.math.BigInteger:
mag = 0xa5b030,
signum = 1,
bitLengthPlusOne = 0,
lowestSetBitPlusTwo = 0,
firstNonzeroIntNumPlusTwo = 0
}
ソース・コードの場所の識別
実装の目的の1つは、デバッガの構成を簡易化して、プログラムの実行中にデバッガが停止しても、関連するソース・ファイルを識別できるようにすることです。ネイティブ・イメージ・ビルダーでは、これを実現するために、適切に構造化されたファイル・キャッシュ内に関連するソースが蓄積されていきます。
ネイティブ・イメージ・ビルダーは、ローカル・ソース・キャッシュに含める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=frames hello > frames
infoセクションには、すべてのコンパイル済Javaメソッドの詳細が含まれています。
abbrevセクションには、Javaファイル(コンパイル・ユニット)およびメソッドを記述する情報セクションのレコードのレイアウトが定義されています。
rangesセクションには、メソッド・コード・セグメントの開始アドレスと終了アドレスの詳細が示されます。
decodedlineセクションでは、メソッド・コード範囲セグメントのサブフラグメントがファイルおよび行番号にマップされています。このマッピングには、インライン化されたメソッドのファイルおよび行番号のエントリが含まれています。
rawlineセグメントには、ファイル、行およびアドレス遷移をエンコードするDWARF状態マシン命令を使用して、行の表がどのように生成されたかに関する詳細が示されます。
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)