Manglingルールは、ソースコード上での名前をエンコードし、プログラム全体で一意な名前を生成するためのルールである。関数名や型名などで適用され、名前の衝突を防ぐことができる。コードの構造からファイルシステムで言うフルパスのようなものを求めると考えてもいいかもしれない。エンコード(Mangle)したものは「シンボル」と呼ばれる。C++で開発されたアプリケーションをリバースエンジニアリングするにあたってこのルールについて学んだのでまとめようと思う。
Itanium C++ ABI
Application Binary Interfaceは、言語をコンピュータ上で動作させるための実装方法を定める。また、アプリケーション同士の互換性についてもバージョンによる変更を含めて定義されることがある。C++ではABIはコンパイラによって異なり、Itanium ABIはGCCで使用されている。この記事ではドキュメントのExternal Namesを見ていく。
※ベンダー向けに提供されている構文は無視します
Symbol
異なるファイルに存在する関数などを呼び出すためアドレス解決が必要になる。このような実行時にアドレス解決を行う必要がある場合にシンボルが使われる。C++ではCと違い、多重定義や実行時型情報が存在するためソースコード上での名前をそのままでは使用できない。コンパイルして生成されたオブジェクトファイルをリンクする際にアドレス解決が行われるため、通常実行ファイル自体にシンボルは必要無い。シンボルは主にライブラリで使用される。
Mangling Rule
BNF記法で記述されている。ドキュメントでは純粋なBNFから表現が一部拡張されているので注意。
- 非終端記号は<>で囲まれている
- 非終端記号においてイタリック体で記述されている部分は無視する
- <function name>は<name>と同じ。(ここでは判別しやすいよう記号の名前は太字にしてます)
- スペースは無視する。見やすさのためにスペースを入れてるだけ
- #から行末まではコメント
- []で囲まれた部分は省略可能
- ()で囲まれた部分はグループ
- +, *は正規表現のリピートと同じ
- 上記以外の文字はすべて終端記号で、その文字自体が記述される。
General Structure
<mangled-name> ::= _Z <encoding>
全てのシンボルは"_Z"から始まる。マングルされたC++のシンボルを識別するために使おう。encodingがシンボルの本体と言える。
<encoding> ::= <function name><bare-function-type>
<encoding> ::= <data name>
<encoding> ::= <special-name>
encodingは3つのパターンがある。さっそくここでBNF記法の曖昧な部分が襲ってくる。デマングルのためのパーサを開発する際はメモ化などで対処しよう…
1つ目の構文は、関数を表す。
2つ目の構文は、クラスや変数などの関数以外の名前を表す。
3つ目の構文は、vtableと実行時型情報に関して表す。
Terminal
非終端記号に深く入る前に終端記号を確認しておこう。
<number> ::= [n]<_non-negative decimal integer_>
整数値を表す。先頭に'n'をつけると負の数値になる。
<float> ::= <_fixed-length lowercase hexadecimal string_>
浮動小数点数は16進文字列でビッグエンディアンで表記する。
<source-name> ::= <positive length number><identifier>
<identifier> ::= <_unqualified source code identifier_>
identifierは_a-zA-Z0-9からなる文字列。source-nameは実際に名前を保持するエンティティ。
Non-Terminal
非終端記号とは構文の中に別の終端記号または非終端記号を含む記号である。
Names
<name> ::= <nested-name>
<name> ::= <unscoped-name>
<name> ::= <unscoped-template-name><template-args>
<name> ::= <local-name>
encodingの次に重要な非終端記号である。
名前空間やクラスのように親を持つ名前はnested-nameで表される。よって、グローバルスコープまたはstd名前空間で宣言された名前はunscoped-nameとして表される。また、unscoped-template-nameはunscoped-nameの拡張である。ローカルクラスのメンバを含む関数内で宣言でされた名前はlocal-nameで表される。
<unscoped-name> ::= <unqualified-name>
<unscoped-name> ::= St<unqualified-name>
<unscoped-template-name> ::= <unscoped-name>
<unscoped-template-name> ::= <substitution>
nameは基本的にunqualified-nameで構成される。Stは"::std::"の省略で、St6vectorは::std::vectorとなる。std名前空間は特別扱いされていて、nested-nameを用いるより短く表記できる。シンボル内に同一の名前が複数存在する場合subsutitionによって省略される。subsutitionには番号がつけられていてパースの順番に依存しているので注意が必要である。
おわりに
マングルされたシンボルに必ずと言っていいほど出現する基礎構文は以上だ。次回があればシンボルのディテールを表現する非常に細かく定義された構文を解説したいと思う。