難解なC++シンボルを即座に復元:c++filtコマンドの仕組みと活用法

概要

Linux環境下でC++バイナリの解析やスタックトレースの調査を行う際、暗号のように無作為に並んだ難解なシンボル文字列に遭遇することがよくあります。これは、関数のオーバーロードや名前空間の一意性を担保するためにコンパイラが自動処理する「名前マングリング(Name Mangling)」の結果です。本稿では、GNUユーティリティのc++filtコマンドを用い、一見判読不能なマングルシンボルを人間が直感的に理解できる関数シグネチャへ復元(デマングル)する仕組みと実務での活用手法を詳しく解説します。

c++filt環境の整備と単一シンボルの基本デコード

バイナリ解析ユーティリティ群をシステムに導入するため、apt-get update -qq && apt-get install -y binutilsを実行して準備を整えます。

apt-get update -qq && apt-get install -y binutils

ツールが正常に配置されたか確認するため、c++filt --version | head -n 2を実行してバージョン情報を取得します。
c++filtは、コンパイラによってエンコードされた低レベルな識別子を、人間が判読可能な関数名やクラス構造へと逆変換する専用ツールです。
⚙️ [主要オプション]

-_, --strip-underscore : シンボル先頭に付与されたアンダースコア(_)を除去してからデコードします。
-n, --no-strip-underscore : 先頭のアンダースコアを削除せず、そのままの状態でパースします。
-p, --no-params : 引数の型シグネチャを省略し、関数名のみを抽出して表示します。
-t, --types : 通常の関数名に加え、データ型自体の名前もデコード対象として処理します。
-s, --format : 対象バイナリが準拠するコンパイラABI仕様に合わせてデコード形式を指定します。

c++filt –version | head -n 2

まずは最小構成のシンボルをテストするため、c++filt _Z1fvを実行します。
引数を取らない関数プロトタイプであるf()へと正確に復元されることが確認できます。

c++filt _Z1fv

続いて、名前空間や引数情報が組み込まれたシンボルを処理するため、c++filt _ZN4Math3addEiiを実行します。
スコープ演算子で区切られたMath名前空間内のadd関数と、2つのint型引数が正しく解析され、Math::add(int, int)として出力されます。

c++filt _ZN4Math3addEii

C++名前マングリングの発生プロセスとnmコマンドによるシンボル抽出

C++ソースコードのコンパイル環境を整えるため、apt-get update -qq && apt-get install -y build-essentialを実行します。

apt-get update -qq && apt-get install -y build-essential

名前空間の定義および関数の多重定義(オーバーロード)を含んだ検証用コードを用意するため、cat << 'EOF' > /tmp/demo.cppを実行してファイルを作成します。

cat « ‘EOF’ > /tmp/demo.cpp
namespace Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
}
EOF

作成したソースコードをオブジェクトファイルへと変換するため、g++ -c /tmp/demo.cpp -o /tmp/demo.oを実行します。

g++ -c /tmp/demo.cpp -o /tmp/demo.o

生成されたオブジェクトファイル内部のシンボルテーブルからadd関数を走査するため、nm /tmp/demo.o | grep addを実行します。
コンパイラによるマングリング処理を受け、関数シグネチャが_ZN10Calculator3addEiiおよび_ZN10Calculator3addEddという識別子へ変換されている様子が確認できます。

nm /tmp/demo.o | grep add

これら難解なシンボルを一括して元の関数表記へ復元するため、パイプライン経由でnm /tmp/demo.o | c++filt | grep addを実行します。
c++filtを通すことで、バイナリ内のシンボルがCalculator::add(int, int)やCalculator::add(double, double)といった可読性の高いシグネチャへ瞬時に変換されます。

nm /tmp/demo.o | c++filt | grep add

複雑なテンプレート構造とパイプラインによる一括デコード

STLコンテナであるstd::vectorとstd::stringをネストさせたテンプレートクラスを検証するため、cat << 'EOF' > /tmp/template_demo.cppを実行してサンプルを配置します。

cat « ‘EOF’ > /tmp/template_demo.cpp
#include 
#include 
template 
class Storage {
public:
void print(T val) {}
};
template class Storage<std::vectorstd::string>;
EOF

シンボル解析の準備として、g++ -c /tmp/template_demo.cpp -o /tmp/template_demo.oを実行してオブジェクトファイルを生成します。

g++ -c /tmp/template_demo.cpp -o /tmp/template_demo.o

生成物からprintメンバ関数に該当するエントリを探すため、nm /tmp/template_demo.o | grep printを実行します。
テンプレートパラメータとネストされた型定義が高度に符号化された結果、目視では判読が極めて困難な長大なシンボルが出現します。

nm /tmp/template_demo.o | grep print

標準出力をパイプでc++filtへ受け渡すため、nm /tmp/template_demo.o | c++filt | grep printを実行します。
長大に符号化されていた文字列が、テンプレート実引数や完全な型情報を含んだ直感的なメンバ関数定義へとリアルタイムに復元されます。

nm /tmp/template_demo.o | c++filt | grep print

引数省略オプション(-p)の活用とエラーログの自動パース

デフォルトの挙動で完全なシグネチャを取得するため、c++filt _ZN10Calculator3addEiiを実行します。
名前空間、関数識別子に加え、末尾の引数リスト(int, int)まで包含した完全修飾形式で出力されます。

c++filt _ZN10Calculator3addEii

引数の型情報を除外し、関数名のみを簡潔に把握したい場合は、c++filt -p _ZN10Calculator3addEiiを実行します。
-pオプションの働きによりパラメータ部分が削ぎ落とされ、所属スコープと関数名であるCalculator::addのみがすっきりと抽出されます。

c++filt -p _ZN10Calculator3addEii

型定義の解決モードを明示的に指定して処理するため、c++filt -t _ZN10Calculator3addEiiを実行します。
-tオプションを指定した場合でも、関数構造と引数の型情報が損なわれることなくCalculator::add(int, int)として適切に表示されます。

c++filt -t _ZN10Calculator3addEii

クラッシュログやエラーテキストに埋もれたシンボルを自動置換するため、echo 'Fatal error in _ZN10Calculator3addEii at runtime' | c++filtを実行します。
c++filtは文脈全体の文字列を保持したまま、内部に含まれるC++シンボルのみを自動検知して可読性の高い関数名へとインライン変換してくれます。

echo ‘Fatal error in _ZN10Calculator3addEii at runtime’ | c++filt

ここまで、C++の名前マングリングが生じる背景と、それを瞬時に人間向けの表現へ戻すc++filtコマンドの実用的な使い方を解説しました。
未解決シンボルに起因するリンクエラーやバックトレースの解析が必要な場面では、ぜひc++filtをパイプラインに組み込んで調査効率を高めてみてください。