jvm開發筆記4—jvm crash信息處理
作者:王智通
ajvm是一個筆者正在開發中的java虛擬機, 用c和少量匯編語言編寫, 目的在於探究一個可運行的java虛擬機是如何實現的, 目前整個jvm的source code代碼量在5000行左右, 預計控製在1w行以內,隻要能運行簡單的java代碼即可。筆者希望ajvm能變成一個教學用的簡單java虛擬機實現, 幫助java程序員在陷入龐大的hotspot vm源碼之前, 能對jvm的結構有個清晰的認識。
ajvm是筆者利用業餘時間編寫的, 每次完成一個重要功能都會以筆記的形式發布到ata, 和大家共同學習和探討。
git repo: https://github.com/cloudsec/ajvm git clone git@github.com:cloudsec/ajvm.git
最近筆者給ajvm增加了stack calltrace的功能, 用於幫助和調試jvm crash後的信息。 大家知道oracle的hotspot jvm在crash後會給出大量的crash信息, 這些信息能幫助jvm開發人員快速定位問題。同樣, ajvm也增加了類似的功能:
1、calltrace(), 打印函數調用棧。
2、截獲SIGSEGV信號, jvm segfault後, 打印離堆棧指針rsp最近的16字節信息;打印cpu寄存器信息;打印函數調用棧。
首先看如何打印函數調用棧:
筆者在《理解堆棧及其利用方法 》: https://blog.aliyun.com/964?spm=0.0.0.0.BykR2E
這篇paper中詳細講述了intel x86和x86_64下進程堆棧的結構, 關於堆棧的基礎知識請大家參考此paper。
下麵舉一個簡單的例子:
#include #include "trace.h" #include "log.h" void test2() { calltrace(); *(int *)0 = 0; } void test1() { test2(); } void test() { test1(); } int main(void) { log_init(); GET_BP(top_rbp); calltrace_init(); test(); return 0; }
在test2函數中調用了calltrace()函數, 用來打印它的函數調用棧, 我們知道它的函數調用棧是這樣的: main->test->test1->test2->calltrace。我們想讓calltrace的輸出信息類似如下:
test2 test1 test main
要完成此功能, 我們要利用gcc編譯器的一個特點, 注意在-O2或-fomit-frame-pointer參數下, 這個方法就無效了。 反匯編這個程序後, 會發現每個函數調用的開頭總會有這麼幾句匯編指令:
0000000000401138 : 401138: 55 push %rbp 401139: 48 89 e5 mov %rsp,%rbp 000000000040114e : 40114e: 55 push %rbp 40114f: 48 89 e5 mov %rsp,%rbp 000000000040115e : 40115e: 55 push %rbp 40115f: 48 89 e5 mov %rsp,%rbp 000000000040116e : 40116e: 55 push %rbp 40116f: 48 89 e5 mov %rsp,%rbp
大家想起來了吧, rbp在intel處理器中代表的是一個堆棧中棧幀開始的地址, rsp代表當前堆棧棧頂的地址。在c語言中一個函數的調用過程是這樣的:
test() { test1(); }
在test函數中調用test1()的時候, cpu會先自動把test1函數後麵的指令地址壓入test1函數的棧幀裏, 然後在執行push rbp; mov rsp, rbp指令。 我們畫一下,從main函數到calltrace函數的整個堆棧棧幀結構:
|...| |rbp|<--| push rbp; mov rsp, rbp ctrace->|rip| | call calltrace + 1 |...| | |rbp|<--| push rbp; mov rsp, rbp test2-> |rip| | call test2 + 1 |...| | |rbp|<--| push rbp; mov rsp, rbp test1-> |rip| | call test1 + 1 |...| | |rbp|<--| push rbp; mov rsp, rbp test-> |rip| | call test + 1 |...| | |rbp|<--| push rbp; mov rsp, rbp main-> |rip| | call main + 1 |...| | glibc |...|<--| rbp->unkonwn
所以在正常情況下堆棧的棧幀中每個rbp後麵,保存的都是上一個函數的返回地址, calltrace的實現其實就很簡單了, 首先得到rbp的地址,然後rbp後麵的地址就是ret rip的地址, 通過這個地址,我們可以解析出棧幀對應的符號信息, 因為ajvm通過自己解析elf文件, 來獲得符號表信息。 calltrace的大致實現如下:
void calltrace(void) { CALL_TRACE trace, prev_trace; uint64_t *rbp, rip, real_rip; int flag = 0, first_bp = 0; printf("Call trace:\n\n"); GET_BP(rbp) while (rbp != top_rbp) { rip = *(uint64_t *)(rbp + 1); rbp = (uint64_t *)*rbp; real_rip = compute_real_func_addr(rip); if (flag == 1) { if (search_symbol_by_addr(real_rip, &prev_trace) == -1) { __error("calltrace: search symbol failed."); exit(-1); } prev_trace.rip = rip - 5; prev_trace.offset = trace.rip - prev_trace.symbol_addr; show_calltrace(&prev_trace); trace = prev_trace; } else { if (search_symbol_by_addr(real_rip, &trace) == -1) { __error("calltrace: search symbol failed."); exit(-1); } trace.rip = rip - 5; flag = 1; } } printf("\n"); }
我們剛才講ajvm還截獲了進程的SIGSEGV信號處理流程, 在jvm初始化的時候,通過signal_init()來實現:
int signal_init(void) { struct sigaction sa; sa.sa_flags = SA_SIGINFO; sa.sa_sigaction = signal_handler; sigemptyset(&sa.sa_mask); if (sigaction(SIGSEGV, &sa, NULL) == -1) { perror("sigaction"); return -1; } return 0; }
當jvm crash後, signal_handler()函數接管了信號的處理流程, 注意此時整個jvm進程的堆棧結構跟calltrace結構有一點不一樣:
|...| |rbp|<--| push rbp; mov rsp, rbp do_sig->|eip| | unkown |...|<----- segfault |...| |rbp|<--| push rbp; mov rsp, rbp test2-> |rip| | call test2 + 1 |...| | |rbp|<--| push rbp; mov rsp, rbp test1-> |rip| | call test1 + 1 |...| | |rbp|<--| push rbp; mov rsp, rbp test-> |rip| | call test + 1 |...| | |rbp|<--| push rbp; mov rsp, rbp main-> |rip| | call main + 1 |...| | glibc |...|<--| rbp->unkonwn
test2並沒有調用do_sig函數, 這是因為test2函數裏有一個空指針引用的操作, 操作係統內核在處理這個缺頁異常中斷的時候, 向進程發送了SIGSEGV信號, 通常情況下, 會直接殺死進程, 但是這個信號被do_sig函數接管了, 我們要在這個函數裏打印充足的調試信息後, 在退出進程。
void signal_handler(int sig_num, siginfo_t *sig_info, void *ptr) { CALL_TRACE trace, prev_trace; uint64_t *rbp, rip, real_rip; int flag = 0, first_bp = 0; assert(sig_info != NULL); printf("\nPid: %d segfault at addr: 0x%016x\tsi_signo: %d\tsi_errno: %d\n\n", getpid(), sig_info->si_addr, sig_info->si_signo, sig_info->si_errno); show_stack(); show_registers(); printf("Call trace:\n\n"); GET_BP(rbp) while (rbp != top_rbp) { rip = *(uint64_t *)(rbp + 1); rbp = (uint64_t *)*rbp; real_rip = compute_real_func_addr(rip); if (flag == 1) { if (search_symbol_by_addr(real_rip, &prev_trace) == -1) { __error("calltrace: search symbol failed."); exit(-1); } prev_trace.rip = rip - 5; if (first_bp == 0) { first_bp = 1; prev_trace.offset = 0; } else { prev_trace.offset = trace.rip - prev_trace.symbol_addr; } show_calltrace(&prev_trace); trace = prev_trace; } else { /* it's in a single handler function, the last call frame is unkown, * we can't locate the rip addr. */ search_symbol_by_addr(real_rip, &trace); trace.rip = rip - 5; flag = 1; } } printf("\n"); exit(0); }
至於show_stack()和show_registers()函數就很簡單了:
#define GET_BP(x) asm("movq %%rbp, %0":"=r"(x)); #define GET_SP(x) asm("movq %%rsp, %0":"=r"(x)); #define GET_AX(x) asm("movq %%rax, %0":"=r"(x)); #define GET_BX(x) asm("movq %%rbx, %0":"=r"(x)); #define GET_CX(x) asm("movq %%rcx, %0":"=r"(x)); #define GET_DX(x) asm("movq %%rdx, %0":"=r"(x)); #define GET_SI(x) asm("movq %%rsi, %0":"=r"(x)); #define GET_DI(x) asm("movq %%rdi, %0":"=r"(x)); #define GET_R8(x) asm("movq %%r8, %0":"=r"(x)); #define GET_R9(x) asm("movq %%r9, %0":"=r"(x)); #define GET_R10(x) asm("movq %%r10, %0":"=r"(x)); #define GET_R11(x) asm("movq %%r11, %0":"=r"(x)); #define GET_R12(x) asm("movq %%r12, %0":"=r"(x)); #define GET_R13(x) asm("movq %%r13, %0":"=r"(x)); #define GET_R14(x) asm("movq %%r14, %0":"=r"(x)); #define GET_R15(x) asm("movq %%r15, %0":"=r"(x)); void show_stack(void) { int i; uint64_t *rsp, *rbp; GET_SP(rsp); GET_BP(rbp); printf("Stack:\t\t\nrsp: 0x%016x\t\trbp: 0x%016x\n", rsp, rbp); for (i = 0; i < 16; i++) { printf("0x%02x ", *((unsigned char *)rsp + i)); } printf("\n\n"); } void show_registers(void) { uint64_t rax, rbx, rcx, rdx, rsi, rdi; uint64_t r9, r10, r11, r12, r13, r14, r15; GET_AX(rax) GET_BX(rbx) GET_CX(rcx) GET_DX(rdx) GET_SI(rsi) GET_DI(rdi) GET_R9(r9) GET_R10(r10) GET_R11(r11) GET_R12(r12) GET_R13(r13) GET_R14(r14) GET_R15(r15) printf("Registers:\n"); printf("rax = 0x%016x, rbx = 0x%016x, rcx = 0x%016x, rdx = 0x%016x\n" "rsi = 0x%016x, rdi = 0x%016x, r8 = 0x%016x, r9 = 0x%016x\n" "r10 = 0x%016x, r11 = 0x%016x, r12 = 0x%016x, r13 = 0x%016x\n" "r14 = 0x%016x, r15 = 0x%016x\n\n", rax, rbx, rcx, rdx, rsi, rdi, r9, r10, r11, r12, r13, r14, r15); }
最後演示一下ajvm在crash後的出錯信息:
Pid: 8739 segfault at addr: 0x0000000000000000 si_signo: 11 si_errno: 0 Stack: rsp: 0x00000000caa88680 rbp: 0x00000000caa886a0 0x90 0x87 0xa8 0xca 0xff 0x7f 0x00 0x00 0x58 0xd3 0xe4 0x3d 0x0c 0x00 0x00 0x00 Registers: rax = 0x000000003de6c144, rbx = 0x000000003e151780, rcx = 0x0000000000000001, rdx = 0x0000000000000001 rsi = 0x000000003de6317a, rdi = 0x0000000000000000, r8 = 0x00000000caa886a0, r9 = 0x0000000000000000 r10 = 0x000000000040accf, r11 = 0x00000000caa88790, r12 = 0x000000003de4d358, r13 = 0x00000000caa88680 r14 = 0x00000000caa886a0, r15 = 0x000000000000000b Call trace: [<0x401457>] jvm_pc_init + 0x0/0x42 [<0x4015dc>] jvm_run + 0x4b/0x7d
利用這個crash信息, 可以幫助程序員快速定位ajvm的bug。
歡迎大家到我的個人站點: https://www.cloud-sec.org與我討論。
最後更新:2017-04-03 07:57:05