nmコマンドでC/C++のシンボルテーブルを見る、C++の名前マングリング、"C"リンケージ、あるいはリンカに関するメモ
前回(g++)と前々回(gcc)のサンプルコードを使ったC++のマングリングや"C"リンケージに関するメモ.
nmコマンドでサンプルコードのシンボルテーブルを覗いた後に、C++からCの関数を呼び出す場合のサンプルコードを示す.
特にリンケージ周りの説明は正確性を欠いているやもしれませんのでご容赦下さい.より詳細な説明は参考リンク等を参照して頂ければと思います.
nmコマンド
nm は、UNIXや類似のオペレーティングシステムに存在するコマンドであり、バイナリファイル(ライブラリ、実行ファイル、オブジェクトファイル)の中身を調べ、そこに格納されているシンボルテーブルなどの情報を表示する。デバッグに使われることが多く、識別子の名前の衝突問題やC++の名前修飾の問題を解決する際に補助として用いられる。
GNUプロジェクトでは、高機能の nm プログラムを GNU Binutils パッケージの一部として提供している。この nm コマンドは他のツールと同様に特定のコンピュータ・アーキテクチャとバイナリフォーマット向けにコンパイルされているので、セキュリティ専門家は疑わしいバイナリファイルを調査するためにネイティブでない nm コマンドを事前に取り揃えておくことが多い。
今回はこいつを使って、サンプルコードのコードのシンボルテーブルを見たり、C++からCの関数を呼び出す場合に起こる問題等について見ていく.
Cのシンボルテーブル
前々回のgccで生成されたバイナリファイル(hello.o, libhello.a, run)に対してnmコマンドを叩いた結果を以下に示す.
$ nm hello.o 0000000000000000 T print_hello U puts $ nm libhello.a hello.o: 0000000000000000 T print_hello U puts $ nm run 0000000000600728 d _DYNAMIC 0000000000600910 d _GLOBAL_OFFSET_TABLE_ 00000000004005f8 R _IO_stdin_used w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable w _Jv_RegisterClasses 0000000000400708 r __FRAME_END__ 0000000000600720 d __JCR_END__ 0000000000600720 d __JCR_LIST__ 0000000000600948 D __TMC_END__ 0000000000600948 A __bss_start 0000000000600938 D __data_start 00000000004004c0 t __do_global_dtors_aux 0000000000600718 t __do_global_dtors_aux_fini_array_entry 0000000000600940 D __dso_handle 0000000000600710 t __frame_dummy_init_array_entry w __gmon_start__ 0000000000600718 t __init_array_end 0000000000600710 t __init_array_start 0000000000400550 T __libc_csu_fini 0000000000400560 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 0000000000600948 A _edata 0000000000600950 A _end 00000000004005ec T _fini 00000000004003b8 T _init 0000000000400400 T _start 000000000040042c t call_gmon_start 0000000000600948 b completed.6092 0000000000600938 W data_start 0000000000400450 t deregister_tm_clones 00000000004004e0 t frame_dummy 000000000040050c T main 000000000040052c T print_hello <<============== ココ!!関数名がそのままシンボルテーブルに載っている U puts@@GLIBC_2.2.5 0000000000400480 t register_tm_clones
よく知られているように、Cでは関数の通用範囲がファイル単位で管理される.
関数の通用範囲に話を絞って言えば、(関数の最初の外部宣言に)static修飾子が付いている関数は通用範囲がそのファイル内部のみとなり、これを内部リンケージと言う.
そうでない関数(static修飾子がついていない通常の関数)は、通用範囲がグローバルな外部リンケージとなり、全ての箇所から参照可能となる.
詳しくはK&Rの付録A(A10.2, A11.2)を参照するかググってもらうとして、Cではいわゆる(関数の)名前空間が内部リンケージと外部リンケージのざっくり2種類しかない.
上記のnmコマンドの結果を見てみると、print_helloという名前がシンボルテーブルに登録されていることがわかる.
print_helloは外部リンケージとして公開されており、関数のプロトタイプ宣言があれば、他のファイルから正確に一意に識別することができる.
リンケージに限らず分割コンパイル全般について、以下で詳細に解説されており非常に参考になる.
C++のシンボルテーブル
一方、前回のg++によって生成されたバイナリファイル(hello.o, libhello.a, run)に対してもnmコマンドを叩いた結果を見てみると、
$ nm hello.o 0000000000000000 T _Z11print_hellov U puts $ nm libhello.a hello.o: 0000000000000000 T _Z11print_hellov U puts $ nm run 00000000006007d8 d _DYNAMIC 00000000006009f0 d _GLOBAL_OFFSET_TABLE_ 00000000004006a8 R _IO_stdin_used w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable w _Jv_RegisterClasses 00000000004005e8 T _Z11print_hellov <<============== ココ!!関数名の前後に文字が追加されている 00000000004007b8 r __FRAME_END__ 00000000006007d0 d __JCR_END__ 00000000006007d0 d __JCR_LIST__ 0000000000600a28 D __TMC_END__ 0000000000600a28 A __bss_start 0000000000600a18 D __data_start 0000000000400580 t __do_global_dtors_aux 00000000006007c8 t __do_global_dtors_aux_fini_array_entry 0000000000600a20 D __dso_handle 00000000006007c0 t __frame_dummy_init_array_entry w __gmon_start__ 00000000006007c8 t __init_array_end 00000000006007c0 t __init_array_start 0000000000400600 T __libc_csu_fini 0000000000400610 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 0000000000600a28 A _edata 0000000000600a30 A _end 000000000040069c T _fini 0000000000400480 T _init 00000000004004c0 T _start 00000000004004ec t call_gmon_start 0000000000600a28 b completed.6092 0000000000600a18 W data_start 0000000000400510 t deregister_tm_clones 00000000004005a0 t frame_dummy 00000000004005cc T main U puts@@GLIBC_2.2.5 0000000000400540 t register_tm_clones
関数print_helloがシンボルテーブル上では_Z11print_hellovという名前にリネームされていることがわかる.
C++のマングリング
C++では言語機能として、関数のオーバーロードや、名前空間が備わっているため、Cのような単純な通用範囲の決定方法では名前の重複が起こってしまう.
そのような問題がを回避するために行われているのが名前マングリングである.
参考:C と C++ 近いようで遠い存在 - debian36の日記
CのモジュールをC++から呼び出す場合に起きる問題
マングリング自体はコンパイラが良きに計らってくれるものとして、実際にこのマングリングが問題になるケースがCのモジュール(関数)をC++側から呼び出す場合である.
C++からCのモジュールを呼び出した場合、CのオブジェクトファイルやライブラリをC++のオブジェクトファイルにリンクする必要がある.
元の(呼び出し側の)オブジェクトファイルがC++なので、扱う通用範囲というか名前空間というかリンケージはC++のもの*1が適用されることから、リンカにはg++を使う必要がある*2.
この時(リンク時)、リンカであるg++*3は上記の例で言えば、hello.oやlibhello.aに、例えば_Z11print_hellovのようなマングリングされたシンボルを期待する.
しかし、hello.oの元のコードがCのソースであり、hello.oがgccによって生成された場合には、hello.oのシンボルテーブルには生のprint_helloが登録されているため、以下のようにリンクを行うタイミングでビルドが失敗することになる.
$ make gcc -Wall -c -o hello.o hello.c ar rv libhello.a hello.o ar: creating libhello.a a - hello.o g++ -Wall run.cc libhello.a -o run /tmp/ccrKBE8s.o: In function `main': run.cc:(.text+0x10): undefined reference to `print_hello()' collect2: error: ld returned 1 exit status make: *** [run] Error 1
このような問題に当たって初めてリンカの存在をはっきりと認識するに至る.
リンカのエラーを見て「プログラマが知るべき97のこと」の「リンカは魔法のプログラムではない」を思い出した.
94 リンカは魔法のプログラムではない
ウォルター・ブライト(Walter Bright)
'' リンカは実はさほど難しいプログラムではありません。むしろ単純でわかりやすいプログラムです。することといえば、オブジェクトファイルのコードセクション、データセクションを連結し、定義されているシンボルと参照を接続し、ライブラリから未解決シンボルを抽出し、実行ファイルを書き出す、それだけです。とても単純で、魔法でも何でもありません。なぜリンカが難しく感じるかといえば、処理の結果できるファイルのフォーマットが驚くほど複雑で、そのままで解読することは難しいからでしょう。だからと言って、リンカ自体が複雑な処理をしているというわけではないのです。(...中略...)リンカがメッセージを出した時、その原因がすぐにはわからないことも確かにあります。しかし、それでも、リンカが特に複雑な処理をしているわけではないということは常に念頭に置くべきでしょう。処理自体は簡単だけれども、その結果できるものが複雑で、詳しく検証するのがなかなか面倒ということなのです。"
(※ 書籍「プログラマが知るべき97のこと」の内容をライセンスCC BY 3.0の元で一部抜粋)
話を元に戻して、このCのオブジェクトファイルとC++のオブジェクトファイルで、シンボルテーブルに登録されている関数の識別子が一致しない問題を回避するには、C++側で"C"リンケージにprint_helloを登録する、ということをしなければならない*4.
Cのモジュールを"C"リンケージに登録する
C++では、少なくとも"C++"リンケージと"C"リンケージがある.
デフォルトでC++リンケージにシンボルが登録されるため、extern "C"で呼び出す関数を"C"リンケージに指定しておくと、リンカが期待するシンボル名がマングリング前の名前(print_hello)になり、リンクする際にCのモジュールを含むオブジェクトファイルやライブラリと不整合を起こさなくなる.
Cからの外部結合の指定に加え、リンケージ指定の用法が加わっている。 C++では名前(関数名や変数名など)に対して多重定義や名前空間、型安全の保障などの都合から、多くのコンパイラは名前修飾を施しCとは異なった名前をリンカに対して用いている。その名前の修飾の仕方を指定するのがリンケージ指定である。少なくとも"C"と"C++"の2種類のリンケージが使用できる。何も指定しないと"C++"になる。"C"リンケージでは名前の変形を抑止しCと互換の名前をリンカに対して用いることを意味する。これによりCとC++を混在させてプログラムを作るときに使われる。
実際に、gccとg++が混在する場合のサンプルコードを示す.
makeでライブラリ(.a)のビルドを管理するサンプル(gccとg++の混在ver)
makeを叩くと以下のようにgccとg++が混在するにも関わらずビルドが上手く言っていることがわかる.
当然、hello.oやrunにnmコマンドを叩くとシンボルテーブルにはマングリング前のprint_helloが登録されている.
$ make gcc -c -o hello.o hello.c ar rv libhello.a hello.o ar: creating libhello.a a - hello.o g++ -Wall run.cc libhello.a -o run
この場合、hello.hはC++であるrun.ccからもCであるhello.cからも#includeされているため、記号定数__cplusplus
によって、C++の場合にだけ"C"リンケージ指定を行うようにしなければならない.
もし、Cのモジュールがそんなに大きなものでない場合には、Cのコードもg++でコンパイルしてしまう手もあるが、Cのモジュールが巨大なライブラリであり、ソースコードが手元にあるとしても、g++でコンパイルしようとすると大量の修正*5が必要になるという場合には、上記の方法でリンクすることができる.
参考:C と C++ 近いようで遠い存在 その2 - debian36の日記
- 作者: 和田卓人,Kevlin Henney,夏目大
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/12/18
- メディア: 単行本(ソフトカバー)
- 購入: 58人 クリック: 2,107回
- この商品を含むブログ (343件) を見る
二十五日半狂乱、10日目(の分)の記事