C++ Advent Calendar 2018
このカレンダーの作成者(Gacchoさん)に招待されたので書かせていただきます😇
C/C++を専門としてる者ではないので、周知されていることを書いてしまいそうですが、ご了承ください...。
前後の記事
前日の記事は、@niinaさんの「メンバ関数をもっと分ける」でした。
次日の記事は、@h6akhさんの「macOS High SierraでC++ REST SDKの導入に苦労したのでメモを残す」です。
CとC++の混在は難しい
なんせ自分もそうだったんですが、CとC++が混在しているプログラムを分割コンパイルし、オブジェクトファイルから実行ファイルを作る時に、時々定義したはずの関数が undefined となりコンパイルに失敗することがありました。
C++コンパイラ
よくよく調べていると、C++コンパイラでは名前マングリング
と呼ばれる処理を行なっているようです。これは名前修飾
とも呼ばれているようで、
名前修飾(なまえしゅうしょく、name mangling)は、現代的なコンピュータプログラミング言語処理系で用いられている手法で、サブルーチン(関数)名などに対する内部名を、その表層的な名前だけではなく、関数であればその引数の型や返戻値の型などといった意味的な情報を含めて修飾した(manglingした)名前とするものである。
つまりは、ソース上で書いた関数名をそのままシンボル名にするのではなく、「引数の型」「返り値の型」など、その関数に関連する情報も追加したものを「シンボル名」にするということです。
なのでCとC++でのコンパイルした際の、シンボル名の違いを確認してみます。
#include <iostream>
void example_function();
int main ( void ) {
example_function();
return 0;
}
#include <stdio.h>
void example_function();
int main ( void ) {
example_function();
return 0;
}
それぞれををgcc
でコンパイルして、nm
コマンドを使ってシンボルテーブルを確認してみます。
nmコマンド
nm は、UNIXや類似のオペレーティングシステムに存在するコマンドであり、バイナリファイル(ライブラリ、実行ファイル、オブジェクトファイル)の中身を調べ、そこに格納されているシンボルテーブルなどの情報を表示する。デバッグに使われることが多く、識別子の名前の衝突問題やC++の名前修飾の問題を解決する際に補助として用いられる。
GNUプロジェクトでは、高機能の nm プログラムを GNU Binutils パッケージの一部として提供している。この nm コマンドは他のツールと同様に特定のコンピュータ・アーキテクチャとバイナリフォーマット向けにコンパイルされているので、セキュリティ専門家は疑わしいバイナリファイルを調査するためにネイティブでない nm コマンドを事前に取り揃えておくことが多い。
$ gcc -c test.c
$ nm c.o
U _example_function
0000000000000000 T _main
$ gcc -c test.cpp
$ nm c.o
U __Z16example_functionv
0000000000000000 T _main
このように、Cの方では、シンボル名 = 定義した関数名
ですが、C++ではシンボル名 ≠ 定義した関数名
です。これがコンパイラによって情報を付加されたシンボル名です。付加される情報はコンパイラに依存します。
コンパイラ | void h(int) | void h(int, char) | void h(void) |
---|---|---|---|
GNU GCC 3.x | _Z1hi | _Z1hic | _Z1hv |
GNU GCC 2.9x | h__Fi | h__Fic | h__Fv |
Intel C++ 8.0 for Linux | _Z1hi | _Z1hic | _Z1hv |
Microsoft VC++ v6/v7 | ?h@@YAXH@Z | ?h@@YAXHD@Z | ?h@@YAXXZ |
Borland C++ v3.1 | @h$qi | @h$qizc | @h$qv |
OpenVMS C++ V6.5 (ARM mode) | H__XI | H__XIC | H__XV |
OpenVMS C++ V6.5 (ANSI mode) | CXX$__7H__FI0ARG51T | CXX$__7H__FIC26CDH77 | CXX$__7H__FV2CB06E8 |
OpenVMS C++ X7.1 IA-64 | CXX$_Z1HI2DSQ26A | CXX$_Z1HIC2NP3LI4 | CXX$_Z1HV0BCA19V |
Digital Mars C++ | ?h@@YAXH@Z | ?h@@YAXHD@Z | ?h@@YAXXZ |
SunPro CC | _1cBh6Fi_v | _1cBh6Fic_v | _1cBh6F_v |
HP aC++ A.05.55 IA-64 | _Z1hi | _Z1hic | _Z1hv |
HP aC++ A.03.45 PA-RISC | h__Fi | h__Fic | h__Fv |
Tru64 C++ V6.5 (ARM mode) | h__Xi | h__Xic | h__Xv |
Tru64 C++ V6.5 (ANSI mode) | __7h__Fi | __7h__Fic |
このコンパイラによって変換されたシンボル名をデマングルします。c++filt
コマンドを使うことによってデマングルすることができます。
c++filt
#include <iostream>
void test_function ();
void test_function2 ( int val );
void test_function3 ( double val );
int test_function4 ( float val );
int main ( void ) {
test_function ();
test_function2 (1);
test_function3 (1.0);
test_function4 (1.0);
return 0;
}
$ gcc -c test.cpp
$ nm test.o
U __Z13test_functionv
U __Z14test_function2i
U __Z14test_function3d
U __Z14test_function4f
0000000000000000 T _main
$ nm test.o | c++filt
U test_function()
U test_function2(int)
U test_function3(double)
U test_function4(float)
0000000000000000 T _main
c++filtのオプションでコンパイラタイプを選択できるので、もし、うまくデマングルできない場合には--format
で明示的に指定してください。
Usage: c++filt [options] [mangled names]
Options are:
[-_|--strip-underscore] Ignore first leading underscore (default)
[-n|--no-strip-underscore] Do not ignore a leading underscore
[-p|--no-params] Do not display function arguments
[-i|--no-verbose] Do not show implementation details (if any)
[-t|--types] Also attempt to demangle type encodings
[-s|--format {none,auto,gnu,lucid,arm,hp,edg,gnu-v3,java,gnat}]
[@<file>] Read extra options from <file>
[-h|--help] Display this information
[-v|--version] Show the version information
Demangled names are displayed to stdout.
If a name cannot be demangled it is just echoed to stdout.
If no names are provided on the command line, stdin is read.
ただ、nm
コマンドには--demangle
ってオプションがあるので、c++filt
を使う必要はないのですが :(
CのモジュールをC++から呼び出す場合に起きる問題
さてここで、本題に戻りますが、C/C++でこのような違いがある中で、それぞれの言語を混在させてコンパイルをした場合どうなるのでしょうか。マングリング自体はコンパイラが良かれと思ってしてくれるものとして、実際にこのマングリングが問題になるケースとしてあげられるのは「Cのモジュール(関数)をC++側から呼び出す」場合である。
C++からCのモジュールを呼び出した場合、CのオブジェクトファイルやライブラリをC++のオブジェクトファイルにリンクする必要があります。
元の(C++側の)オブジェクトファイルがC++なので、扱う範囲(名前空間というかリンケージ)はC++のものが適用されます。そのため、リンカにはC++対応(g++)などを使う必要があります。
リンク時に、リンカであるg++はマングリングされたシンボルを期待する。しかし、CのモジュールがCのソースであり、ソースがgccによって生成された場合には、Cソースのオブジェクトファイルのシンボルテーブルには生のシンボル(関数名)が登録されているため、以下のようにリンクを行うタイミングでビルドが失敗することになる。
g++ -Wall run.cc libhello.a -o run
/tmp/ccrKBE8s.o: In function `main':
run.cc:(.text+0x10): undefined reference to `C_SOURCE_FUNCTION()'
collect2: error: ld returned 1 exit status
make: *** [run] Error 1
C++から呼び出されるモジュールを"C"リンケージに登録する
Cでは「Cリンケージ」がありますが、Cを含むC++では「C++リンケージ/Cリンケージ」の2つが存在することになります。リンケージを指定しない場合には、デフォルトで「C++リンケージ」にシンボルが登録されるようになっています。そのため「extern "C" { ... }」を用いて、C++から呼び出される関数を"C"リンケージに登録する必要があります。この「extern "C"」を使うことにより、シンボル名がマングルングされる前の名前になり、C/C++混合のシステムでも正常にリンク処理を行うことができます。
#include <iostream>
void test_function ();
void test_function2 ( int val );
void test_function3 ( double val );
int test_function4 ( float val );
void test_function5 ( int a );
int main ( void ) {
test_function ();
test_function2 (1);
test_function3 (1.0);
test_function4 (1.0);
test_function5 (1);
return 0;
}
$ gcc -c test.cpp
$ nm test.o
U __Z13test_functionv
U __Z14test_function2i
U __Z14test_function3d
U __Z14test_function4f
U __Z14test_function5i
0000000000000000 T _main
#include <iostream>
extern "C" void test_function ();
extern "C" void test_function2 ( int val );
void test_function3 ( double val );
int test_function4 ( float val );
extern void test_function5 ( int a );
int main ( void ) {
test_function ();
test_function2 (1);
test_function3 (1.0);
test_function4 (1.0);
test_function5 (1);
return 0;
}
$ gcc -c extern_test.cpp
$ nm extern_test.o
U __Z14test_function3d
U __Z14test_function4f
U __Z14test_function5i
0000000000000000 T _main
U _test_function
U _test_function2
このように「extern "C"」を指定した関数のみ、マングリングされる前の名前がシンボル名に指定されていることが確認できます。
C++からCモジュールを呼び出すときのまとめ
結局のところ、C++からCのモジュールを呼び出す際にはextern "C"
を用いるということです。なぜなら、C++コンパイラでは、マングリングによって、シンボル名が関数名ではなくなり、Cコンパイラとの互換性がなくなってしまうからです。
一般的な使い方?
#include
するときに、そのソースごとextern "C"
をかけます。つまりはこう
extern "C" {
#include "aaaaaa.h"
#include "bbbbbb.h"
}
もしくは、C++コンパイラに、Cの関数でありマングリングをさせないようにしたければ、以下のようにもできます。
#ifdef __cplusplus
extern "C" {
#endif
void func1();
#ifdef __cplusplus
}
#endif
つまりは、__cplusplus
が定義されている(C++コンパイラの)場合のみ、extern "C" {
, }
を有効にする。ということになります。