c 的 backtrace 如何做到, 問 chatgpt 馬上就給出 list 1. 的範例程式, 真的好用, 以 list 2 來說:
_start -> __libc_start_main -> main -> f1 -> f2 -> f3 -> print_backtrace會得到
xx [0x400b7b] xx [0x400d60] xx [0x401087] xx [0x4010a4] xx [0x4010c0] xx [0x401689] xx [0x400a5a]使用 addr2line 可以查到對應的 function
descent@u64:backtrace$ addr2line -f -e t2 0x400b7b print_backtrace /home/descent/git/progs/backtrace/t2.c:15 addr2line -f -e t2 0x400a5a _start ??:?會得到:
print_backtrace -> f3 -> f2 -> f1 -> main -> __libc_start_main -> _start
但如果再問 chatgpt 要怎麼實做 backtrace(), backtrace_symbols(), 它就鬼打牆了。
所以如果不借助 backtrace(), backtrace_symbols() 要怎麼辦到呢?
這和平台有關, 本篇是在 x86_64 環境的實做。
先來理解 c 語言呼叫 function 時做的動作。
L902 main call f1, L884 ~ L886 是進入 f1 時做的事情。
902 4010bb: e8 ca ff ff ff callq 40108a <f1> main call f1 時, f1 會做 884 000000000040108a <f1>: 885 40108a: 55 push %rbp 886 40108b: 48 89 e5 mov %rsp,%rbp 890 40109f: e8 c9 ff ff ff callq 40106d <f2> f1 call f2 時, f2 會做 873 000000000040106d <f2>: 874 40106d: 55 push %rbp 875 40106e: 48 89 e5 mov %rsp,%rbp
push %rbp mov %rsp,%rbp都是在 function 的最開始時會做的事情。table 1. main call f1, f1 call f2 把 rsp 的內容整理起來。
0 | sp | |||
1 | call f1 | 0x7fffffffe058 | ret addr 0x4010c0 | |
2 | f1_rbp=rsp = 0x7fffffffe050 | 0x7fffffffe050 | main_rbp (0x7fffffffe070) | |
3 | call f2 | 0x7fffffffe048 | ret addr 0x4010a4 | |
4 | f2_rbp=rsp = 0x7fffffffe050 | 0x7fffffffe050 | f1_rbp (0x7fffffffe050) |
指令 call 會發生的 sp 操作: rsp - 8, 再把 ret address 放入 rsp, 我用 gdb 把從 main 到 f3 時的 stack 內容記錄在 table 1。
用了 -no-pie 是希望不要編譯成 relocation 的執行檔, 用 objdump 在對照位址時比較方便。其他編譯選項沒太大影響。
原理是這樣, 先抓到目前 rbp 的值, 假如目前在 f2, 抓到 f2_rpb 就可以知道 f1 function 的 rpb f1_rpb, 知道了 f1_rpb 就可以知道 main function 的 rpb, 那麼知道每個 function 的 rbp 要幹麻呢? 為了取得 return address, 怎麼取得, 每層 function 的 rbp + 8 的位址就可以得到 (對照 table 1. L1, L3)。
寫成 c code 就是: 上一層 function 的 return address = *(uintptr_t*)(rpb + 8)
那麼又怎麼從目前 function 的 rpb 值得到上一層的 rpb 值呢? 從 rpb 值的位址取得 (參考 table 1. L2, L4), 寫成 c code 就是: 上一層 function 的 rpb = *(uintptr_t*)(rpb)
這樣一層一層追, 就可以追到 __libc_start_main, 那 _start 追得到嗎? 抱歉, 目前我還不知道怎麼從 __libc_start_main 追到 _start。另外還有一個問題, 從這層的 rpb 一直追到上一層的 rpb, 要怎麼判定追到 __libc_start_main 這層了, 這邊我是觀察出來的, 上一層的 rpb 應該會比目前這層的 rpb 大, 我就是這樣判定的, 有可能會出錯嗎? 當然有可能, 但我想不到別的辦法了。
list 2. L81 ~ L139 就是在做這樣的事情。另外還需要知道目前在那個 function, 所以用了 L77 得到目前的位址。這些抓暫存器、抓目前位址都是透過 chatgpt 問到, 相當方便。
再來有了位址要怎麼找出對應的 function, 這邊我偷懶了, 直接使用 addr2line 這個指令幫忙。光是 addr2line 怎麼辦到的, 可能又是一個主題了。
list 2 L142, L146 就是在取得 rbp 和 return address, __builtin_frame_address(), __builtin_return_address() 是 gcc 內建 function, 比較有可攜性。我不滿足這樣的作法, 想「知道」怎麼辦到的, 才有了本篇文章。
ref:
誰在呼叫我?不同的backtrace實作說明好文章