GCCの__builtin_return_addressを使ってカジュアルにバックトレース
仕事でWindows上のバックトレースを取得するStackWalk関数周りを調べていて、ふとMacOSXではどうやるのか気になったのでやってみました。
Leopard以降では、そのものズバリ backtrace()、backtrace_symbols()という関数があり、またC++限定であれば、例外処理のための実装である_Unwind_Backtrace()という関数が使えるようです(参照)。
ここで紹介するのは、あまりバイナリハック的なことをせずに、C/C++でカジュアルにスタックのバックトレースを取得する方法です。カジュアルなぶん、環境にかなり依存すると思われます。テストした環境はMacOSX 10.4.11 (PPC) です。
原理はとても単純で、以下の3ステップです。
- __builtin_return_address() 関数を使って、関数の戻るポインタを取得する。
- 引数を1,2,3..インクリメントしていき、スタックの一番下まで行ったところで NULL が返ってくるという性質を利用する(環境依存)。
- 少し変なところとしては、__builtin_return_address() に変数を与えると、コンパイルエラーになるため、リテラルを使って平に書き下す。
- dlfcn.h で提供されている dladdr() を使って、そのポインタが含まれるモジュールおよび関数名を取得する。
- 必要があれば 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() を使って、カジュアルにバックトレースをダンプする例を紹介しました。取りあえずコンセプトコードを書いただけで、ロクにテストしてませんので悪しからず。