c 的 backtrace 是在 call function 時, 如何得知誰 call 了這個 function。
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
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 時做的動作, 參考 list 3 的反組譯。
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. L2, L4)。table 1. main call f1, f1 call f2 把 rsp 的內容整理起來。
table 1. main call f1; f1 call f2 stack 內容
0 | | | sp | |
1 | call f1 | | 0x7fffffffe058 | ret addr 0x4010c0 |
2 | | push %rbp (main_rbp) ; f1_rbp=rsp = 0x7fffffffe050 | 0x7fffffffe050 | main_rbp (0x7fffffffe070) |
3 | call f2 | | 0x7fffffffe048 | ret addr 0x4010a4 |
4 | | push %rbp (f1_rbp) ; f2_rbp=rsp = 0x7fffffffe040 | 0x7fffffffe040 | f1_rbp (0x7fffffffe050) |
指令 call 會發生的 sp 操作: rsp - 8, 再把 ret address 放入 rsp, 我用 gdb 把從 main 到 f2 時的 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 (backtrace(), backtrace_symbols() 可以追到 _start)。另外還有一個問題, 從這層的 rpb 一直追到上一層的 rpb, 要怎麼判定追到 __libc_start_main 這層了, 這邊是觀察出來的, 上一層的 rpb 應該會比目前這層的 rpb 大, 我就這樣判定, 有可能會出錯嗎? 當然有可能, 但我想不到別的辦法了。list 6 L105, L106 有類似的檢查條件。
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, 比較有可攜性。我不滿足這樣的作法, 想「知道」怎麼辦到的, 才有了本篇文章。
在 glibc 2.39 sysdeps/i386/backtrace.c __backtrace (void **array, int size) 可以看到類似的作法。
ref:
誰在呼叫我?不同的backtrace實作說明好文章
沒有留言:
張貼留言
使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。
我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。