ついカッとなって実行バイナリにパッチ

とある都合で、ソフトウェア開発の際にソースコードの提供されていないツールを使うことになりました。x86Linux上で動く、ちょっとしたtoolchainです。が、そのツールの処理速度が遅く、入力サイズに対して、結果が出てくるまでの時間がどうもO(N^2)かそれよりひどい。遅くてイライラするので、昨晩ついカッとなってパッチを当てました。そのメモです。また、ありがちな事態(?)な気もするので、みなさんどうしてるのかなー的なお伺いも兼ねて。

ボトルネックの特定


そのツール(以下A)の実行バイナリはstripされておらず.symtabが残っていました。のでまず、どこが遅いのかgoogle-perftoolsをLD_PRELOADしてそのソフトウェアを実行し、実行プロファイルを取りました。すると、嬉しいことにある一つの関数(以下F)で全体の90%以上の時間を消費していることがわかりました。関数Fは、Aの実行バイナリの中に含まれており、DSO(DLL)内の関数では無いので、LD_PRELOAD一発で高速版に置き換えたりはできないのですが、まぁどうにかできそうな範疇の予感です。

# yum install graphviz gv
# yum localinstall google-perftools-0.93-1.i386.rpm

% CPUPROFILE=/tmp/profile.out LD_PRELOAD=/usr/lib/libprofiler.so.0 ./A
PROFILE: interrupts/evictions/bytes = 22379/6911/720720
% pprof ./A /tmp/profile.out
Welcome to pprof!  For help, type 'help'.
(pprof) top
Total: 22379 samples
   22364  99.9%  99.9%    22364  99.9% F
      13   0.1% 100.0%    22378 100.0% msort_with_tmp
       1   0.0% 100.0%        1   0.0% memcpy
       1   0.0% 100.0%    22379 100.0% main
       0   0.0% 100.0%    22379 100.0% _start
       0   0.0% 100.0%    22364  99.9% G
       0   0.0% 100.0%    22378 100.0% qsort
       0   0.0% 100.0%    22379 100.0% __libc_start_main
(pprof) gv    <-- ざっくりとしたコールグラフを確認

次に、どこからその遅い関数Fが呼ばれているのか、callgrindを使って正確に調べました。callgrind上でのソフトウェアの動作はかなり遅くなりますので、ただでさえ遅いこのソフトのプロファイルを取るのには何時間もかかりました。もちろん、Aを objdump -d で逆アセンブルしてFの呼び出し箇所を静的に調べる*1こともできますが、callgrindを使用すると、呼び出し箇所だけでなく、それぞれの箇所を何回通ったかもわかり、より便利です。

# yum install valgrind kdesdk

% valgrind --tool=callgrind ./A
==18353== Callgrind, a call-graph generating cache profiler.
==18353== Copyright (C) 2002-2007, and GNU GPL'd, by Josef Weidendorfer et al.
==18353== Using LibVEX rev 1732, a library for dynamic binary translation.
==18353== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP.
==18353== Using valgrind-3.2.3, a dynamic binary instrumentation framework.
==18353== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al.
==18353== For more details, rerun with: -v
==18353== 
==18353== For interactive control, run 'callgrind_control -h'.
==18353== 
==18353== Events    : Ir
==18353== Collected : 128019689295
==18353== 
==18353== I   refs:      128,019,689,295
% kcachegrind ./callgrind.out.18353 &   <-- 詳細なコールグラフを確認

callgrindの結果をkcachegrindでvisualizeしたものと、Aの重要な部分を逆アセンブルしたリストを眺めた*2ところ、要するにこのソフトウェアは、細かい部分を捨象すると次のようなつくりになっており、遅いのだという結論を得ることができました。

/* tooooslow.c */
#include <stdlib.h>

typedef struct { int data; } S;
#define N 50000  /* 入力サイズ */
S* input = 0;    /* 入力データ */

int F(const S* s1, const S* s2) { /* この子が遅い */
  int i, dmy = 0;
  for (i = 0; i < N; ++i) {
    /* 実際は、input, s1->data, s2->data を用いた O(N) の計算 */ 
    ++dmy; 
  }
  return (s1->data) - (s2->data);
}

int G(const void* s1, const void* s2) {
  return F((const S*)s1, (const S*)s2);
}

int main() {
  int i;
  input = calloc(N, sizeof(S));
  for(i = 0; i < N; ++i) input[i].data = (N - i) % 128; /* 適当に初期化 */

  qsort(input, N, sizeof(S), G);
  exit(0);
}

objdump -dしたところ、Gは、mainのqsort以外では使われていません。また、Fの内部でinputに変更が加わることはなく、そのためFは、少なくともmainのqsortから呼ばれる場合については、(s1->data, s2->data)の組が同じなら戻り値も同じ、算数で言うところの関数、GCCでいうところの__attribute__((pure))な関数です。このFをどうにかします。

パッチ方法の検討


Fの処理オーダーを改善すれば、このツールAが高速化すると見込めるわけですが、Fのアルゴリズムを、ソースコードなしでO(N)からO(1)に改善するのは、ちょっと一晩では難しいと思ったので、Fを次のようにラップすることで妥協することにしました。

int F_wrap(const S* s1, const S* s2) {
  key = 連結(s1->data, s2->data);
  if (hash.ある?(key)) { return 覚えてた奴; }
  ret = もとのF(s1, s2);
  hash.おぼえる(key, ret);
  return ret;
}

また、

という俺ルールにしました。私はこれらの技術が達人のみなさんと比べてそんなに流暢に使えるわけでもなく、手を出すと一晩では仕上らない予感がしたからです。なお、これらを使った格好いい方法はBinary Hacksに載ってるような気もします。


で、このルールの元、次の方法でパッチすることにしました。

  • LD_PRELOADでF_wrapをAのプロセス空間に滑り込ませる
  • Gの書き換えは、__attribute__((constructor)) な関数もPRELOADしておくことで実行時にmainの前で行う。

これに必要な情報は、Aをobjdump -dすれば収集できます。

% objdump -d tooooslow | lv
...
080483e4 <F>:
 80483e4:       55                      push   %ebp
 80483e5:       89 e5                   mov    %esp,%ebp
 80483e7:       83 ec 10                sub    $0x10,%esp
...
0804841d <G>:
 804841d:       55                      push   %ebp
 804841e:       89 e5                   mov    %esp,%ebp
 8048420:       83 ec 08                sub    $0x8,%esp
 8048423:       8b 45 0c                mov    0xc(%ebp),%eax
 8048426:       8b 55 08                mov    0x8(%ebp),%edx
 8048429:       89 44 24 04             mov    %eax,0x4(%esp)
 804842d:       89 14 24                mov    %edx,(%esp)
 8048430:       e8 af ff ff ff          call   80483e4 <F>          <-- 書き換える場所
...

パッチ


こんな感じになりました。なお、高速化(hash)部分は省略し、F_wrap()からF()を呼び、F()の戻り値をprintfするだけのコードにしてあります。

// bin_patch.cpp
#ifndef _GNU_SOURCE
 #define _GNU_SOURCE
#endif

#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <stdint.h>
#include <sys/mman.h>
#include <dlfcn.h>

#define F         0x80483e4U
#define CALL_F    0x8048430U
#define PAGESIZE  4096

using namespace std;
typedef struct { int data; } S;

extern "C" int F_wrap(const S* p, const S* q) {
  int (*f)(const S*, const S*) = (int (*)(const S*, const S*))F;
  int ret = f(p, q);
    printf("F(%p [%d], %p [%d]) = %d\n", p, p->data, q, q->data, ret);
  return ret;
}

__attribute__((constructor)) static void do_patch() {
  int ret;
  if (getenv("DONT_PATCH_A")) return;

  // Aのテキスト部分を書き換え可能にする
  if ((CALL_F & ~(PAGESIZE-1)) != ((CALL_F + 4) & ~(PAGESIZE-1))) {
    // ページをまたいでパッチするので *2
    ret = mprotect((void*)(CALL_F & ~(PAGESIZE-1)), PAGESIZE * 2, PROT_READ | PROT_WRITE | PROT_EXEC);
  } else {
    ret = mprotect((void*)(CALL_F & ~(PAGESIZE-1)), PAGESIZE,     PROT_READ | PROT_WRITE | PROT_EXEC);
  }
  if (ret != 0) { perror("mprotect"); return; }

  // Aのコードを実際に書き換える
  unsigned int* p = (unsigned int*)(CALL_F + 1); // 1足しているのは 'e8' をスキップするため
  if (*p != (F - (CALL_F + 5))) {
    // mprotectがEACCESS/EFAULTを戻さないことを確認してから読む方が確実なのでそうしている
    fprintf(stderr, "パッチ対象のバイナリじゃないよー\n"); return;
  }
  *p = ((uintptr_t)F_wrap) - (CALL_F + 5); // アドレスの書き換え (PC相対のアドレスを書く)
  __asm__ __volatile__("push %%ebx ; cpuid ; pop %%ebx" :: "a"(0) : "memory", "%ecx", "%edx");
}

使いかたは、DSOとしてコンパイル・リンクし、LD_PRELOADするだけです。

 % g++ -Wall -O2 -fPIC -shared -o bin_patch.so bin_patch.cpp -ldl
 % LD_PRELOAD=./bin_patch.so ./tooooslow
 F(0xb7ec300c [79], 0xb7ec3010 [78]) = 1
 F(0xb7ec3008 [80], 0xb7ec300c [78]) = 2
 F(0xb7ec3008 [80], 0xb7ec3010 [79]) = 1
 ...

誤って、あるいは事情があって 'tooooslow' 以外にPRELOADしてしまっても、よほど運が悪くなければ何もしません。

 % LD_PRELOAD=./bin_patch.so /bin/echo hoge
 パッチ対象のバイナリじゃないよー
 hoge

以上でございます。


なお、Aをvalgrind上で実行している場合、mprotectがENOMEMで戻ってしまい、実行時パッチに失敗することがありました。理由はいまのところ不明。callgrindやcachegrindでパッチ後のプロファイルを取りたいなどの場合は、xxdやbvi、hte、あるいはemacsの M-x hexl-find-file などの適当なバイナリエディタで事前に実行ファイルAに静的にパッチしておくとよいと思われます*3

余談


実は最初、Gの call F を call F_wrap@DSO に簡単に書き換えることはできないのではないかとおもい、次のような方法にしていました。動的リンカに自身(F_wrap)のアドレスを教えてもらえばいいやと思っていたわけです。

  • Gの call F を、call F_wrap に書き換えたいのだけど、F_wrapのアドレスが事前にわからないので、とりあえず call exit@plt に書き換えておく。exit@pltのアドレスは、nm --synthetic ./A などですぐわかる。
  • F_wrapをexitにaliasしておく。あるいは、単にF_wrapではなくexitという名前でラップ関数をつくり、gcc -fno-builtin-exit でコンパイルしておく。すると、exit@plt は、このラップ関数をlibcのexitだと思って呼ぶようになる。
  • F_wrapの冒頭で、自分がFとして呼ばれたのか、exitとして呼ばれたのかを判定する。バックトレースすれば確実にわかるが、めんどくさいので引数のs1をintにキャストして、-256から+255の範囲だったらexit呼び出しだろうということにする(これはひどい)。

という方法です*4。最近PLTを違う用途に活用しすぎです。

// bin_patch_PLT.cpp
...
#define EXIT_PLT  0x80482f8U
__attribute__((constructor)) static void do_patch() {
  (さっきと同じコード)
  *p = EXIT_PLT - (CALL_F + 5); // exit@pltに飛ばす
}

extern "C" int F_wrap(const S* p, const S* q) {
  if (((int)p >= 256) || ((int)p < -256)) {
    // case 1. pはアドレスっぽい。Fを呼ぶ 
    (さっきと同じコード)
  }
  // case 2. pはexit statusっぽい。libcのexitを呼ぶ 
  void (*real_exit)(int) = (void (*)(int))dlsym(RTLD_NEXT, "exit");
  real_exit((int)p);
  return 0; // not reached
}
void exit(int) __attribute__((alias("F_wrap")));

で、この方法はかなり汚いhackなので、自分でも何をしているのか忘れそうだなーと思いこの日記を書き始めました。でも書いている途中で、直接 F_wrap に飛ばせるじゃんということがわかり、なんだか何が難しいやらよくわからない(当社比)ぼんやりしたエントリになったのでした。でも、トラブル解決の記録ものとして晒しておきます。もしいつか誰かの何かのお役に立つことでもあれば幸いでございます。

成果


とりあえず、待ち時間に昼寝ができそうなほど長かったリンク時間が1/6に短縮されました。わーい。あと、これを手がけたおかげで、今日以降はPLTを経由してない関数呼び出しも好きなようにフックできるぞーっと。関数のアドレスがわかるならだけど。


TODO: hogetrace絡みでshinhさんに個人的に教えてもらったあれこれをもう一回読む。なんか全然違うこの話題に応用できそうでタイムリー。

まとめ


ベンダに直してもらえYO!

*1:今回のケースでは、ライセンス上の問題はありませんでした

*2:今回のkcachegrindの使いかた: "self"でソートしてもっとも時間のかかっている関数(F)をクリックし、"Call Graph"で右クリックしてGraphのCaller DepthをUnlimitedにして、どういう経路でFが呼ばれているか眺めればOK

*3:が、F_wrap()がどこにロードされるか事前には必ずしも決められないか....。次の「余談」のところを見てください

*4:そういえばPRELOADするDSOのPT_LOADのp_vaddrって、100%でなくてもいいですが、効きますっけ? ローダ読め? はい

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


先日、次のような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で確認できます <- しつこい

50行straceもどき


すこし前に、straceコマンドもどきを50行くらいで書いてみたことがあるので、それを貼ってみまーす。いきなりコード。あ、C99です。

// strace_modoki.c:  Linux/x86専用です。x86_64カーネルでは-m32でコンパイルしても動きません。

#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <asm/user.h>
#include <asm/ptrace.h>

int main() {
  int i;
  static const char* syscallstr[1000] = {0};
  for(i = 0; i < 1000; ++i) syscallstr[i] = "???";

  syscallstr[0] = "restart_syscall";
  syscallstr[1] = "exit";
  syscallstr[2] = "fork";
  /* 略 */
  syscallstr[321] = "signalfd";
  syscallstr[322] = "timerfd";
  syscallstr[323] = "eventfd";

  if (!fork()) {
    // child
    ptrace(PTRACE_TRACEME, 0, 0, 0);
    execve("/bin/echo",
           (char *const []){ "/bin/echo", "123", NULL },
           (char *const []){ NULL });
    _exit(-1);
  } else {
    int st, in_syscall = 1; // うまく動かなかったら1を0にしてみてください :-)
    if (in_syscall) printf("enter execve() ");
    wait(&st); // wait for exec
    while (ptrace(PTRACE_SYSCALL, p, 0, 0) == 0) { // cont until syscall enter/leave
      wait(&st); // wait for syscall enter/leave
      if(WIFEXITED(st)) {
        puts("= ?"); break;
      }
      long orig_eax = ptrace(PTRACE_PEEKUSER, p, 4 * ORIG_EAX, 0);
      struct user_regs_struct regs;
      ptrace(PTRACE_GETREGS, p, 0, &regs);
      if (in_syscall == 0) {
        in_syscall = 1;
        printf("enter %s(0x%lx) ",
               (orig_eax >= 0 && orig_eax < 1000) ? syscallstr[orig_eax] : "???", regs.ebx);
      } else {
        in_syscall = 0;
        printf("= %ld\n", regs.eax);
      }
      fflush(stdout);
    }
  }
  return 0;
}

冒頭の syscallstr[nnn] = "xxx"; の部分は、/usr/include/asm/unistd.h をもとに、適当に変形して生成してください。余談ですがこのファイルをたまにのぞくと新規に追加されたシステムコールがわかって楽しいです。signalfdとか。エラーチェック他全部省略の手抜きコードです。うまく動かなかったらゴメンナサイ。


これを実行すると、まずfork()して子プロセスで /bin/echo 123 を実行し、親プロセスがその子の様子をptraceで観察します。出力はこんな感じになります。

% gcc -Wall -W strace_modoki.c
% ./a.out 
enter execve() = 0
enter brk(0x0) = 164818944
enter access(0x4bee8f) = -2
enter open(0x4bf077) = 3
enter fstat64(0x3) = 0
enter mmap2(0x0) = -1208172544
enter close(0x3) = 0
enter open(0xb7fd7a08) = 3
enter read(0x3) = 512
enter fstat64(0x3) = 0
enter mmap2(0x0) = -1208176640
enter mmap2(0x4c5000) = 5001216
enter mmap2(0x5ff000) = 6287360
enter mmap2(0x602000) = 6299648
enter close(0x3) = 0
enter mmap2(0x0) = -1208180736
enter set_thread_area(0xbfac95c4) = 0
enter mprotect(0x5ff000) = 0
enter mprotect(0x4c1000) = 0
enter munmap(0xb7fcc000) = 0
enter brk(0x0) = 164818944
enter brk(0x9d50000) = 164954112
enter fstat64(0x1) = 0
enter mmap2(0x0) = -1208119296
enter write(0x1) 123
= 4
enter close(0x1) = 0
enter munmap(0xb7fd9000) = 0
enter exit_group(0x0) = ?
% 

全くpretty printされていませんが、一応straceっぽい出力が得られています。ptraceの詳細については、man 2 ptraceや、2002年のこの記事あたりが参考になると思います。