google-perftools を別の CPU に移植してみた のジョーク版みたいなつもりで始めてみたら、意外と学べることがあったため、ちょいとまとめておく。
結論を先に言っておくと、google-perftools から提供される malloc (tcmalloc) を使おうとすると gcc の cygwin 対応のコードが原因で SIGSEGV が発生するため、本当に移植する場合はまずそちらを何とかしないといけないと思われる。
パッチ
ネタ元のパッチを参考にしつつ、cygwin 向けの修正を適用したパッチは http://www.freeshells.ch/~fuddy/cygwin/tmp/google-perftools-0.94.1-1bl1.src.patch に置いている。*1
autotools まわりの修正
ネタ元と共通の修正としては
くらいで、後は DLL を生成するようにする cygwin 特有の修正として
- configure.ac
- AC_LIBTOOL_WIN32_DLL を追加する。
- Makefile.am
- lib*_la_LDFLAGS に -no-undefined を追加する。
を加えた。
また autoreconf する前に acinclude *2 することで acx_pthread の定義を上書きしておく。*3
ソースの修正
もともと Linux 向けに書かれたソースコードと思われるため、いろいろと修正しないとコンパイルすらできない。
というわけで以下のように修正する。ただし、修正方針が正しいとは限らない。とりあえず動いてくれないと試しようがないため、当たり障りのない最小限の修正にとどめておく。
- src/maybe_threads.cc
- 配列の添字を int にキャストする。
- src/google/heap-checker.h
- #include
を追加する。
- #include
- src/getpc.h
- GetPC() は Binary Hacks ―ハッカー秘伝のテクニック100選 の #79 を参考に実装する。
diff -ruN origsrc/google-perftools-0.94.1/src/getpc.h src/google-perftools-0.94.1/src/getpc.h --- origsrc/google-perftools-0.94.1/src/getpc.h 2007-11-28 07:17:59.000000000 +0900 +++ src/google-perftools-0.94.1/src/getpc.h 2007-12-30 19:57:22.656250000 +0900 @@ -146,6 +146,17 @@ return (void*)eip; } +#elif defined(__CYGWIN__) +// Thanks to binary hacks #70 for the concept of getPC. +inline void* GetPC(void) +{ + void *p; + asm(".byte 0xe8,0,0,0,0\n\t" /* call the following popl insn */ + "popl %0\n\t" + : "=m" (p)); + return p; +} + // Special case #2: Windows, which has to do something totally different. #elif defined(WIN32) // If this is ever implemented, probably the way to do it is to have
- src/heap-checker.cc
- weak 属性の箇所を無効にする。
- src/profiler.cc
- src/getpc.h に合わせて GetPC() を修正する。
使ってみる
tcmalloc はソースを修正する必要はなく、再リンクだけで使用することができる。(のが売りのひとつらしい。)
以下の tcmalloc.c というサンプルで試してみる。
#include <stdlib.h> int main(int argc, char** argv) { void* p = malloc(100); }
% gcc -g tcmalloc.c -o tcmalloc.exe -ltcmalloc % gdb ./tcmalloc.exe GNU gdb 6.5.50.20060706-cvs (cygwin-special) Copyright (C) 2006 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i686-pc-cygwin"... (gdb) run Starting program: /cygdrive/c/home/src/c/tcmalloc.exe Loaded symbols for /cygdrive/c/WINDOWS/system32/ntdll.dll Loaded symbols for /cygdrive/c/WINDOWS/system32/kernel32.dll Loaded symbols for /usr/bin/cygwin1.dll Loaded symbols for /cygdrive/c/WINDOWS/system32/advapi32.dll Loaded symbols for /cygdrive/c/WINDOWS/system32/rpcrt4.dll Loaded symbols for /cygdrive/c/WINDOWS/system32/secur32.dll Loaded symbols for /usr/bin/cygtcmalloc-0.dll Loaded symbols for /usr/bin/cygstacktrace-0.dll Program received signal SIGSEGV, Segmentation fault. 0x64adc7d5 in __w32_sharedptr_initialize () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll (gdb)
__w32_sharedptr_initialize() 内で SIGSEGV が発生する。
(gdb) where #0 0x64adc7d5 in __w32_sharedptr_initialize () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #1 0x64adbcd5 in _Unwind_SjLj_Register () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #2 0x64ac8b65 in malloc () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #3 0x64adc7fa in __w32_sharedptr_initialize () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #4 0x64adbcd5 in _Unwind_SjLj_Register () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #5 0x64ac8b65 in malloc () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #6 0x64adc7fa in __w32_sharedptr_initialize () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #7 0x64adbcd5 in _Unwind_SjLj_Register () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll #8 0x64ac8b65 in malloc () from /cygdrive/c/home/src/c/cygtcmalloc-0.dll : :
スタックトレースから
- malloc() -> _Unwind_SjLj_Register()
- _Unwind_SjLj_Register() -> __w32_sharedptr_initialize()
- __w32_sharedptr_initialize() -> malloc()
という感じで呼び出しが無限に続いていることによるスタックオーバーフローが SIGSEGV の原因っぽいことがわかる。
malloc() -> _Unwind_SjLj_Register()
#2 のアドレス 0x64ac8b65 前後を逆アセンブルしてみる。
(gdb) disassemble 0x64ac8b65 Dump of assembler code for function malloc: 0x64ac8b30 <malloc+0>: push %ebp 0x64ac8b31 <malloc+1>: mov %esp,%ebp 0x64ac8b33 <malloc+3>: lea 0xffffffe8(%ebp),%eax 0x64ac8b36 <malloc+6>: push %edi 0x64ac8b37 <malloc+7>: push %esi 0x64ac8b38 <malloc+8>: push %ebx 0x64ac8b39 <malloc+9>: sub $0x8c,%esp 0x64ac8b3f <malloc+15>: mov %esp,0xffffffcc(%ebp) 0x64ac8b42 <malloc+18>: mov %eax,0xffffffc4(%ebp) 0x64ac8b45 <malloc+21>: lea 0xffffffa4(%ebp),%eax 0x64ac8b48 <malloc+24>: movl $0x64ad7730,0xffffffbc(%ebp) 0x64ac8b4f <malloc+31>: movl $0x64ae5e36,0xffffffc0(%ebp) 0x64ac8b56 <malloc+38>: movl $0x64ac8e72,0xffffffc8(%ebp) 0x64ac8b5d <malloc+45>: mov %eax,(%esp) 0x64ac8b60 <malloc+48>: call 0x64adbca0 <_Unwind_SjLj_Register> 0x64ac8b65 <malloc+53>: cmpb $0x0,0x64af1448 : :
malloc が C++ で定義されていることにより、g++ が例外処理のために _Unwind_SjLj_Register() を挿入している *4 ことがこの呼び出しの連鎖の発端のようである。
_Unwind_SjLj_Register() -> __w32_sharedptr_initialize()
続いて #1 のアドレス 0x64adbcd5 前後も逆アセンブルしてみる。
(gdb) disassemble 0x64adbcd5 Dump of assembler code for function _Unwind_SjLj_Register: 0x64adbca0 <_Unwind_SjLj_Register+0>: push %ebp 0x64adbca1 <_Unwind_SjLj_Register+1>: mov %esp,%ebp 0x64adbca3 <_Unwind_SjLj_Register+3>: push %ebx 0x64adbca4 <_Unwind_SjLj_Register+4>: sub $0x14,%esp 0x64adbca7 <_Unwind_SjLj_Register+7>: mov 0x64b04940,%edx 0x64adbcad <_Unwind_SjLj_Register+13>: mov 0x8(%ebp),%ebx 0x64adbcb0 <_Unwind_SjLj_Register+16>: test %edx,%edx 0x64adbcb2 <_Unwind_SjLj_Register+18>: je 0x64adbcd0 <_Unwind_SjLj_Register+48> 0x64adbcb4 <_Unwind_SjLj_Register+20>: mov 0x24(%edx),%eax 0x64adbcb7 <_Unwind_SjLj_Register+23>: test %eax,%eax 0x64adbcb9 <_Unwind_SjLj_Register+25>: js 0x64adbcf0 <_Unwind_SjLj_Register+80> 0x64adbcbb <_Unwind_SjLj_Register+27>: mov 0x24(%edx),%ecx 0x64adbcbe <_Unwind_SjLj_Register+30>: test %ecx,%ecx 0x64adbcc0 <_Unwind_SjLj_Register+32>: jne 0x64adbd10 <_Unwind_SjLj_Register+112> 0x64adbcc2 <_Unwind_SjLj_Register+34>: mov 0x1c(%edx),%eax 0x64adbcc5 <_Unwind_SjLj_Register+37>: mov %eax,(%ebx) 0x64adbcc7 <_Unwind_SjLj_Register+39>: mov %ebx,0x1c(%edx) 0x64adbcca <_Unwind_SjLj_Register+42>: add $0x14,%esp 0x64adbccd <_Unwind_SjLj_Register+45>: pop %ebx 0x64adbcce <_Unwind_SjLj_Register+46>: pop %ebp 0x64adbccf <_Unwind_SjLj_Register+47>: ret 0x64adbcd0 <_Unwind_SjLj_Register+48>: call 0x64adc720 <__w32_sharedptr_initialize> 0x64adbcd5 <_Unwind_SjLj_Register+53>: mov 0x64b04940,%edx : :
元々の gcc の _Unwind_SjLj_Register() には __w32_sharedptr_initialize() の呼び出しはないが、gcc-3.4.4-3.patch にそれが含まれている。
--- gcc-3.4.4-3-orig/gcc/unwind-sjlj.c 2003-11-02 00:00:08.000000000 +0000 +++ gcc-3.4.4-3/gcc/unwind-sjlj.c 2005-06-03 21:03:34.000000000 +0100 : (略) : void _Unwind_SjLj_Register (struct SjLj_Function_Context *fc) { +#if defined (__MINGW32__ ) || defined (__CYGWIN__) + W32_SHAREDPTR_INITIALIZE (); +#endif + #if __GTHREADS if (use_fc_key < 0) fc_key_init_once (); : (略) :
__w32_sharedptr_initialize() -> malloc()
最後に #0 のアドレス 0x64adc7d5 前後を逆アセンブルしてみる。
(gdb) disassemble 0x64adc7d5 0x64adc7fa Dump of assembler code from 0x64adc7d5 to 0x64adc7fa: 0x64adc7d5 <__w32_sharedptr_initialize+181>: mov %esi,(%esp) 0x64adc7d8 <__w32_sharedptr_initialize+184>: call 0x64add0c0 <FindAtomA@4> 0x64adc7dd <__w32_sharedptr_initialize+189>: movzwl %ax,%eax 0x64adc7e0 <__w32_sharedptr_initialize+192>: sub $0x4,%esp 0x64adc7e3 <__w32_sharedptr_initialize+195>: test %eax,%eax 0x64adc7e5 <__w32_sharedptr_initialize+197>: mov %eax,0xffffff94(%ebp) 0x64adc7e8 <__w32_sharedptr_initialize+200>: jne 0x64adc8a0 <__w32_sharedptr_initialize+384> 0x64adc7ee <__w32_sharedptr_initialize+206>: movl $0x30,(%esp) 0x64adc7f5 <__w32_sharedptr_initialize+213>: call 0x64ac8b30 <malloc> End of assembler dump.
たしかに malloc() を呼び出しており、これで呼び出しが無限に続くことになる。
ちなみに __w32_sharedptr_initialize() のソースも gcc-3.4.4-3.patch (gcc/config/i386/w32-shared-ptr.c) にある。
W32_EH_SHARED * __w32_sharedptr; void __w32_sharedptr_initialize (void) { W32_EH_SHARED *w32_sharedptr; char s[SHARED_ATOM_NAME_LEN]; ATOM atom; if (__w32_sharedptr) return; memset (s, SHAREDPTR_BIT1, SHAREDPTR_BITS); memcpy (&(s[SHAREDPTR_BITS]), w32_atom_suffix, sizeof(w32_atom_suffix)); atom = FindAtomA (s); if (atom) w32_sharedptr = __w32_sharedptr_get (atom); else { w32_sharedptr = (W32_EH_SHARED *) malloc (sizeof(W32_EH_SHARED)); if (!w32_sharedptr) abort (); __w32_eh_shared_initialize (w32_sharedptr); if (__w32_sharedptr_set (w32_sharedptr)) { #ifdef __CYGWIN__ /* recreate atom after fork */ pthread_atfork (NULL,NULL,__w32_sharedptr_fixup_after_fork); #endif } else { free (w32_sharedptr); w32_sharedptr = __w32_sharedptr_get (FindAtomA (s)); } } __w32_sharedptr_terminate = &w32_sharedptr->terminate; __w32_sharedptr_unexpected = &w32_sharedptr->unexpected; /* THIS MUST BE THE LAST STEP */ __w32_sharedptr = w32_sharedptr; }
malloc() の呼び出しを避けるためには __w32_sharedptr を静的に確保した領域にするなどの対策を施せば良いように見える。(おいそれとはできなさそうだけど。)
よくよく考えてみたら、これは別に google-perftools の問題ではなくて、cygwin の C++ で malloc を再定義しようとしたら必ず起こる問題のように思える。となると、やっぱり gcc/config/i386/w32-shared-ptr.c を (需要があればだが) 何とかしてもらわないといけないのではないかと思う。
*1:上述のとおり、これを適用して、ビルドしても動かないのであしからず。
*2:ac-archive に含まれている。上記のパッチには acinclude で生成した m4 が含まれているため、再度実行する必要はないはず。
*3:でないと、何故か pthread が使用できないと判断されるため。その場合は -DNO_THREADS を指定しておかなければならなくなる。