Closed12

libbacktrace code reading

tanishikingtanishiking

libunwind から backtrace を作るコードは

Programmatic access to the call stack in C++ - Eli Bendersky's website にあるように

#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>

// Call this function to get a backtrace.
void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

  // Initialize cursor to current frame for local unwinding.
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);

  // Unwind frames one by one, going up the frame stack.
  while (unw_step(&cursor) > 0) {
    unw_word_t offset, pc;
    unw_get_reg(&cursor, UNW_REG_IP, &pc);
    if (pc == 0) {
      break;
    }
    printf("0x%lx:", pc);

    char sym[256];
    if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
      printf(" (%s+0x%lx)\n", sym, offset);
    } else {
      printf(" -- error: unable to obtain symbol name for this frame\n");
    }
  }
}

void foo() {
  backtrace(); // <-------- backtrace here!
}

void bar() {
  foo();
}

int main(int argc, char **argv) {
  bar();

  return 0;
}

こうして

gcc -o libunwind_backtrace -Wall -g libunwind_backtrace.c -lunwind

こうじゃ

$ LD_LIBRARY_PATH=/usr/local/lib ./libunwind_backtrace
0x400958: (foo+0xe)
0x400968: (bar+0xe)
0x400983: (main+0x19)
0x7f6046b99ec5: (__libc_start_main+0xf5)
0x400779: (_start+0x29)

We can obtain the function symbol names and the address of the instruction where the call was made (more precisely, the return address which is the next instruction).

良かったですね、なんですが行番号やファイル名も欲しいんだよなぁ!

Fortunately, it's all in the DWARF information of the binary, and given the address we can extract the exact call location in a number of ways. The simplest is probably to call addr2line

でも runtime が addr2line 呼び出しとかしてほしくないし、addr2line 相当のコードをコピペするのもなんだかなという話ですわ。

ひとまずは https://github.com/eliben/pyelftools/blob/master/examples/dwarf_decode_address.py みたいなコード書けば良さそうなんだけど、libbacktrace ってやつもあって、こっちのほうが production 向けって感じがするので、最終的には dwarf_decode_address みたいなことやるにしても、libbacktrace の流れ読んでおいても良いでしょう!

C/C++: printing stacktrace containing file name, function name, and line numbers using libbacktrace - Jiyang Tang

tanishikingtanishiking

libbacktrace をどうやって使うかって〜いうと、テスト読むと良さそうで

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/btest.c#L482-L492

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/btest.c#L83

backtrace_create_state でなんか state を初期化して、

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/backtrace.h#L69-L89

backtrace_full でバックトレースを取得

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/backtrace.h#L104-L117

backtrace_create_state で executable から dwarf information を読んで、キャッシュを作っておいて、backtrace_full はそれを読んで...って感じかな

tanishikingtanishiking

backtrace_full はこんな感じ

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/backtrace.c#L102-L129

_Unwind_Backtrace (unwind, &bdata);bdata->can_alloc が >= 0 であることをチェックしたり、bdataにもろもろのデータを置いたりしそう

bdataが void pointer なのでどういうデータがあるのかよく分からんけどああこれですね

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/backtrace.c#L45-L61

いや! bdata->data がやっぱ void* だった

tanishikingtanishiking

どういう仕組みかよく分からんけど _Unwind_Backtrace はこれに行き着きそうで

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/backtrace.c#L63-L97

この backtrace_pcinfo ってやつでなんかいい感じに bdata->data にデータを入れるのね

  if (!bdata->can_alloc)
    bdata->ret = bdata->callback (bdata->data, pc, NULL, 0, NULL);
  else
    bdata->ret = backtrace_pcinfo (bdata->state, pc, bdata->callback,
				   bdata->error_callback, bdata->data);
tanishikingtanishiking

fileline_initialize

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/fileline.c#L158-L163

なんか自分自身を読み込む方法いろいろあるっぽい

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/fileline.c#L195-L235

state->filename
libstacktracebacktrace_create_state の引数に渡したやつ

FILENAME is the path name
of the executable file; if it is NULL the library will try
system-specific path names. If not NULL, FILENAME must point to a
permanent buffer.

ほかはだいたいOS specific なインターフェースをいろいろ試してる感じね

c++ - Finding current executable's path without /proc/self/exe - Stack Overflow

そして取得した descriptor を使って

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/fileline.c#L259-L263

backtrace_initialize

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/internal.h#L280-L296

Read initial debug data from a descriptor, and set the
fileline_data, syminfo_fn, and syminfo_data fields of STATE.
Return the fileln_fn field in *FILELN_FN--this is done this way so
that the synchronization code is only implemented once.

ほうほう

各executableに対して実装されていて、例えばELF

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/elf.c#L7386-L7393

この elf_add ってやつで ELF をパースして読み込んでいく

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/elf.c#L7401-L7403

tanishikingtanishiking

長くなってきたので

elf_add はこういう感じでかなり長い

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/elf.c#L6495-L6507

PIE チェック

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/elf.c#L6629-L6633

実行ファイルを位置独立実行形式(PIE: Position Independent Code)にすると、実行ファイルは位置独立となり、任意のメモリアドレスにロードできるようになる。このため、PIEな実行ファイルをロードするアドレスをランダム化することによって、ROPなどのコード再利用攻撃に対処することが可能である。
位置独立実行形式(PIE)によって確保できるエントロピー(32bitの場合) - /dev/null

なんだけどこれだとaddressが一意に決まらなくて困るので(?)死ぬ(死ぬのは私であり、プロセスが死ぬわけではない)

debug_info と debug_line を読む

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/elf.c#L6679C1-L6680

うんうん

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/elf.c#L6793-L6797

elfファイルのdebugセクション分割とgdbの分割されたデバッグ情報のサポート機能めも - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ

なんかいろいろ頑張って最終的には

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/elf.c#L7273-L7277

backtrace_dwarf_add ってやつに行き着く

backtrace_dwarf_add

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/dwarf.c#L4340-L4352

debug_info とか読んで、マップを作る

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/dwarf.c#L4283-L4293

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/dwarf.c#L2425-L2430

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/dwarf.c#L2449-L2452

なんかしらんけど debug_aranges は使ってないらしい
なんかなくても良くね? みたいな話で clang もデフォルトでは debug_aranges 使わなくなったらしいしなんかそんなに深い理由はないのかもしれん
Consider emitting DWARF aranges · Issue #45246 · rust-lang/rust

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/dwarf.c#L4399

fileline_fndwarf_fileline って関数へのポイインタをセットする

https://github.com/ianlancetaylor/libbacktrace/blob/cdb64b688dda93bbbacbc2b1ccf50ce9329d4748/dwarf.c#L4234-L4240

dwarf_fileline は さっき作った debug_info のマッピングからDIEとか行情報取ってきて返すやつ

tanishikingtanishiking

当然 PIE だと ASLR によって実行ごとにアドレスが違ってひけまんな

https://stackoverflow.com/questions/49083152/libunwind-pc-value-not-working-with-addr2line

https://stackoverflow.com/questions/55025500/get-address-for-addr2line-in-pie-binary-inside-program

ていうか PIE なプログラムに埋め込まれてる DWARF の low_pc とかって何?

it looks like passing file offsets instead of addresses works. –
Aliaksei Kandratsenka

うーん?

このスクラップは2023/11/28にクローズされました