【ISSUE】CとC++が混在したプログラムでの注意点

0

2024年12月23日 17:28

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++でのコンパイルした際の、シンボル名の違いを確認してみます。

img

img
それぞれををgccでコンパイルして、nmコマンドを使ってシンボルテーブルを確認してみます。

nmコマンド

nm は、UNIXや類似のオペレーティングシステムに存在するコマンドであり、バイナリファイル(ライブラリ、実行ファイル、オブジェクトファイル)の中身を調べ、そこに格納されているシンボルテーブルなどの情報を表示する。デバッグに使われることが多く、識別子の名前の衝突問題やC++の名前修飾の問題を解決する際に補助として用いられる。

GNUプロジェクトでは、高機能の nm プログラムを GNU Binutils パッケージの一部として提供している。この nm コマンドは他のツールと同様に特定のコンピュータ・アーキテクチャとバイナリフォーマット向けにコンパイルされているので、セキュリティ専門家は疑わしいバイナリファイルを調査するためにネイティブでない nm コマンドを事前に取り揃えておくことが多い。

img

このように、Cの方では、シンボル名 = 定義した関数名ですが、C++ではシンボル名 ≠ 定義した関数名です。これがコンパイラによって情報を付加されたシンボル名です。付加される情報はコンパイラに依存します。

コンパイラvoid h(int)void h(int, char)void h(void)
GNU GCC 3.x_Z1hi_Z1hic_Z1hv
GNU GCC 2.9xh__Fih__Fich__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__XIH__XICH__XV
OpenVMS C++ V6.5 (ANSI mode)CXX$__7H__FI0ARG51TCXX$__7H__FIC26CDH77CXX$__7H__FV2CB06E8
OpenVMS C++ X7.1 IA-64CXX$_Z1HI2DSQ26ACXX$_Z1HIC2NP3LI4CXX$_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-RISCh__Fih__Fich__Fv
Tru64 C++ V6.5 (ARM mode)h__Xih__Xich__Xv
Tru64 C++ V6.5 (ANSI mode)__7h__Fi__7h__Fic

このコンパイラによって変換されたシンボル名をデマングルします。c++filtコマンドを使うことによってデマングルすることができます。

c++filt

img

img

c++filtのオプションでコンパイラタイプを選択できるので、もし、うまくデマングルできない場合には--formatで明示的に指定してください。

img

ただ、nmコマンドには--demangleってオプションがあるので、c++filtを使う必要はないのですが :(

CのモジュールをC++から呼び出す場合に起きる問題

さてここで、本題に戻りますが、C/C++でこのような違いがある中で、それぞれの言語を混在させてコンパイルをした場合どうなるのでしょうか。マングリング自体はコンパイラが良かれと思ってしてくれるものとして、実際にこのマングリングが問題になるケースとしてあげられるのは「Cのモジュール(関数)をC++側から呼び出す」場合である。

C++からCのモジュールを呼び出した場合、CのオブジェクトファイルやライブラリをC++のオブジェクトファイルにリンクする必要があります。

元の(C++側の)オブジェクトファイルがC++なので、扱う範囲(名前空間というかリンケージ)はC++のものが適用されます。そのため、リンカにはC++対応(g++)などを使う必要があります。

リンク時に、リンカであるg++はマングリングされたシンボルを期待する。しかし、CのモジュールがCのソースであり、ソースがgccによって生成された場合には、Cソースのオブジェクトファイルのシンボルテーブルには生のシンボル(関数名)が登録されているため、以下のようにリンクを行うタイミングでビルドが失敗することになる。

img

C++から呼び出されるモジュールを"C"リンケージに登録する

Cでは「Cリンケージ」がありますが、Cを含むC++では「C++リンケージ/Cリンケージ」の2つが存在することになります。リンケージを指定しない場合には、デフォルトで「C++リンケージ」にシンボルが登録されるようになっています。そのため「extern "C" { ... }」を用いて、C++から呼び出される関数を"C"リンケージに登録する必要があります。この「extern "C"」を使うことにより、シンボル名がマングルングされる前の名前になり、C/C++混合のシステムでも正常にリンク処理を行うことができます。

img
img

img
img
このように「extern "C"」を指定した関数のみ、マングリングされる前の名前がシンボル名に指定されていることが確認できます。

C++からCモジュールを呼び出すときのまとめ

結局のところ、C++からCのモジュールを呼び出す際にはextern "C"を用いるということです。なぜなら、C++コンパイラでは、マングリングによって、シンボル名が関数名ではなくなり、Cコンパイラとの互換性がなくなってしまうからです。

一般的な使い方?

#includeするときに、そのソースごとextern "C"をかけます。つまりはこう

img

もしくは、C++コンパイラに、Cの関数でありマングリングをさせないようにしたければ、以下のようにもできます。

img

つまりは、__cplusplusが定義されている(C++コンパイラの)場合のみ、extern "C" {, }を有効にする。ということになります。
[cv:issue_marketplace_engineer]

0

診断を受けるとあなたの現在の業務委託単価を算出します。今後副業やフリーランスで単価を交渉する際の参考になります。また次の単価レンジに到達するためのヒントも確認できます。