構造体のサイズ違いで悩んだのでリンク時に検出


先日、次のようなC++のクラスが原因で少々悩みました。

struct A {
#ifdef V2
  int bar_; /* バージョン2以降でしか使わないメンバ変数(らしい) */
#endif
  int foo_;
  /* 以下略 */
};

このAを使っている.cppの一部は、g++ -DV2でコンパイルされており、残りは-DV2なしでコンパイルされていたのです。sizeof(A)が異なるオブジェクト同士がリンクされてしまい、まずいことになっていました。たとえば、次のようなa.hと、

// a.h 
struct A {
#ifdef V2
  int bar_;
#endif
  int foo_;

  A();
  int getFoo() const { return foo_; }
  void setFoo(int foo);
};

a.cpp, main.cpp を用意して、

// a.cpp
#include "a.h"

A::A() :
#ifdef V2
  bar_(0),
#endif
  foo_(0) {}

void A::setFoo(int foo) {
  foo_ = foo;
}
// main.cpp
#include "a.h"
#include <cstdio>

int main() {
  A a;
  a.setFoo(123);
  std::printf("foo=%d\n", a.getFoo());
}

一方の.cppにしか -DV2 をあたえないでコンパイルを行うと、

% g++ -DV2 -c a.cpp       <-- sizeof(A) == 8
% g++      -c main.cpp    <-- sizeof(A) == 4
% g++ a.o main.o

% ./a.out 
foo=0       <--- 123ではなく0が出力される

main()の2行目でセットした値123がどこかに消えてしまう結果となります。この場合ですと、setFoo()ではきちんとA::foo_に値をセットしているものの、getFoo()では、誤って A::bar_ から値を読む結果になってしまっています。なお、操作する相手が整数でなくポインタだったりすると、不可解なクラッシュが起きたりすると思います。まぁ、(これも)よくある事態な気がしますが、構造体Aを書いたのが自分ではなく、また今回は馴染の無いビルドシステムを相手にしていたこともあり、原因がここだと判明するまでに少し時間がかかってしまったのでした。


(追記) あ、今たまたま眺めたGCCWiki (http://gcc.gnu.org/wiki/Visibility) にも、... if you forget your preprocessor defines in just one object file ... なんて記述があるなぁ。やはりハマるのは私だけではないのだなー。


というわけで、また同じ事で悩むのが嫌なので、「ひとつのクラス/構造体Aについて、sizeof(A)の値が異なる.o同士がリンクされたこと」を自動的に検出できないか考えました。手を入れるのはクラス/構造体の定義された.hと.cppだけという条件で。

構造体のサイズ違いの自動検出 (ランタイム)


まず、ランタイムに検出する方法を考えました。これはごく簡単に実現できそうです。

// a.hへ追記
__attribute__((constructor)) static void size_checker() {
  extern std::size_t sizeof_A;
  assert(sizeof_A == sizeof(A));
}
// a.cppに追記
std::size_t sizeof_A = sizeof(A); // 正しい(基準となる)サイズを覚える

これで、もしsizeof(A)の値が異なる.o同士がリンクされていると、実行時にmain()の前でチェックが行われ、問題があればassertで止まります。止まった場所をgdbのbacktraceやtracefなどで :-) 調べれば、原因箇所がわかるはずです。


例:

% g++ -DV2 -ggdb3 -c a.cpp 
% g++      -ggdb3 main.cpp a.o   <-- -DV2をわざと付け忘れてみる

% ./a.out                        <-- 実行時にmain()より前にassertで止まる
a.out: a.h:22: void size_checker(): Assertion `sizeof_A == sizeof(A)' failed.

% tracef -ClT ./a.out            <-- どこで止まったか調べる (main.cppのコンパイル方法がまずいとわかる)
[pid 1613] +++ process 1613 attached (ppid 1612) +++
[pid 1613] === symbols loaded: './a.out' ===
[pid 1613] ==> _start() at 0x00000000004004f0
[pid 1613]    ==> __libc_csu_init() at 0x00000000004006f0
[pid 1613]       ==> _init() at 0x0000000000400480
[pid 1613]          ==> call_gmon_start() at 0x000000000040051c
[pid 1613]          <== call_gmon_start() [rax = 0x0]
[pid 1613]          ==> frame_dummy() at 0x00000000004005a0
[pid 1613]          <== frame_dummy() [rax = 0x0]
[pid 1613]          ==> __do_global_ctors_aux() at 0x0000000000400780
[pid 1613]             ==> global constructors keyed to _ZN1AC2Ev() at 0x00000000004006cc [/tmp/a.cpp:13] 
[pid 1613]                ==> size_checker()() at 0x00000000004006a0 [/tmp/a.h:20] 
[pid 1613]                <== size_checker()() [rax = 0x8]
[pid 1613]             <== global constructors keyed to _ZN1AC2Ev() [rax = 0x8]
[pid 1613]             ==> global constructors keyed to main() at 0x0000000000400634 [/tmp/main.cpp:11]  <-- main.cppで不一致発生
[pid 1613]                ==> size_checker()() at 0x0000000000400608 [/tmp/a.h:20] 
a.out: a.h:22: void size_checker(): Assertion `sizeof_A == sizeof(A)' failed.
[pid 1613] --- SIGABRT received (#6 Aborted) ---
[pid 1613] +++ process 1613 (ppid 1612) KILLED by SIGABRT (#6 Aborted) +++
tracef: done

調べたい対象がAだけでなく複数あるなら、templateを使ってライブラリ化しておくとよいとおもいます。グローバルなオブジェクトのコンストラクタでチェックを行います。こちらは__attribute__を使っていないので、MSVCなど、g++以外のコンパイラでも動くと思います。

// size_check_r.h
#include <cassert>
namespace hoge {
  template <typename T> struct SizeChecker {
    SizeChecker() {
      assert(size_ == sizeof(T));
    }
    static std::size_t size_;
  };
}

#define CHECK_SIZE(T) namespace { hoge::SizeCheckerR<T> sizeof_ ## T ## _checker_; }
#define DEF_PROPER_SIZE(T) template<> std::size_t hoge::SizeChecker<T>::size_ = sizeof(T)

これを用意して、

// a.h
#include <size_check_r.h>

// 中略
CHECK_SIZE(A);

および

// a.cpp

// 中略
DEF_PROPER_SIZE(A);

と。

構造体のサイズ違いの自動検出 (リンク時)


どうせなら、実行時ではなくリンク時に検査できたらよりgoodです。やってみます。まず、a.hをリンクすると勝手に、「sizeof(A)の値をシグネチャ(シンボル名)に含むような関数」を呼ぶようにします。

// a.hに追記 (boost/mpl/int.hpp をインクルードしてください)
__attribute__((constructor)) static void size_checker() {
  extern void size_checker_helper_(boost::mpl::int_<sizeof(A)>);
  size_checker_helper_(boost::mpl::int_<sizeof(A)>());
}

関数名にsizeof(A)の値(4とか8とか)を含むようにするのは難しいので、整数値を型に写像する boost::mpl::int_<> を用いて、関数の引数に含めるようにしています。boost::mpl::int_<> は、Lokiでいう Int2Type です。


そして、a.cppでのみ、(a.cppでの)sizeof(A)の値をシグネチャに含む関数を定義します。

// a.cppにのみ追記
void size_checker_helper_(boost::mpl::int_<sizeof(A)>) {}

こうしておくと、

% g++ -DV2 -c a.cpp ; g++ main.cpp a.o  
/tmp/ccQTotL4.o: In function `size_checker()':
main.cpp:(.text+0x7a): undefined reference to `size_checker_helper_(mpl_::int_<4>)'
collect2: ld returned 1 exit status

みたいな感じで、「(a.cppでのsizeof(A)は8だったのに) main.cppでは4だったぞ!」とリンクタイムにsizeof(A)の不一致を叱ってもらえます。ちょっとエラーメッセージがわかりにくいですが。。


前と同じようにライブラリ化してみます。

// size_check_l.h
#include <boost/mpl/int.hpp>
#include <boost/mpl/identity.hpp>
namespace hoge {
  template <typename T> struct SizeChecker {
    SizeChecker() {
      extern void size_checker_helper_(typename boost::mpl::identity<T>, typename boost::mpl::int_<sizeof(T)>);
      size_checker_helper_(boost::mpl::identity<T>(), boost::mpl::int_<sizeof(T)>());
    }
  };
}

#define CHECK_SIZE(T) namespace { hoge::SizeChecker<T> sizeof_ ## T ## _checker_; } 
#define DEF_PROPER_SIZE(T) void size_checker_helper_(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>) {}

boost::mpl::identityは、Tをそれ固有の軽い型に写像するクラステンプレートです (LokiのType2Typeに相当)。CHECK_SIZEマクロ、DEF_PROPER_SIZEマクロの使いかたは前と同じです。

構造体のサイズ違いの自動検出 (リンク時 その2)


上の方法だと、検出はリンク時に行えますが、実行時にmain関数の前で余計なコードが走ってしまう*1のが気になります。そこを改良。

// size_check_l2.h (最終版)
#include <boost/mpl/int.hpp>
#include <boost/mpl/identity.hpp>

#define CHECK_SIZE(T)							                              \
  extern void size_checker_helper_(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>);             \
  namespace {								                              \
    void (* sizeof_ ## T ## _checker )(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>)          \
      __attribute__((used))	                         					      \
        = size_checker_helper_;						                              \
  }
#define DEF_PROPER_SIZE(T)                                                                            \
  void size_checker_helper_(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>) {}

size_checker_helper_関数を呼ぶのをやめて、その関数へのポインタを持つだけにしました。これで、ランタイムコストは0で、ポインタn個と、チェック関数1つ分のバイナリサイズの肥大だけで済みます。ポインタを無名名前空間から出し、単にstaticな変数としてもよいです。


__attribute__((used)) は、ポインタがコンパイラに要らない子扱いされないようにする対策です(コンパイル時に消えてなくなると、リンク時のチェックが効かなくなります...)。MSVCではどうしたらいいのかなぁ。

% nm -C --format=posix a.out | grep check 
size_checker_helper_(boost::mpl::identity<A>, mpl_::int_<8>) T 00000000004005f0 0000000000000002
(anonymous namespace)::sizeof_A_checker d 0000000000600a70 0000000000000008
(anonymous namespace)::sizeof_A_checker d 0000000000600a78 0000000000000008

% objdump -Cd a.out | lv
...
00000000004005f0 <size_checker_helper_(boost::mpl::identity<A>, mpl_::int_<8>)>:
  4005f0:       f3 c3                   repz retq 
...

私の環境(x86_64なLinux)では、チェック関数は2バイトでした。これは余談ですが、nmを --format=posix あるいは -fp オプション付きで使うと、各シンボルのサイズを出せて便利だと最近認識し、実行ファイルの中から、自動で巨大なグローバル変数をみつけたり、自動で巨大な関数を見つけたりするのに使ってます。ちょっとしたセルフチェックとして。


ポインタn個と関数を特別にでっちあげたセクションに配置するように__attribute__((section))しておき、あとでそこをstrip -Rしてしまう手もあるかもしれません。

// size_check_l3.h
#include <boost/mpl/int.hpp>
#include <boost/mpl/identity.hpp>

#define CHECK_SIZE(T)							                        \
  extern void size_checker_helper_(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>);       \
  namespace {								                        \
    void (* sizeof_ ## T ## _checker )(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>)    \
      __attribute__((section(".size_checker"), used))						\
        = size_checker_helper_;						                        \
  }
#define DEF_PROPER_SIZE(T)                        \
  __attribute__((section(".size_checker_text")))  \
  void size_checker_helper_(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>) {}
% strip -R .size_checker_text -R .size_checker a.out

ちょっと面倒かな。

boostを使わない


boostを使わない/使えない場合は、

namespace boost { namespace mpl {
  template<int N> struct int_ {}
  template<typename T> struct identity {};
}}

などと自分でどこかに書いておいて、こちらを使えばOKです。

まとめ


またどーでもいいことで長文になってしまった。。まとめます。

// size_check.h
#include <boost/mpl/int.hpp>
#include <boost/mpl/identity.hpp>

#define CHECK_SIZE(T)							                     \
  extern void size_checker_helper_(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>);    \
  namespace {								                     \
    void (* sizeof_ ## T ## _checker )(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>) \
      __attribute__((section(".size_checker"), used))					     \
        = size_checker_helper_;						                     \
  }
#define DEF_PROPER_SIZE(T)                        \
  __attribute__((section(".size_checker_text")))  \
  void size_checker_helper_(boost::mpl::identity<T>, boost::mpl::int_<sizeof(T)>) {}

という10行くらいのコードを書きました。

// test.h
#include "size_check.h"

struct A {
#ifdef V2
  char bar_; /* バージョン2以降でしか使わないメンバ変数(らしい) */
#endif
  int foo_;
};
CHECK_SIZE(A); // 追記
// test.cpp
#include "test.h"
DEF_PROPER_SIZE(A); // 追記

もし、#ifdefでサイズの変わる構造体や、packされる可能性のある構造体を作成したり見かけたりしたら、今回書いたマクロを使って、そこに CHECK_SIZE(), DEF_PROPER_SIZE() と書いておくと、次のように、間違ったMakefile、不可解なビルドシステム...に起因するよくわからない不具合をサクっと検出することができ、貴重な時間を無駄にしなくて済むかもしれません。。


例:

// main.cpp
#include "test.h"
int main() {}
% g++ -DV2 -O2 -c test.cpp ; g++      -O2 -c main.cpp ; g++ test.o main.o 
main.o:(.data+0x0): undefined reference to `size_checker_helper_(boost::mpl::identity<A>, mpl_::int_<4>)'
collect2: ld returned 1 exit status

% g++      -O2 -c test.cpp ; g++ -DV2 -O2 -c main.cpp ; g++ test.o main.o 
main.o:(.data+0x0): undefined reference to `size_checker_helper_(boost::mpl::identity<A>, mpl_::int_<8>)'
collect2: ld returned 1 exit status

-DV2の有無が一致していないと怒られる。

% g++ -DV2 -O2 -c test.cpp ; g++ -DV2 -O2 -c main.cpp ; g++ test.o main.o
% g++      -O2 -c test.cpp ; g++      -O2 -c main.cpp ; g++ test.o main.o  

一致していると怒られない。

% g++ -DV2 -O2 -fPIC -shared -o test.so test.cpp

% g++      -O2 main.cpp test.so     
/tmp/ccaJzoMv.o:(.size_check+0x0): undefined reference to `size_checker_helper_(boost::mpl::identity<A>, mpl_::int_<4>)'
collect2: ld returned 1 exit status

% g++ -DV2 -O2 main.cpp test.so
%

構造体がDSO (DLL) の中に格納されている場合でも、問題なくサイズ不整合が検出可能。

% g++ -fpack-struct -DV2 -O2 -c test.cpp ; g++               -DV2 -O2 -c main.cpp ; g++ test.o main.o
main.o:(.data+0x0): undefined reference to `size_checker_helper_(boost::mpl::identity<A>, mpl_::int_<8>)'
collect2: ld returned 1 exit status

% g++               -DV2 -O2 -c test.cpp ; g++ -fpack-struct -DV2 -O2 -c main.cpp ; g++ test.o main.o
main.o:(.data+0x0): undefined reference to `size_checker_helper_(boost::mpl::identity<A>, mpl_::int_<5>)'
collect2: ld returned 1 exit status

% g++ -fpack-struct -DV2 -O2 -c test.cpp ; g++ -fpack-struct -DV2 -O2 -c main.cpp ; g++ test.o main.o
%

構造体のpack指定(-fpack-struct や #pragma pack(push,N))の不整合も検出可能。以上、「いまさらMPLを覚えたのでなんでもそれで叩いてみる」シリーズ第2弾(?)でした。

RFC


なにかもっとよさげな方法があれば教えてください。stripに頼らずともバイナリ肥大/ランタイムコストが0な奴とか、もっとずっとシンプルな方法とか、boostにそういうのあるよ、とか。


クラスの作成者が何もせずともsizeofの整合性を確認できたりしないかな。うーん、全ての*.oから、デバッグ情報として書かれているクラスのサイズをダンプしておいて比較..とかかなぁ。いまやってるDWARF3の勉強用の題材としてはよさげだけど、コンパイル以外の作業をしないと発見できないというのはヘボいな。

*1:tracefで確認できます <- しつこい