やってみた 2026年10月8日

RAD DebuggerのLinux対応をコンテナで試したら、ブレークポイントでは止まりビットフィールドは32bitのまま見えた

XECIN CデバッガLinuxOSS

GitHubのトレンドに Epic Games の RAD Debugger が上がっていて、思わずクリックしました。名前だけは知っていたんですが、Windows 専用のデバッガだと思い込んでいたんですよね。

ところが CHANGELOG を開くと、9月30日(UTC)に出た v0.9.29-alpha の項目の一番上に「preliminary support for native Linux x64 debugging」と書いてありました。Linux 向けのバイナリはまだ配布しておらず、README の手順でソースからビルドしてほしい、とのこと。一方で README の冒頭は、いまも「Windows x64 のデバッグだけに対応」という説明のままでした。どっちが本当なのか、自分の手で確かめたくなりました。

恥ずかしながら、僕は普段 gdb をなんとなく叩いている程度で、デバッガの中身はほとんど知りません。なので今回は「ビルドできるか」「自分で書いた C のプログラムを止められるか」「変数がちゃんと見えるか」の3つに絞って試しています。

試した環境はこんな感じです。

  • 使い捨ての Docker コンテナ(debian:bookworm-slim、Debian 12.14)。メモリ 4GB・CPU 2 に制限し、ホストのフォルダはマウントしない
  • ホストは Windows の Docker Desktop(カーネル表示は 5.15.167.4-microsoft-standard-WSL2)
  • コンパイラは Debian 標準の clang 14.0.6
  • RAD Debugger は main の b6d8c3fd(コミット日時は 2026-10-07 13:55 -07:00)。起動画面の表示は「0.9.30 ALPHA」でした

Linux でビルドするまで(拍子抜けするほど速かった)

README の Linux 手順は、clang(または gcc)と、freetype・X11 まわり・OpenGL/EGL の開発用パッケージを7つ入れて ./build.sh を叩くだけです。README のサンプル出力は Ubuntu の clang 18.1.3 だったので、Debian 12 の clang 14 では古すぎて怒られるんじゃないかと身構えていました。

# 依存を入れる(README の7パッケージ + clang/llvm + git)
$ apt-get install -y --no-install-recommends git ca-certificates clang llvm \
    libfreetype6-dev libx11-dev libxext-dev libxfixes-dev libxrandr-dev libgl-dev libegl-dev
apt rc=0 elapsed=40.3263s

$ git clone --depth 1 https://github.com/EpicGames/raddebugger.git
clone elapsed_ms=2213          # .git 込みで 34MB

# デバッグビルド(引数なし = デバッガ本体)
$ ./build.sh
[clang compile]
[debug mode]
[building metagen]
searching /work/raddebugger/src... 583 files found
parsing metadesk... 25 metadesk files parsed
gathering tables... 139 tables found
generating layer code...
[assembling blake3]
llvm-ar: warning: creating build/blake3.a
[INFO] Debug build complete. For a faster build, call this script with the `release` argument (this will take significantly longer than a debug build).
build.sh rc=0 elapsed_ms=6745

$ ls -la build/raddbg
-rwxr-xr-x 1 root root 7692984 Oct  8 12:08 raddbg

拍子抜けするくらい、何事もなく通りました。エラーは0件、デバッグビルドは6.7秒。CLI のユーティリティ radbin は ./build.sh radbin で1.4秒、画面を持たない版の ./build.sh raddbg_non_graphical も6.0秒でした。ソース583ファイルからコード生成して1つにまとめてビルドする構成らしく、待ち時間はほぼ apt の40秒だけでした。

試しに使ったのは、ビットフィールドを持つ構造体を混ぜた小さな C のプログラムです。CHANGELOG の既知の問題に「Bitfield type info is not yet supported correctly」とあったので、それが実際にどう見えるのかも確かめたかったんです。

typedef struct Flags {
  unsigned int ready   : 1;
  unsigned int mode    : 3;
  unsigned int retries : 4;
  unsigned int code    : 24;
} Flags;

typedef struct Order { int id; const char *name; Flags flags; double price; } Order;

static int g_counter = 0;

static double total_price(const Order *orders, int count) {
  double sum = 0.0;
  for (int i = 0; i < count; i += 1) {
    sum += orders[i].price;      /* 22行目: ここにブレークポイント */
    g_counter += 1;
  }
  return sum;
}

int main(void) {
  Order orders[3] = {
    {1, "apple",  {1, 2, 3, 0xABCDE}, 120.0},
    {2, "banana", {0, 5, 15, 42},      80.5},
    {3, "cherry", {1, 7, 0, 7},       300.25},
  };
  double t = total_price(orders, 3);
  printf("total=%.2f counter=%d\n", t, g_counter);   /* 実行結果: total=500.75 counter=3 */
  return 0;
}

clang -g -O0 でビルドすると DWARF のバージョンは5でした。RAD Debugger は DWARF をそのまま読むのではなく、独自の RDI という形式に変換してから使う作りです。その変換を担う radbin を先に単体で叩いてみました。

# ELF/DWARF -> RDI 変換(radbin 0.9.30)
$ radbin --rdi demo --out:demo.rdi
demo rc=0 ms=18 rdi_bytes=9292 elf_bytes=17880      # .debug_* セクション合計は 1,080B

# 自分でビルドしたデバッガ本体(-g 付き)も変換してみる
$ radbin --rdi build/raddbg --out:raddbg.rdi
raddbg rc=0 ms=545 rdi_bytes=6288992                # .debug_* セクション合計は 2,080,111B

# 中身をテキストでダンプ(1,428行)。Flags の部分だけ抜粋
$ radbin --dump demo.rdi
  // udt[7]
  'Flags':
  {
    self_type: 46
    flags: `None`
    members:
    {
      'ready': { kind: DataField, type: 48, off: 0 }
      'mode': { kind: DataField, type: 48, off: 0 }
      'retries': { kind: DataField, type: 48, off: 0 }
      'code': { kind: DataField, type: 48, off: 0 }
    }
  }
  # type[48] は kind: Alias / byte_size: 4 / name: 'unsigned int'

# Breakpad 形式への変換も試した
$ radbin --breakpad demo --out:demo.sym
MODULE windows x86_64 709c1b02fe6e3122 /work/demo/demo
FILE 1 work/demo/demo.c
FUNC 1150 5e 0 main
FUNC 1150 5e 0 main
FUNC 11b0 68 0 total_price

変換は速いです。デバッガ本体の約2MBの DWARF でも0.5秒ちょっとでした。ただ、RDI のほうが元の DWARF より大きくなりました(1,080B → 9,292B、2,080,111B → 6,288,992B)。なぜ大きくなるのかは、今回は調べきれていません。

気になったのはダンプの Flags の部分です。4つのメンバーが全部「unsigned int、オフセット0」になっていて、何ビット目から何ビットぶん、という情報が見当たりません。rdi.h を grep すると RDI_TypeKind_Bitfield = 0xF000 という型の種類自体は定義されているので、形式としては表せるけど、DWARF からの変換ではまだ使われていない、ということなんだと思います(ここは推測です)。

Breakpad のほうは、アドレスとサイズ自体は llvm-nm -S の結果(main が 0x1150・0x5e、total_price が 0x11b0・0x68)と一致していました。ただ、Linux の ELF なのに MODULE 行が windows になる、main の行が2回出る、FILE のパスの先頭の / が欠ける、の3点が目に留まりました。Breakpad を使う予定がある人は、ここは自分の環境で確かめてから使ったほうがよさそうです。

ブレークポイントで止めて、ビットフィールドを覗いてみた

いよいよ GUI です。コンテナに画面はないので、仮想ディスプレイの Xvfb(1600x900)を立てて、その上でデバッガを起動しました。GPU は無く、OpenGL まわりは Mesa(libgl1-mesa-dri)を入れただけです。正直なところ、ここで描画まわりのエラーが出て終わるだろうと思っていたんですが、普通にウィンドウが出ました。

Xvfb上で起動したRAD Debugger 0.9.30 ALPHAの初期画面。左にGetting Startedパネルとロゴ、中央にAdd Targetボタン、右にTargets・Watch・Output・Breakpoints・Call Stack・Threadsの各パネルが並び、Targetsは空、下部のステータスはNot running

起動直後の画面です。よく見ると、右上の Targets パネルが空なんです。デバッガ側の README(ビルドすると build/raddbg_readme.md に出てきます)には「raddbg の後ろにプログラムのパスを書くとターゲットになる」とあって、僕は raddbg --user:... --project:... /work/demo/demo と渡していました。ところが載っていない。プログラムを走らせて成功したら閉じる --auto_run -q も試しましたが、60秒待っても終わらず timeout で打ち切り(rc=124)でした。画面なし版の raddbg_non_graphical でも同じ結果です。

最初は全然分からなくて、Linux だとまだ動かないのかと諦めかけました。そこで、外から別プロセスで命令を送れる --ipc を使うことにしました。命令の名前は、同じ raddbg_readme.md に一覧があります。

# 起動済みのデバッガに外から命令を送る
$ raddbg --ipc add_target /work/demo/demo
$703                    # なぜかこれだけ表示された(意味は未確認)
$ raddbg --ipc add_breakpoint /work/demo/demo.c:22
(response received, no output)
$ raddbg --ipc run
(response received, no output)

# 5秒後のプロセスの状態
$ ps -eo pid,ppid,stat,args | grep '/work/demo/demo$'
 6113  5853 t    /work/demo/demo
$ grep -E "State|TracerPid" /proc/6113/status
State:	t (tracing stop)
TracerPid:	5867        # 親の 5853 が raddbg 本体。5867 は raddbg のスレッドの1つと思われる(未確認)

# 注意: ここは自分の使い方次第で結果が変わったところ。
# 2つ目のインスタンスを起動したまま --ipc を送ると、届いたのは先に起動していた方だった。
# 2つ目を閉じた後は --ipc が rc=0 を返すのに画面が変わらず、
# 最後はウィンドウに xdotool でキー(Escape / F5)を送って操作した

ターゲットを足してから run すると、ちゃんと止まりました。ステータスバーが赤くなって「demo hit a breakpoint」。debuggee は t (tracing stop) で、TracerPid の 5867 は raddbg 本体(5853)とは別の番号でした。raddbg のスレッドの1つだと思われます(未確認)。--privileged も --cap-add も付けていない普通のコンテナで、ptrace が通っていました。

告白すると、この --ipc まわりでは自分のミスでかなり遠回りしています。最初に起動したインスタンスを pkill -x raddbg で消したつもりが消えておらず、2つ目のウィンドウの裏で1つ目が命令を受け取っていました。画面に映っているのは何も起きていない2つ目なので「命令が効かない」と勘違いしていました。今思えば、まず ps を見るべきでした。2つ目を閉じたあとは、--ipc を送っても rc=0 なのに画面が変わらなくなりました。原因はまだ分かっていません。

ブレークポイントで停止中のRAD Debugger。demo.cの22行目に赤い丸と黄色い矢印が付き、行の右にsum = 0.0と表示。右のWatchパネルではorders[0].flagsが中括弧の中に180149813、1801...と並び、orders[0].flags.codeが180149813(unsigned int)、g_counterが0。Call Stackはtotal_price、main、???が2行。下部のステータスにdemo hit a breakpoint

止まったところで、Watch に orders[0].flags と orders[0].flags.code を足した画面がこれです。ソースの22行目の横に sum = 0.0 と値が出ていて、Call Stack には total_price → main と並んでいます(その下の2行は ???)。

そして本命のビットフィールド。code は 0xABCDE(10進で703710)を入れたはずなのに、表示は 180149813 でした。

見たもの期待した値表示された値
Watch の orders[0].flags.code0xABCDE(703710)180149813(unsigned int)
Watch の orders[0].flags{1, 2, 3, 703710}{180149813, 1801…}(列幅で途中まで表示)
radbin のダンプの 4 メンバービット位置と幅が分かれている全部 unsigned int・off: 0

180149813 という数字の正体を手計算してみました。ready=1、mode=2、retries=3、code=0xABCDE を下位ビットから詰めると、32bit の値は 0x0ABCDE35 です。これを10進にすると 180149813。表示とぴったり一致しました。つまり、どのメンバーを見ても「4つをまとめた32bitの箱」そのものが出ている、ということです。radbin のダンプで全部オフセット0の unsigned int になっていたのと、ちゃんとつじつまが合います。CHANGELOG に既知の問題として書いてあった件を、自分の目で見た形です。

最後に F5(続行)を3回送りました。1回目と2回目の後はまだ t のままで、3回目で debuggee がいなくなりました。ループは3周なので、22行目で3回止まった計算と合います。ただ、2回目・3回目に止まっている間の画面は1回目と同じ画像のままで、g_counter が増えていく様子は画面では確かめられませんでした。ここも理由は分かっていません。

試してみての所感

コンテナを立ててから壊すまで、11分13秒でした。そのうちビルドに使ったのは3回合わせて14秒ほど。「Linux でソースから入れる」と聞いて身構えていたぶん、ここは素直にうれしい誤算です。

今回の体験で学んだことを並べておきます。

  • README の冒頭(Windows のみ)より、CHANGELOG の v0.9.29-alpha(Linux に暫定対応)のほうが新しかった。alpha のプロジェクトは CHANGELOG から読む
  • Debian 12 の clang 14 でも、エラーなしでビルドできた(README の例は clang 18)
  • コマンドラインで渡したターゲットは、僕の環境では Targets に載らなかった。--ipc add_target なら載った
  • --ipc は「どのインスタンスに届いたか」を、送る前に ps で確かめる
  • ビットフィールドは、どのメンバーも同じ32bitの値で見える。値を読むときは自分でビットを切り出す必要がある

C で書いた業務の小物ツールを Linux で追いかけるときに、gdb の代わりの選択肢になるか。気になって触ってみたんですが、現時点の僕の答えは「ブレークポイントとコールスタックを見る用途なら、もう試せる。ビットフィールドを多用するコードはまだ様子見」です。Linux 対応は CHANGELOG 自身が「very early」と書いている段階なので、次のリリースでビットフィールドとコマンドラインのターゲットがどうなるか、また同じプログラムで確かめてみたいと思います。