空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

GCCの__builtin_return_addressを使ってカジュアルにバックトレース

仕事でWindows上のバックトレースを取得するStackWalk関数周りを調べていて、ふとMacOSXではどうやるのか気になったのでやってみました。

Leopard以降では、そのものズバリ backtrace()、backtrace_symbols()という関数があり、またC++限定であれば、例外処理のための実装である_Unwind_Backtrace()という関数が使えるようです(参照)。

ここで紹介するのは、あまりバイナリハック的なことをせずに、C/C++でカジュアルにスタックのバックトレースを取得する方法です。カジュアルなぶん、環境にかなり依存すると思われます。テストした環境はMacOSX 10.4.11 (PPC) です。

原理はとても単純で、以下の3ステップです。

  1. __builtin_return_address() 関数を使って、関数の戻るポインタを取得する。
    • 引数を1,2,3..インクリメントしていき、スタックの一番下まで行ったところで NULL が返ってくるという性質を利用する(環境依存)。
    • 少し変なところとしては、__builtin_return_address() に変数を与えると、コンパイルエラーになるため、リテラルを使って平に書き下す。
  2. dlfcn.h で提供されている dladdr() を使って、そのポインタが含まれるモジュールおよび関数名を取得する。
  3. 必要があれば cxxabi.h で提供されている abi::__cxa_demangle() を使って、読みやすいフォーマットに変換する。

※static で定義されたローカルな関数に対しては、dladdr() が正しい値を返さず、正しい結果が得られないようです。

説明は以上。コードは以下の通りです。

#include <dlfcn.h>
#include <cxxabi.h>
#include <stdio.h>
#include <stdlib.h>

#define b(n)                                    \
do                                              \
{                                               \
  ptr = __builtin_return_address(n);            \
                                                \
  if (ptr)                                      \
  {                                             \
    *buffer++ = ptr;                            \
  }                                             \
  else                                          \
  {                                             \
    return n-1;                                 \
  }                                             \
} while (0)

//! Populate function return address array
//!
//! @param[out] buffer
//!   Buffer to populate addresses pre-allocated by caller.
//!   Must be longer than 80.
//!
//! @return
//!   The number of populated addresses.
//!
int BackTrace(void** buffer)
{
  void* ptr = 0;
  b(1); b(2); b(3); b(4); b(5); b(6); b(7); b(8); b(9);
  b(10); b(11); b(12); b(13); b(14); b(15); b(16); b(17); b(18); b(19);
  b(20); b(21); b(22); b(23); b(24); b(25); b(26); b(27); b(28); b(29);
  b(30); b(31); b(32); b(33); b(34); b(35); b(36); b(37); b(38); b(39);
  b(40); b(41); b(42); b(43); b(44); b(45); b(46); b(47); b(48); b(49);
  b(50); b(51); b(52); b(53); b(54); b(55); b(56); b(57); b(58); b(59);
  b(60); b(61); b(62); b(63); b(64); b(65); b(66); b(67); b(68); b(69);
  b(70); b(71); b(72); b(73); b(74); b(75); b(76); b(77); b(78); b(79);
  return 79;
}

#undef b

void DumpBackTrace()
{
  Dl_info info = {};
  const int bufferSize = 80;
  void* buffer[bufferSize] = {};
  const int n = BackTrace(buffer);
  char* dem = (char*)malloc(64);
  size_t length = 64;
  int status = 0;

  puts("////////////////////////////////////////\n");

  for (int i = 0; i < n; ++i)
  {
    if (dladdr(buffer[i], &info))
    {
      dem = abi::__cxa_demangle(info.dli_sname, dem, &length, &status);

      if (!status)
      {
        printf("[%d] %s, %s (%s)\n", i, info.dli_fname, dem, info.dli_sname);
      }
      else
      {
        printf("[%d] %s, %s\n", i, info.dli_fname, info.dli_sname);
      }
    }
    else
    {
      printf("[%d] 0x%08x\n", i, (unsigned int)buffer[i]);
    }
  }

  free(dem);
}

使用例は以下の通りです。

void Test(int i = 90)
{
  if (--i)
  {
    DumpBackTrace();
    Test(i);
  }
}

int main()
{
  Test();
  return 0;
}

例えばこれを g++ -O0 でコンパイルすると出力は

////////////////////////////////////////
[0] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[1] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[2] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[3] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[4] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
 :
[74] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[75] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[76] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[77] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[78] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)

のように、ひたすら再帰(上限80まで)呼び出しされる様子がダンプされますが、g++ -O3 にすると

////////////////////////////////////////
[0] /Users/xoinu/Documents/src/bt/./a.out, Test(int) (_Z4Testi)
[1] /Users/xoinu/Documents/src/bt/./a.out, main
[2] /Users/xoinu/Documents/src/bt/./a.out, tart
[3] /Users/xoinu/Documents/src/bt/./a.out, tart

となり、末尾呼び出しが最適化されてしまっている様子が分かります。

まとめ

__builtin_return_address() と dladdr() を使って、カジュアルにバックトレースをダンプする例を紹介しました。取りあえずコンセプトコードを書いただけで、ロクにテストしてませんので悪しからず。