閱讀864 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

  上一篇:go 大數據處理之如何確保斷電不丟數據
  下一篇:go Swift字符串類型