いけむランド

はてダからやってきました

google-perftools を cygwin に移植してみようとして挫折した

google-perftools を別の CPU に移植してみた のジョーク版みたいなつもりで始めてみたら、意外と学べることがあったため、ちょいとまとめておく。


結論を先に言っておくと、google-perftools から提供される malloc (tcmalloc) を使おうとすると gcccygwin 対応のコードが原因で SIGSEGV が発生するため、本当に移植する場合はまずそちらを何とかしないといけないと思われる。

パッチ

ネタ元のパッチを参考にしつつ、cygwin 向けの修正を適用したパッチは http://www.freeshells.ch/~fuddy/cygwin/tmp/google-perftools-0.94.1-1bl1.src.patch に置いている。*1

autotools まわりの修正

ネタ元と共通の修正としては

  • configure.ac
    • mmap が使用できることを認識させる。
  • Makefile.am
    • CXXFLAGS に -fno-omit-frame-pointer が追加されるようにする。

くらいで、後は DLL を生成するようにする cygwin 特有の修正として

  • configure.ac
    • AC_LIBTOOL_WIN32_DLL を追加する。
  • Makefile.am
    • lib*_la_LDFLAGS に -no-undefined を追加する。

を加えた。

また autoreconf する前に acinclude *2 することで acx_pthread の定義を上書きしておく。*3

ソースの修正

もともと Linux 向けに書かれたソースコードと思われるため、いろいろと修正しないとコンパイルすらできない。
というわけで以下のように修正する。ただし、修正方針が正しいとは限らない。とりあえず動いてくれないと試しようがないため、当たり障りのない最小限の修正にとどめておく。

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
    :
    :

スタックトレースから

  1. malloc() -> _Unwind_SjLj_Register()
  2. _Unwind_SjLj_Register() -> __w32_sharedptr_initialize()
  3. __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
    :
    :

mallocC++ で定義されていることにより、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 の問題ではなくて、cygwinC++malloc を再定義しようとしたら必ず起こる問題のように思える。となると、やっぱり gcc/config/i386/w32-shared-ptr.c を (需要があればだが) 何とかしてもらわないといけないのではないかと思う。

*1:上述のとおり、これを適用して、ビルドしても動かないのであしからず。

*2:ac-archive に含まれている。上記のパッチには acinclude で生成した m4 が含まれているため、再度実行する必要はないはず。

*3:でないと、何故か pthread が使用できないと判断されるため。その場合は -DNO_THREADS を指定しておかなければならなくなる。

*4:http://meraman.dip.jp/index.php?g%2B%2BTryCatch より