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 [email protected]:cloudsec/ajvm.git


      前3篇笔记:

     jvm开发笔记1---class文件解析器 : http://www.alibabatech.org/article/detail/3076/0

     jvm开发笔记2---java反汇编器: http://www.alibabatech.org/article/detail/3078/97

     jvm开发笔记3---java虚拟机雏形 :http://www.alibabatech.org/article/detail/3499/0

 

     最近笔者给ajvm增加了stack calltrace的功能, 用于帮助和调试jvm crash后的信息。 大家知道oracle的hotspot jvm在crash后会给出大量的crash信息, 这些信息能帮助jvm开发人员快速定位问题。同样, ajvm也增加了类似的功能:

 

    1、calltrace(),  打印函数调用栈。

    2、截获SIGSEGV信号, jvm segfault后, 打印离堆栈指针rsp最近的16字节信息;打印cpu寄存器信息;打印函数调用栈。

 

   首先看如何打印函数调用栈:

   笔者在《理解堆栈及其利用方法 》: http://www.alibabatech.org/article/detail/3500/4

   这篇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。


你可能感兴趣的:(jvm,java基础)