name mangling と extern "C"

Linkers & Loaders


さて、T君と飲んだ理由は、昼間にオシゴトの技術的な相談に乗ってあげたからなのだが…。平日昼に急に携帯に電話がかかってきてビックリした。曰く、

ずっとJ2EEなコードを書いて暮らしていたのだが、わけあって急にVxWorksC++で組み込み機器向けのコードを書くことになった。が、組み込みやC++に詳しい人間が全く居なくて難航している。


現状誤動作しており、とりあえず早急に修正しなければならないコードがあるのだが、誤動作の原因がさっぱりわからない。相談に乗ってもらえないか。ていうか助けて。コンパイラGNUのを使っている。Cを学んだ事はある。

へいへいOKOK。

構造体Aがある。この構造体はa.hで定義されている。

構造体Aを操作する関数fooがある。fooのプロトタイプはfoo.hに書いてあり、int foo(A* a); となっている。foo.cppで関数fooが実装されている。

今問題を起こしているのはbar.cppに書かれた関数barである。barの中の、foo関数呼び出しのあたりでクラッシュする。bar.cppは、何故だかfoo.hではなく、foo2.hをインクルードしており、foo2.hには関数fooのプロトタイプが int foo(A a); と書かれている。どの .cpp を見ても、int foo(A a); が実装されている様子はなく、int foo(A* a); の実装しかない。

foo.h と foo2.h にヘッダがわかれてしまっている理由はわからない。foo2.h のプロトタイプはおそらくtypoだろうということになりつつある。コンパイルもリンクも成功し、実行時に暴走する。

わからないのは、

  • コンパイルが通るのはともかく、なぜリンク時にエラーにならないのか
  • リンクが成功してしまっているからには、bar関数はfoo.cppの関数fooを呼んでしまっているということか

の二点。

だそうだ。foo2.h のtypoを修正すれば直るのかもしれないが、リンクが成功して暴走したリクツがわからないと不安とのこと。


「extern "C"」と電話口で一言で答えるのはちょっとムリがあったので、「foo2.h のどこかに extern "C" って書いてあるよね?」とだけ予め確認し、30分ほど仕事を休んでメールで回答*1


<以下彼に書いたメールをなんとなく転記>---------------------------------------


現状、foo.cppの実装が呼ばれてその中でクラッシュしていると思う。ヘッダファイルfoo2.hのプロトタイプ int foo(A a); を int foo(A* a); に修正すれば治ると思う。修正しなくてもリンクが成功してしまう原因は…ここから先に書くけどちょっと長くなるよ〜


■ name mangling とシンボル名


CやC++の「関数名」は、オブジェクトファイルの中では「シンボル名」に変化する。


まずCのソースコードの場合は、次のようにごく単純な変換規則になる。

--------------------------------+---------------
C関数名(というかシグネチャ)     | シンボル名
--------------------------------+---------------
int foo(const char* pBar)       | foo
--------------------------------+---------------

つまり、引数や戻り値の情報はシンボル名には反映されないということ。シンボル名がfooじゃなくて_fooになったりすることもあるけど、その程度のもの。


次、C++の場合。

--------------------------------+---------------
C++関数名(というかシグネチャ)   | シンボル名
--------------------------------+---------------
int foo(const char* pBar)       | _Z3fooPKc
--------------------------------+---------------

こんな感じにグチャグチャなシンボル名になる。グチャグチャで人間には読めないんだけど、関数名のほかに、引数の個数とそれぞれの型の情報を含むようになっている*2。関数名をこういうグチャグチャなシンボル名にすることを、name mangling という。なんでシンボル名に引数情報を含める必要があるかというと、C++には関数のオーバーロードがあるから。mangleしないと、int foo(void) と int foo(int a) のシンボル名が衝突してしまい、多重定義ができない。なお、メンバ関数の場合は属するクラス名の情報がシンボルに含められる。


name mangling の具体的なアルゴリズムは、コンパイラとそのバージョンによっていろいろで、ISOやなんかで標準化は、残念ながらされていない。



■ extern "C" の罠


ここまでで、C++のname manglingについて解説した。た・だ・し、「C++言語で書いた関数をC言語から呼びたい(あるいはその逆)」というニーズが常にあるので、C++コンパイラに、「name manglingをせず、C言語方式に関数名をそのままシンボル名に汁!」と指令する方法がある。それが extern "C" という奴。


次のコード

extern "C" {
  int foo(int a) {}
}

コンパイルすると、オブジェクトファイルに含まれるシンボル名は "foo" になる。また、

-[foo.h]-------
extern "C" {
  int hogehoge(int a);  // C言語で開発された関数のプロトタイプ
}
---------------

-[foo.cpp]-----
int foo(void) {
  return hogehoge(1);
}
---------------

なんてコードを書くと、foo.cppのhogehoge関数呼び出しは、アセンブラレベルで見ると

call _Z8hogehogei

ではなく、

call hogehoge

になる。



■ シンボル名のダンプ方法


UNIX(というかGNUのtoolchain)を使っている場合は、次のようにするとシンボル名が見られる。

-[test.cpp]-----------------------
struct A {
    int a;
    int b;
};

int foo(A a) {}
int foo(A* a) {}
------------------------------------
$ g++ -c test.cpp
$ nm test.o
00000000 T _Z3foo1A
00000014 T _Z3fooP1A

ただ、このままだとmangleされていて人間にはなんのことだかわからんので、

$ nm test.o | c++filt
00000000 T foo(A)
00000014 T foo(A*)

とすると、demangleされて読みやすくなる*3。上を見ると、構造体をポインタで渡すか値で渡すかでシンボル名が違うのがわかる。


ライブラリに含まれるシンボル名一覧が欲しい場合、

$ nm -g /usr/lib/libstdc++.so.2.8.0 | egrep -ie " (W|T) " | c++filt

のようにする(/usr/lib/libstdc++.so.2.8.0 のところは使っているライブラリ名に置き換えること)。ファイル名はlibで始まり、拡張子は .so または .a のことがUNIXだと多いのだが、VxWorksはどうなんだろ。

$ nm -gol /usr/lib/lib*.a | egrep -ie " (W|T) " | c++filt | grep 探したい関数名

のようにすると、あるシンボルの含まれるライブラリ名(-o)と、場合によっては.c/.cppファイル名(-l)までわかって便利なこともある。


一点だけ注意が。ある種類の(=stripされた).soファイルを単純にnmすると、"no symbols" と言われて望む出力が得られないことがある。こういう場合、nm -D /path/to/libsome.so とすればよい。stripすると"normal symbol table"はマッサラになってしまうが、"dynamic symbol table" は消えないので*4そちらを参照するようにnmに伝えるということ。objdump -T でも良いが、本用途にはちと出力が多すぎる気が。


■ 怪奇現象を再現するサンプル

-[test_a.h]-----------
// 構造体定義
struct A {
        int mem1;
        int mem2;
};
------------------------
-[test.cpp]-----------------------------
// 呼ばれ側関数の実装
#include "test_a.h"

extern "C" {
        int foo(A* a) {
                return a->mem1 + a->mem2;
        }
}
------------------------------------------
-[test_byval.h]----------------------------
// 呼び側がインクルードするヘッダファイル
extern "C" {
        // シグネチャ間違ってる!! けどextern C
        int foo(A a);
}
---------------------------------------------
-[main.cpp]----------------------------------
// 呼び側関数
#include "test_a.h"
#include "test_byval.h"

int main(void) {
        A a;
        a.mem1 = 100;
        a.mem2 = 200;
        return foo(a);
}
---------------------------------------------

コンパイル&リンク&実行してみるテスト↓

$ g++ -c test.cpp         (呼び出され側コンパイル)
$ g++ -c main.cpp         (呼び側コンパイル)
$ g++ test.o main.o       (リンク -> 成功しやがる!)
$ ./a.out                 (実行)
Segmentation fault        (クラッシュ)

*1:日本で3本の指に楽勝で入る電機メーカーの部署で働いていて、分かる人が全くいないってちょっと末期的だなぁと思いつつ…

*2:戻りの型の情報は含まれない

*3:おっと、nm --demangle test.o のほうがスマートですね。訂正。

*4:消えてしまったら動的なリンク処理ができなくなる