GDBを使用したネイティブ実行可能ファイルのデバッグ

使用するGDB

デバッグ情報を使用したイメージのビルド

デバッグ情報を使用してネイティブ実行可能ファイルをビルドするには、アプリケーションのコンパイル時にjavac-gコマンドライン・オプションを指定してから、native-imageビルダーに指定します。これにより、ソース・レベルのデバッグが有効になり、デバッガ(GDB)はマシン命令をJavaファイル内の特定のソース行と関連付けます。

native-image引数に-gを追加すると、debuginfoが生成されます。イメージの横には、debuginfoを含む<imagename>.debugファイルと、Javaソース・ファイルを含むsourcesフォルダがあります。デバッガは、lineinfoのソースを表示するためにこれを使用します。たとえば:

hello_image
hello_image.debug
sources

GDBは、指定されたネイティブ実行可能ファイル<imagename><imagename>.debugファイルを自動的にロードします。(イメージとその*.debugファイルの間にリンクがあります)

デバッグ・エクスペリエンスを向上させるために、-g-O0を組み合せることをお薦めします。後者のオプションは、指定しない場合にデバッガに見られるGraalコンパイラのインライン化およびその他の最適化を無効にします(たとえば、デバッガは、ある行から次の行にステップ実行するのではなく、行間を前後にジャンプする場合があります)。同時に、-O0では、コンパイラで追加のメタデータを収集することも可能になり、デバッガはローカル変数などの解決に役立ちます。

新しいデバッグ情報でのGDBの使用

イメージ・ビルド情報

*.debugファイルには、次のようにアクセスできるイメージ・ビルドに関する追加情報が含まれています。

readelf -p .debug.svm.imagebuild.classpath hello_image.debug

イメージのビルドに使用されたすべてのクラスパス・エントリのリストを確認できます。

String dump of section '.debug.svm.imagebuild.classpath':
  [     0]  /home/user/.mx/cache/HAMCREST_e237ae735aac4fa5a7253ec693191f42ef7ddce384c11d29fbf605981c0be077d086757409acad53cb5b9e53d86a07cc428d459ff0f5b00d32a8cbbca390be49/hamcrest.jar
  [    b0]  /home/user/.mx/cache/JUNIT_5974670c3d178a12da5929ba5dd9b4f5ff461bdc1b92618c2c36d53e88650df7adbf3c1684017bb082b477cb8f40f15dcf7526f06f06183f93118ba9ebeaccce/junit.jar
  [   15a]  /home/user/mx/mxbuild/jdk20/dists/jdk9/junit-tool.jar
  [   1a9]  /home/user/graal/substratevm/mxbuild/jdk20/com.oracle.svm.test/bin

以下のセクションがあります

mainメソッドがある場所

次を使用して

info functions ::main

mainという名前のすべてのメソッドを検索し、次にb <main method name>を使用します。次に例を示します。

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

File hello/Hello.java:
76:	void hello.Hello::main(java.lang.String[]*);

File java/util/Timer.java:
534:	void java.util.TimerThread::mainLoop();
(gdb) b 'hello.Hello::main'

Breakpoint 1 at 0x83c030: file hello/Hello.java, line 76.

ブレークポイントの設定

まず、ブレークポイントを設定するメソッドのタイプを検索します。次に例を示します。

(gdb) info types ArrayList
All types matching regular expression "ArrayList":

...
File java/util/ArrayList.java:
	java.util.ArrayList;
	java.util.ArrayList$ArrayListSpliterator;
	java.util.ArrayList$Itr;
	java.util.ArrayList$ListItr;
...

次のGDBオートコンプリートを使用します:

(gdb) b 'java.util.ArrayList::

タブを2回押すと、選択できるすべてのArrayListメソッドが表示されます。

java.util.ArrayList::ArrayList(int)                                                java.util.ArrayList::iterator()
java.util.ArrayList::ArrayList(java.util.Collection*)                              java.util.ArrayList::lastIndexOf(java.lang.Object*)
java.util.ArrayList::add(int, java.lang.Object*)                                   java.util.ArrayList::lastIndexOfRange(java.lang.Object*, int, int)
java.util.ArrayList::add(java.lang.Object*)                                        java.util.ArrayList::listIterator()
java.util.ArrayList::add(java.lang.Object*, java.lang.Object[]*, int)              java.util.ArrayList::listIterator(int)
java.util.ArrayList::addAll(int, java.util.Collection*)                            java.util.ArrayList::nBits(int)
java.util.ArrayList::addAll(java.util.Collection*)                                 java.util.ArrayList::outOfBoundsMsg(int)
...

完了する場合

(gdb) b 'java.util.ArrayList::add`

addのすべてのバリアントにブレークポイントがインストールされます。

配列

配列には、索引を介してアクセスして個々の配列要素を取得できるdataフィールドがあります。次に例を示します。

Thread 1 "hello_image" hit Breakpoint 1, hello.Hello::main(java.lang.String[]*) (args=0x7ff33f800898) at hello/Hello.java:76
76	        Greeter greeter = Greeter.greeter(args);
(gdb) p args
$1 = (java.lang.String[] *) 0x7ff33f800898
(gdb) p *args
$2 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0x1e37be0
    }, <No data fields>}, 
  members of java.lang.String[]:
  len = 4,
  data = 0x7ff33f8008a0
}
(gdb) p args.data
$3 = 0x7ff33f8008a0
(gdb) ptype args.data
type = class _z_.java.lang.String : public java.lang.String {
} *[0]

ここでは、索引を介してargs.dataにアクセスできます。

この場合、4つの配列要素の最初の要素はStringへのポインタです。

(gdb) p args.data[0]
$4 = (_z_.java.lang.String *) 0x27011a

String

Java Stringオブジェクトの実際の内容を確認するには、次のようにして、valueフィールドを参照します:

(gdb) p args.data[0]
$4 = (_z_.java.lang.String *) 0x27011a

args.data[0]はStringオブジェクトを指します。derefを行いましょう。

(gdb) p *args.data[0]
$5 = {
  <java.lang.String> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0x1bb4780
      }, <No data fields>}, 
    members of java.lang.String:
    value = 0x270118,
    hash = 0,
    coder = 0 '\000',
    hashIsZero = false,
    static CASE_INSENSITIVE_ORDER = 0x19d752,
    ...
    static COMPACT_STRINGS = true
  }, <No data fields>}

valueフィールドには、Stringデータが保持されます。valueのタイプを確認しましょう。

(gdb) p args.data[0].value
$3 = (_z_.byte[] *) 0x250119

valuebyte[]型です。

前述したように、配列の要素にはdataフィールドを介してアクセスできます。

(gdb) p args.data[0].value.data
$10 = 0x7ff33f8008c8 "this\376\376\376\376\200G\273\001\030\001'"

GDBはスマートであるため、バイト・ポインタをすぐに使用できるC文字列として解釈できます。しかし、基本的には配列です。次に、thistを示します。

(gdb) p args.data[0].value.data[0]
$13 = 116 't'

最後のcharの後のガベージの理由は、Java文字列の値が(C文字列とは異なり) 0で終了していないことです。ガベージが開始する場所を確認するには、lenフィールドを調べます。

(gdb) p args.data[0].value.len
$14 = 4

ダウンキャスト

ソースで静的型Greeterの変数を使用し、そのデータを検査するとします。

75	    public static void main(String[] args) {
76	        Greeter greeter = Greeter.greeter(args);
77	        greeter.greet(); // Here you might have a NamedGreeter

ご覧のとおり、現在GDBは行77のgreeterの静的タイプについてのみ知っています。

Thread 1 "hello_image" hit Breakpoint 2, hello.Hello::main(java.lang.String[]*) (args=<optimized out>) at hello/Hello.java:77
77	        greeter.greet();
(gdb) p greeter
$17 = (hello.Hello$Greeter *) 0x7ff7f9101208

また、NamedGreeterサブクラスにのみ存在するフィールドを表示することはできません。

(gdb) p *greeter
$18 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0x1d1cae0
    }, <No data fields>}, <No data fields>}

ただし、オブジェクトのクラスオブジェクトを指すhubフィールドがあります。そのため、Greeterオブジェクトのランタイム型をアドレス0x7ff7f9101208で確認できます:

(gdb) p greeter.hub
$19 = (_z_.java.lang.Class *) 0x1d1cae0
(gdb) p *greeter.hub
$20 = {
  <java.lang.Class> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0x1bec910
      }, <No data fields>}, 
    members of java.lang.Class:
    typeCheckStart = 1188,
    name = 0xb94a2, <<<< WE ARE INTERESTED IN THIS FIELD
    superHub = 0x90202,
    ...
    monitorOffset = 8,
    optionalIdentityHashOffset = 12,
    flags = 0,
    instantiationFlags = 3 '\003'
  }, <No data fields>}
(gdb) p greeter.hub.name
$21 = (_z_.java.lang.String *) 0xb94a2
(gdb) p greeter.hub.name.value.data
$22 = 0x7ff7f80705b8 "hello.Hello$NamedGreeter\351\001~*"

これで、そのオブジェクトの実際の型が、hello.Hello$NamedGreeterであることがわかりました。次に、この型にキャストします:

(gdb) set $rt_greeter = ('hello.Hello$NamedGreeter' *) greeter

これで、ダウンキャストされたコンビニエンス変数rt_greeterを調べることができます:

(gdb) p $rt_greeter
$23 = (hello.Hello$NamedGreeter *) 0x7ff7f9101208
(gdb) p *$rt_greeter
$24 = {
  <hello.Hello$Greeter> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0x1d1cae0
      }, <No data fields>}, <No data fields>}, 
  members of hello.Hello$NamedGreeter:
  name = 0x270119
}

これで、NamedGreeterサブタイプにのみ存在するnameフィールドを確認できます。

(gdb) p $rt_greeter.name
$25 = (_z_.java.lang.String *) 0x270119

そのため、nameフィールドはString型です。Stringの内容を確認する方法は前述のとおりです:

(gdb) p $rt_greeter.name.value.data
$26 = 0x7ff7f91008c0 "FooBar\376\376\200G\273\001\027\001'"
⚠️ ダウンキャストする静的型が圧縮参照である場合、ダウンキャストで使用される型も圧縮参照の型である必要があります。

たとえば、次のようなフィールドがあるとします。

(gdb) p elementData.data[0]

$38 = (_z_.java.lang.Object *) 0x290fcc

ArrayListの内部配列では、最初のエントリが、_z_.接頭辞の付いたjava.lang.Objectを指していますが、これは圧縮参照であることを示しています。

そのオブジェクトのランタイム型を確認するには、次のようにします:

(gdb) p elementData.data[0].hub.name.value.data

$40 = 0x7ff7f8665600 "java.lang.String=\256\271`"

これで、圧縮参照が実際に指しているのが、java.lang.Stringであるとわかります。

そのため、キャストするときは、必ず_z_.接頭辞を使用します。

(gdb) p ('_z_.java.lang.String' *) elementData.data[0]

$41 = (_z_.java.lang.String *) 0x290fcc
(gdb) p *$41

$43 = {
  <java.lang.String> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0x1bb4780
      }, <No data fields>}, 
    members of java.lang.String:
    value = 0x290fce,
    ...

そのStringの内容を表示するには、再度次のようにします:

(gdb) p $41.value.data

$44 = 0x7ff7f9207e78 "#subsys_name\thierarchy\tnum_cgroups\tenabled"

インスタンス・メソッドでのthis変数の使用

(gdb) bt
#0  hello.Hello$NamedGreeter::greet() (this=0x7ff7f9101208) at hello/Hello.java:71
#1  0x000000000083c060 in hello.Hello::main(java.lang.String[]*) (args=<optimized out>) at hello/Hello.java:77
#2  0x0000000000413355 in com.oracle.svm.core.JavaMainWrapper::runCore0() () at com/oracle/svm/core/JavaMainWrapper.java:178
#3  0x00000000004432e5 in com.oracle.svm.core.JavaMainWrapper::runCore() () at com/oracle/svm/core/JavaMainWrapper.java:136
#4  com.oracle.svm.core.JavaMainWrapper::doRun(int, org.graalvm.nativeimage.c.type.CCharPointerPointer*) (argc=<optimized out>, argv=<optimized out>) at com/oracle/svm/core/JavaMainWrapper.java:233
#5  com.oracle.svm.core.JavaMainWrapper::run(int, org.graalvm.nativeimage.c.type.CCharPointerPointer*) (argc=<optimized out>, argv=<optimized out>) at com/oracle/svm/core/JavaMainWrapper.java:219
#6  com.oracle.svm.core.code.IsolateEnterStub::JavaMainWrapper_run_e6899342f5939c89e6e2f78e2c71f5f4926b786d(int, org.graalvm.nativeimage.c.type.CCharPointerPointer*) (__0=<optimized out>, __1=<optimized out>)
at com/oracle/svm/core/code/IsolateEnterStub.java:1
(gdb) p this
$1 = (hello.Hello$NamedGreeter *) 0x7ff7f9001218
(gdb) p *this
$2 = {
  <hello.Hello$Greeter> = {
    <java.lang.Object> = {
      <_objhdr> = {
        hub = 0x1de2260
      }, <No data fields>}, <No data fields>}, 
  members of hello.Hello$NamedGreeter:
  name = 0x25011b
}
(gdb) p this.name
$3 = (_z_.java.lang.String *) 0x270119

JavaまたはC++コードの場合と同様に、インスタンス・メソッドでは、this.の接頭辞は必要ありません。

(gdb) p name
$7 = (_z_.java.lang.String *) 0x270119
(gdb) p name.value.data
$8 = 0x7ff7f91008c0 "FooBar\376\376\200G\273\001\027\001'"

静的フィールドへのアクセス

静的フィールドはオブジェクトのインスタンスを出力するたびに表示されますが、必要なのは特定の静的フィールドの値のみです。

(gdb) p 'java.math.BigDecimal::BIG_TEN_POWERS_TABLE'
$23 = (_z_.java.math.BigInteger[] *) 0x132b95

すべての静的フィールドのリストを取得するには、次を使用します。

(gdb) info variables ::

.classオブジェクトの検査

イメージ内のすべてのJavaタイプについて、そのクラス・オブジェクト(ハブ)に簡単にアクセスする方法があります。

(gdb) info types PrintStream
All types matching regular expression "PrintStream":

...
File java/io/PrintStream.java:
	java.io.PrintStream;
	java.io.PrintStream$1;
...

java.io.PrintStreamのハブへのアクセスには、.class接尾辞を使用できます:

(gdb) p 'java.io.PrintStream.class'
$4 = {
  <java.lang.Object> = {
    <_objhdr> = {
      hub = 0x1bec910
    }, <No data fields>}, 
  members of java.lang.Class:
  typeCheckStart = 1340,
  name = 0xbab58,
  superHub = 0x901ba,
  ...
  sourceFileName = 0xbab55,
  classInitializationInfo = 0x14d189,
  module = 0x14cd8d,
  nestHost = 0xde78d,
  simpleBinaryName = 0x0,
  companion = 0x149856,
  signature = 0x0,
  ...
}

これにより、たとえば、java.io.PrintStreamが属するモジュールを確認できます:

(gdb) p 'java.io.PrintStream.class'.module.name.value.data
$12 = 0x7ff7f866b000 "java.base"

インライン化されたメソッド

PrintStream.writelnでのブレークポイントの設定

(gdb) b java.io.PrintStream::writeln
Breakpoint 2 at 0x4080cb: java.io.PrintStream::writeln. (35 locations)

次にナビゲートします:

(gdb) bt
#0  java.io.BufferedWriter::min(int, int) (this=<optimized out>, a=8192, b=14) at java/io/BufferedWriter.java:216
#1  java.io.BufferedWriter::implWrite(java.lang.String*, int, int) (this=0x7ff7f884e828, s=0x7ff7f9101230, off=<optimized out>, len=<optimized out>) at java/io/BufferedWriter.java:329
#2  0x000000000084c50d in java.io.BufferedWriter::write(java.lang.String*, int, int) (this=<optimized out>, s=<optimized out>, off=<optimized out>, len=<optimized out>) at java/io/BufferedWriter.java:313
#3  0x0000000000901369 in java.io.Writer::write(java.lang.String*) (this=<optimized out>, str=<optimized out>) at java/io/Writer.java:278
#4  0x00000000008df465 in java.io.PrintStream::implWriteln(java.lang.String*) (this=0x7ff7f87e67b8, s=<optimized out>) at java/io/PrintStream.java:846
#5  0x00000000008e10a5 in java.io.PrintStream::writeln(java.lang.String*) (this=0x7ff7f87e67b8, s=<optimized out>) at java/io/PrintStream.java:826
#6  0x000000000083c00c in java.io.PrintStream::println(java.lang.String*) (this=<optimized out>, x=<optimized out>) at java/io/PrintStream.java:1168
#7  hello.Hello$NamedGreeter::greet() (this=<optimized out>) at hello/Hello.java:71
#8  0x000000000083c060 in hello.Hello::main(java.lang.String[]*) (args=<optimized out>) at hello/Hello.java:77
#9  0x0000000000413355 in com.oracle.svm.core.JavaMainWrapper::runCore0() () at com/oracle/svm/core/JavaMainWrapper.java:178
#10 0x00000000004432e5 in com.oracle.svm.core.JavaMainWrapper::runCore() () at com/oracle/svm/core/JavaMainWrapper.java:136
#11 com.oracle.svm.core.JavaMainWrapper::doRun(int, org.graalvm.nativeimage.c.type.CCharPointerPointer*) (argc=<optimized out>, argv=<optimized out>) at com/oracle/svm/core/JavaMainWrapper.java:233
#12 com.oracle.svm.core.JavaMainWrapper::run(int, org.graalvm.nativeimage.c.type.CCharPointerPointer*) (argc=<optimized out>, argv=<optimized out>) at com/oracle/svm/core/JavaMainWrapper.java:219
#13 com.oracle.svm.core.code.IsolateEnterStub::JavaMainWrapper_run_e6899342f5939c89e6e2f78e2c71f5f4926b786d(int, org.graalvm.nativeimage.c.type.CCharPointerPointer*) (__0=<optimized out>, __1=<optimized out>)
    at com/oracle/svm/core/code/IsolateEnterStub.java:1

上部フレームに関する追加情報を問い合せると、minimplWriteにインライン化されていることがわかります:

(gdb) info frame
Stack level 0, frame at 0x7fffffffdb20:
 rip = 0x84af8a in java.io.BufferedWriter::min(int, int) (java/io/BufferedWriter.java:216); saved rip = 0x84c50d
 inlined into frame 1
 source language unknown.
 Arglist at unknown address.
 Locals at unknown address, Previous frame's sp in rsp

minが使用されている場所に進むと、minによって値14が返されたことがわかります(想定どおり):

(gdb) bt
#0  java.lang.String::getChars(int, int, char[]*, int) (this=0x7ff7f9101230, srcBegin=0, srcEnd=14, dst=0x7ff7f858ac58, dstBegin=0) at java/lang/String.java:1688
#1  java.io.BufferedWriter::implWrite(java.lang.String*, int, int) (this=0x7ff7f884e828, s=0x7ff7f9101230, off=<optimized out>, len=<optimized out>) at java/io/BufferedWriter.java:330
...

デバッグ中のsvm_dbg_ヘルパー関数のコール

イメージが-H:+IncludeDebugHelperMethodsで構築されると、デバッグ中にGDBからコールできる追加の@CEntryPoint関数が定義されます。次に例を示します。

(gdb) p greeter 
$3 = (hello.Hello$Greeter *) 0x7ffff6881900

ここにも、ローカルで名前が付けられたgreeter (静的型はhello.Hello$Greeter)があります。ランタイム型の確認には、前述のメソッドを使用できます。

または、svm_dbg_ヘルパー関数を使用することも可能です。たとえば、実行中のデバッグ・セッションの中から、次のようにコールできます:

void svm_dbg_print_hub(graal_isolatethread_t* thread, size_t hubPtr)

graal_isolatethread_tの値と、出力するハブの絶対アドレスを渡す必要があります。ほとんどの場合、graal_isolatethread_tの値は、プラットフォーム固有のレジスタにある現在のIsolateThreadの値にすぎません。

プラットフォーム レジスタ
amd64 $r15
aarch64 $r28

最後に、svm_dbg_print_hubをコールする前に、出力するハブの絶対アドレスがあることを確認してください。次を使用すると、

(gdb) p greeter.hub
$4 = (_z_.java.lang.Class *) 0x837820 <java.io.ObjectOutputStream::ObjectOutputStream(java.io.OutputStream*)+1120>

現在の状況では、greeterhubフィールドがハブへの圧縮参照を保持していることが明らかになります(hub-typeには接頭辞_z_.が付いています)。したがって、別のsvm_dbg_ヘルパー・メソッドを使用して、ハブ・フィールドの絶対アドレスを先に取得する必要があります。

(gdb) call svm_dbg_obj_uncompress($r15, greeter.hub)
$5 = 140737339160608
(gdb) p/x $5
$6 = 0x7ffff71b7820

svm_dbg_obj_uncompressをコールすることで、ハブがアドレス0x7ffff71b7820にあることがわかり、svm_dbg_print_hubをコールできるようになります。

(gdb) call (void) svm_dbg_print_hub($r15, 0x7ffff71b7820)
hello.Hello$NamedGreeter

svm_dbg_ヘルパーへのコールはどちらも、単一のコマンド・ラインに結合できます。

(gdb) call (void) svm_dbg_print_hub($r15, svm_dbg_obj_uncompress($r15, greeter.hub))
hello.Hello$NamedGreeter

現在、次のsvm_dbg_ヘルパー・メソッドが定義されています。

int svm_dbg_ptr_isInImageHeap(graal_isolatethread_t* thread, size_t ptr);
int svm_dbg_ptr_isObject(graal_isolatethread_t* thread, size_t ptr);
int svm_dbg_hub_getLayoutEncoding(graal_isolatethread_t* thread, size_t hubPtr);
int svm_dbg_hub_getArrayElementSize(graal_isolatethread_t* thread, size_t hubPtr);
int svm_dbg_hub_getArrayBaseOffset(graal_isolatethread_t* thread, size_t hubPtr);
int svm_dbg_hub_isArray(graal_isolatethread_t* thread, size_t hubPtr);
int svm_dbg_hub_isPrimitiveArray(graal_isolatethread_t* thread, size_t hubPtr);
int svm_dbg_hub_isObjectArray(graal_isolatethread_t* thread, size_t hubPtr);
int svm_dbg_hub_isInstance(graal_isolatethread_t* thread, size_t hubPtr);
int svm_dbg_hub_isReference(graal_isolatethread_t* thread, size_t hubPtr);
long long int svm_dbg_obj_getHub(graal_isolatethread_t* thread, size_t objPtr);
long long int svm_dbg_obj_getObjectSize(graal_isolatethread_t* thread, size_t objPtr);
int svm_dbg_obj_getArrayElementSize(graal_isolatethread_t* thread, size_t objPtr);
long long int svm_dbg_obj_getArrayBaseOffset(graal_isolatethread_t* thread, size_t objPtr);
int svm_dbg_obj_isArray(graal_isolatethread_t* thread, size_t objPtr);
int svm_dbg_obj_isPrimitiveArray(graal_isolatethread_t* thread, size_t objPtr);
int svm_dbg_obj_isObjectArray(graal_isolatethread_t* thread, size_t objPtr);
int svm_dbg_obj_isInstance(graal_isolatethread_t* thread, size_t objPtr);
int svm_dbg_obj_isReference(graal_isolatethread_t* thread, size_t objPtr);
long long int svm_dbg_obj_uncompress(graal_isolatethread_t* thread, size_t compressedPtr);
long long int svm_dbg_obj_compress(graal_isolatethread_t* thread, size_t objPtr);
int svm_dbg_string_length(graal_isolatethread_t* thread, size_t strPtr);
void svm_dbg_print_hub(graal_isolatethread_t* thread, size_t hubPtr);
void svm_dbg_print_obj(graal_isolatethread_t* thread, size_t objPtr);
void svm_dbg_print_string(graal_isolatethread_t* thread, size_t strPtr);
void svm_dbg_print_fatalErrorDiagnostics(graal_isolatethread_t* thread, size_t sp, void * ip);
void svm_dbg_print_locationInfo(graal_isolatethread_t* thread, size_t mem);