🐞

逆引き Linux アプリ開発デバッグコマンド

2022/09/26に公開

逆引き Linux アプリ開発デバッグコマンド

はじめに

ローカル環境では検証したのに、実環境にデプロイしたら動かない。そんなとき printf を仕込んでリビルドしアップロード、を繰り返すばかりではなく、複数のデバッグ手段を持っていると開発効率が大きく上がります。

中核となるのは GNU bintuil と GCC toolchain です。しかしこれらのツール群は、 UNIX 哲学 に基づいて個々のコマンドがシンプルで素っ気ないうえ、日本語の情報も少ないため、開発経験が長い方でも使い道を知らない場合があるようです。

そこで本記事では、デバッグに使える様々なコマンドを、使い道ごとに並べて一覧化してみました。

なおコンパイラは gcc 前提に記載していますが、LLVM も概ね同じツールチェインが提供されているようです。

バイナリ解析

バージョンや実行ターゲットの取り違えなど、そもそも狙い通りの実行ファイルが配置されているか、疑わしい場合などに使用するコマンドたちです。

バイナリファイルの形式が知りたい

file コマンドでファイルの種別を判定できます。

$ file /usr/bin/ldd
/usr/bin/ldd: Bourne-Again shell script, ASCII text executable
$ file /usr/bin/cat
/usr/bin/cat: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=44af8b317775373b1a7783fbd0d83c2fe7f21f6e, for GNU/Linux 3.2.0, stripped

「そのようなファイルやディレクトリはありません」

ファイルは確かに存在するのに上記メッセージが表示されてしまう場合、 x64 OS 上で x86 ファイルを起動したなど、実行ファイルと実行環境のアーキテクチャが一致していない可能性があります。
以下のように file コマンドで調査可能です。

$ ./gdbserver
bash: ./gdbserver: そのようなファイルやディレクトリはありません
$file ./gdbserver
./gdbserver: ELF 32-bit LSB pie executable, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=28629e78b07281967d0441be16b89411113e3e6d, for GNU/Linux 3.2.0, stripped

実行ファイルを起動しないでバージョン情報が知りたい

起動不具合の調査中、ファイルバージョンが知りたいのに、そもそも起動できないから -v オプションが使えない…そんなとき、strings コマンドでバイナリに含まれる可読文字列を抽出できます。

$ strings /usr/bin/bash | grep "Bash version"
@(#)Bash version 5.1.16(1) release GNU

なお、実行ファイルにパスワードなどを平文で保存すると、本コマンドで容易に抽出できてしまうので注意が必要です。

特定のクラスや関数が含まれているか知りたい

同一のソースツリーから複数のターゲットを作り分ける場合、構成ごとにリンクさせる・させないファイルを CMakeLists.txt などに設定しますが、意図通りにリンクできていない場合があります。

そんなとき、以下コマンドで表示されるシンボルテーブルから特定の関数が含まれるか検索できます。

objdump --syms <実行ファイル> | grep <関数名>

SIMD 演算が有効か調べたい

objdump -d で逆アセンブルを抽出することで、SIMD演算による高速化が有効な設定でビルドされているかや、最適化によって SIMD 化されているかを検証できます。

バイナリファイルを16進数でダンプしたい

hexdump コマンドで可能です。

環境設定

正しく配置したはずの実行ファイルを起動できない際などに使用するコマンドたちです。

パスが通っている共有ライブラリを確認する

sudo ldconfig -p で表示できます。

もし意図したディレクトリにとっていない場合、LD_LIBRARY_PATH 環境変数を設定後 sudo ldconfig コマンドでキャッシュを再構築します。

実行ファイルが依存する共有ライブラリを表示する

ldd コマンドでロードされる共有ライブラリのパスを表示できます。

$ ldd /usr/bin/bash
    linux-vdso.so.1 (0x00007fffc77bf000)
    libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f66c58f6000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f66c56ce000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f66c5a9a000)

=> (not found) と表示されている場合、そのファイルのパス設定が欠けています。

sudo で実行すると起動しない

sudo 実行時は環境変数が引き継がれません。現ユーザーの環境変数を引き継ぐには sudo -E オプション指定が必要です。

デバッガ

起動直後に segfault で落ちる不具合の解析手法です。

ローカルで再現しない不具合のリモートデバッグ

デプロイ先でしか再現しない不具合を調査する際、 gdb は依存パッケージが多く(例えば python) インストールがためらわれる場合があります。

そんな場合でも gdbserver を使えば、TCP 通信経由でステップ実行や変数ウォッチなどのデバッグが可能です。
わずか数百KBのシングルバイナリで依存も少なく、ファイル一つをコピーすれば動かせることが多いです。

まずリモート側で SSH などで以下コマンドを起動しておきます。

$ gdbserver :<ポート番号> <起動コマンド>

そして開発機ローカルで gdb を起動し、サーバーへ接続します。

$ gdb
(gdb) target remote <サーバーアドレス>:<ポート番号>

vscode で launch.json を構築済みの場合、"miDebuggerServerAddress" : "<address>:<port>" のようにサーバーアドレスを追加するだけでビジュアルデバッグが可能です。

リリースビルドでしか再現しない不具合のデバッグ

デバッグビルドは一般的に、最適化無効&デバッグ情報作成& DEBUG マクロ定義です。
これらはコンパイラにとって一体のものではなく、個別に指定することもできます。

CMake であれば RelWithDebInfo でビルドすればよいです。

cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo

Makefile の場合 CXXFLAG, CFLAG-g オプションを指定します。

メモリリークを検出したい

valgrind をインストールし、引数に検査対象の起動コマンドを与えることで、メモリ異常発生時にメッセージを表示できます。

メモリ異常発生時にブレークしたい

valgrind 公式ページの手順に従い、 valgrind 内蔵の gdbserver に接続することで、異常発生箇所でブレークすることができます。

valgrind --vgdb=yes --vgdb-error=1 prog

別のターミナルで gdb を起動し、コンソールに以下コマンドを入力します。

(gdb) target remote | vgdb

vscode の場合、Valgrind Task Integration のページを参考に(この拡張機能自体のインストールは不要) C/C++ Extension の setupCommandstarget remote | vgdb を実行するよう launch.json を構成しましょう。

ポストモーテム

プログラムが落ちたあとの記録から不具合解析する手法です。
ssh 接続できない遠隔環境のサポートに有効です。

segfalut の詳細を表示する

catchsegv コマンドによって、 SEGV シグナル発生時にバックトレースを出力できます。

$ catchsegv ./segv.out
*** signal 11
Register dump:

 RAX: 0000000000000000   RBX: 0000000000000000   RCX: 0000562617907df8
 RDX: 00007ffdee1440d8   RSI: 00007ffdee1440c8   RDI: 0000000000000001

…

Backtrace:
./segv.out(+0x113d)[0x56261790513d]
./segv.out(+0x1153)[0x562617905153]
/lib/x86_64-linux-gnu/libc.so.6(+0x29d90)[0x7f0c0d603d90]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x80)[0x7f0c0d603e40]
./segv.out(+0x1065)[0x562617905065]

ファイルがデバッグ情報を含んでいれば、バックトレースのアドレスからソースコード行の特定が可能です。

$ addr2line -e ./segv.out 0x113d
segv.c:3

含んでいない場合 objdump -t で表示したシンボル情報から近い命令アドレスを探します。

なお catchsegv 及び内部で使用する libSegFault.so は最近の glibc からは外されたようです。 Ubuntu 22.04 LTS の場合 glibc-tools パッケージに移動しています。

クラッシュダンプを採取する

コアダンプが採取できると実行パスだけでなく、終了時の変数の中身まで表示することができます。ダンプファイルを送ってもらうことができれば、かなりの情報が得られます。ただしメモリ上の全データが含まれますので、機密情報を含むシステムの場合、取扱いに注意が必要です。

ulimit -c unlimited などでコアダンプのサイズを設定すると、生成が有効になります。通常実行ファイルと同階層に生成されますが、書込不可領域に配置した場合など変更したければ /proc/sys/kernel/core_pattern で設定可能です。

なお Ubuntu では Apport というクラッシュレポート機能に送られ、以下の振り分けが行われるようです。

  • /var/crash/ : パッケージ管理下のダンプ
  • /var/lib/apport/coredump/ : それ以外のダンプ

フリーズの原因を特定したい

開発環境で再現できているのであれば、 gdb --pid=<PID> で実行中のプロセスにアタッチしてデバッガを起動できます。

デバッガのない環境に対しては、フリーズしたプロセスに対して kill -s SEGV <PID> で強制的に segfault を発生させることで、上記の解析手法が使えます。

リソース調査

基本操作なので羅列のみ

使用量

  • top : CPU 使用率
  • free : メモリ消費
  • df : ディスク使用量

列挙

  • ps : プロセスID
  • lsof : 使用中ファイル
  • lsmod : カーネルモジュール

Windows

ここで紹介したコマンドの一部は Microsoft が提供する以下のツールに類似機能があります。

最後に

紹介したツールはいずれも90年代から存在する枯れたものばかりです。
しかしこれらのツールがないと、実行バイナリは、得体のしれないデータの恐ろしい塊になってしまいます。ツールの使いこなしが、迷宮入りするか否かの勝敗を分けることも少なくありません。
goやrustのようなネイティブバイナリを出力する言語の伸展に伴い、バイナリのデバッグ技法は、ますます重要になってくるのではないか、と思います。

参考

Discussion