存取不合法位址時會產生 signal SIGSEGV。重建呼叫流程需要的步驟如下:
- 程式加 SIGSEGV 的 signal handler 來 dump process 資訊、CPU 暫存器、和 stack。
- 編譯時加「-g」選項,並保留 unstripped 的 obj 檔或執行檔備用。
- 程式發生 SIGSEGV,取回 dump 資訊。
- 分析 dump 資訊找出「程式碼位址」。
- 用「addr2line」轉換「程式碼位址」成原始碼檔名和行號,或者無除錯資訊時用「nm」取得函數名稱。
透過 sigaction() 安裝 SIGSEGV 時的呼叫函數「sa_sigaction」,其第三個引數會指到 ucontext_t 格式的資料 (定義在 ucontext.h),其中 mcontext_t 的資訊是處理器相關的,包括 CPU 暫存器等,定義在 sigcontext.h,在 ARM 是:
struct sigcontext {
unsigned long trap_no;
unsigned long error_code;
unsigned long oldmask;
unsigned long arm_r0;
unsigned long arm_r1;
unsigned long arm_r2;
unsigned long arm_r3;
unsigned long arm_r4;
unsigned long arm_r5;
unsigned long arm_r6;
unsigned long arm_r7;
unsigned long arm_r8;
unsigned long arm_r9;
unsigned long arm_r10;
unsigned long arm_fp; // frame pointer only if optimization disabled
unsigned long arm_ip; // temp workspace
unsigned long arm_sp; // top of stack,可反向建構出呼叫流程
unsigned long arm_lr; // link address/workspace,return 位址 (在 caller)
unsigned long arm_pc; // program counter,SIGSEGV 時的程式位址
unsigned long arm_cpsr;
unsigned long fault_address; // fault data address
};
PC 是當機時的 Program Counter,查詢 /proc/[pid]/maps 可以知道落在哪個檔案,如果是執行檔,「addr2line -ife <unstripped執行檔> <PC>」可以查到在原始碼檔名和行號。如果是 relocatable object (so 檔),除非編譯時有下「-g」參數,不然只能知道落在哪個 symbol。「arm-linum-nm -lnDS <so檔>」列出 symbol 的偏移及大小,PC 減去 so 檔載入的位址就是偏移,查表可知是哪個函數。
LR 是呼叫函數的下個指令,照 PC 的方式,可知道在是哪個函數裡面的位置,其前一個呼叫的函數就是 PC 所在的函數。
再來是透過 SP 回朔更多 link addresses,有幾個技巧:
- ARM 指令不在 thumb 模式都是 32-bit 對齊,所以只需要找 4 倍數的位址。
- 只會落在 maps 中可執行的檔案才可能是。
- addr2line 找 LR 所在的函數可能的呼叫者,一直往回推。
- arm-linux-nm -nS <unstripped執行檔> 可列出 symbol,但不會有 inline 函數,這個時候可以找原始檔 symbol 的位址範圍。
- PC 和 LR 可能都在函式庫。
可能進階改善
- 程式解析 log 資料自動重新建立呼叫流程。
- locate 並回復函數引數和 local 變數。
- 讓 log 資料 loadable by GDB。
注意:signal 呼叫的「sa_sigaction」函數是執行在 the context of a signal handler,呼叫的函數有限制,請「man 7 signal」查看 section “Async-signal-safe functions”。
沒有留言:
張貼留言