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

生成されたネイティブ実行可能ファイルは、最小限のシンボル情報で高度に最適化されたコードであるため、デバッグが困難になります。これを解決するには、ビルド時にデバッグ情報を結果のバイナリに埋め込みます。この情報は、マシン・コードを解釈して元のJavaメソッドに戻す方法をデバッガに正確に伝えます。

このガイドでは、標準のLinux GNUデバッガ(GDB)を使用してネイティブ実行可能ファイルをデバッグする方法について学習します。

ノート: GDBを使用したネイティブ・イメージのデバッグは現在、macOSの初期サポートでLinux上で機能します。この機能は試験段階です。

デモの実行

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

前提条件

GDBを使用してネイティブ実行可能ファイルのデバッグをテストするステップに従います。次のワークフローは、GDB 10.1を使用するLinuxで動作することが確認されています。

  1. GraalVM JDK Downloaderを使用して、ネイティブ・イメージを含む最新のGraalVM JDKをダウンロードしてインストールします:
     bash <(curl -sL https://get.graalvm.org/jdk)
    
  2. 次のコードをGDBDemo.javaという名前のファイルに保存します。

     public class GDBDemo {
         static long fieldUsed = 1000;
    
         public static void main(String[] args) {
             if (args.length > 0) {
                 int n = -1;
                 try {
                     n = Integer.parseInt(args[0]);
                 } catch (NumberFormatException ex) {
                     System.out.println(args[0] + " is not a number!");
                 }
                 if (n < 0) {
                     System.out.println(args[0] + " is negative.");
                 }
                 double f = factorial(n);
                 System.out.println(n + "! = " + f);
             } 
    
             if (false)
                 neverCalledMethod();
    
             StringBuilder text = new StringBuilder();
             text.append("Hello World from GraalVM Native Image and GDB in Java.\n");
             System.out.println(text.toString());
         }
    
         static void neverCalledMethod() {
             System.out.println("This method is unreachable and will not be included in the native executable.");
         }
    
         static double factorial(int n) {
             if (n == 0) {
                 return 1;
             }
             if (n >= fieldUsed) {
                 return Double.POSITIVE_INFINITY;
             }
             double f = 1;
             while (n > 1) {
                 f *= n--;
             }
             return f;
         }
     }
    
  3. これをコンパイルし、デバッグ情報を使用してネイティブ実行可能ファイルを生成します:

     $JAVA_HOME/bin/javac -g GDBDemo.java
    
     native-image -g -O0 GDBDemo
    

    -gオプションは、デバッグ情報を生成するようにnative-imageに指示します。結果のネイティブ実行可能ファイルには、GDBによって認識される形式のデバッグ・レコードが含まれます。

    -O0を渡して、コンパイラの最適化を実行しないことを指定することもできます。すべての最適化の無効化は必須ではありませんが、一般的にはデバッグ・エクスペリエンスが向上します。

  4. デバッガを起動し、ネイティブ実行可能ファイルを実行します:

     gdb ./gdbdemo
    

    gdbプロンプトが開きます。

  5. ブレークポイントを設定します: breakpoint <java method>と入力してブレークポイントを設定し、run <arg>と入力してネイティブ実行可能ファイルを実行します。ブレークポイントは、ファイルと行、またはメソッド名で構成できます。デバッグ・セッションの例を次に示します。

     $ gdb ./gdbdemo
     GNU gdb (GDB) 10.2
     Copyright (C) 2021 Free Software Foundation, Inc.
     ...
     Reading symbols from ./gdbdemo...
     Reading symbols from /dev/gdbdemo.debug...
     (gdb) info func ::main
     All functions matching regular expression "::main":
    
     File GDBDemo.java:
     5:	void GDBDemo::main(java.lang.String[]*);
     (gdb) b ::factorial
     Breakpoint 1 at 0x2d000: file GDBDemo.java, line 32.
     (gdb) run 42
     Starting program: /dev/gdbdemo 42
     Thread 1 "gdbdemo" hit Breakpoint 1, GDBDemo::factorial (n=42) at GDBDemo.java:32
     32	        if (n == 0) {
     (gdb) info args
     n = 42
     (gdb) step
     35	        if (n >= fieldUsed) {
     (gdb) next
     38	        double f = 1;
     (gdb) next
     39	        while (n > 1) {
     (gdb) info locals
     f = 1
     (gdb) ...
    

ネイティブ実行可能ファイルがセグメンテーション違反の場合、スタック全体のバックトレースを出力できます(bt)。

デバッガは、マシン命令をバイナリからJavaファイル内の特定のソース行に戻します。コンパイル済メソッド内のシングルステップ実行には、インライン化されたコードのファイルおよび行番号情報が含まれることに注意してください。GDBでは、同じコンパイル済メソッドの処理中でもファイルを切り替えることができます。

通常のデバッグ・アクションのほとんどは、GDBでサポートされています:

デバッグ情報の生成は、Javaプログラムを同等のC++プログラムとしてモデル化することによって実装されます。GDBは主にC (およびC++)のデバッグ用に設計されているため、Javaアプリケーションのデバッグ時に考慮すべき点がいくつかあります。ネイティブ・イメージのデバッグのサポートの詳細は、リファレンス・ドキュメントを参照してください。