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寄存器信息;打印函数调用栈。

 

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

笔者在《理解堆栈及其利用方法 》: http://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。

 

欢迎大家到我的个人站点: http://www.cloud-sec.org与我讨论。

时间: 2024-09-27 06:47:07

jvm开发笔记4&#8212;jvm crash信息处理的相关文章

jvm开发笔记3&amp;#8212;java虚拟机雏形

作者:王智通   一.背景 笔者希望通过自己动手编写一个简单的jvm来了解java虚拟机内部的工作细节毕竟hotsopt以及android的dalvik都有几十万行的c代码级别. 在前面的2篇开发笔记中已经实现了一个class文件解析器和一个java反汇编器 在这基础上 java虚拟机的雏形也已经写好.还没有内存管理功能 没有线程支持.它能解释执行的指令取决于我的java语法范围 在这之前我对java一无所知 通过写这个jvm顺便也把java学会了 它现在的功能如下 1.java反汇编器 山寨了

jvm开发笔记2&amp;#8212;java反汇编器

作者:王智通   这两天在class文件解析器的基础上, 加上了java反汇编的功能, 反汇编器是指令解释器的基础,通过编写反汇编器可以熟悉jvm的指令系统, 不过jvm的指令一共有201个,反汇编过程基本就是个体力活.在<java虚拟机规范>中对每一条指令都有了详细的描述,下面说说我是如何解析bytecode的: 一个java文件经过javac编译后会生成class格式文件, 在class格式中method字段里会有Code属性,Code属性包含了java的指令码和长度. 首先用class解

jvm开发笔记1&amp;#8212;class文件解析器

作者:王智通   笔者最近对java虚拟机产生了浓厚的兴趣, 想了解下最简单的jvm是如何写出来的,于是看起了<java虚拟机规范>,这个规范如同intel开发手册一样,是每个jvm开发人员必须掌握的. 要想翻译执行java byte code, 首先得从java class文件中把Code属性解析出来才行. 在笔者看来, java的class文件结构着实比elf文件结构复杂很多,不过在复杂的结构, 只要耐心对照着手册中的结构一一解析即可, 经过几天的努力, 用c实现了一个class文件解析器

jvm开发笔记5 &amp;#8211; 虚拟机内存管理

作者:王智通   一. 前言 ajvm是笔者正在开发中的一个java虚拟机, 想通过编写这个jvm帮助程序员了解jvm的具体实现细节, 它是国内第一个开源的java虚拟机项目:https://github.com/cloudsec/ajvm, 同时笔者把它的开发笔记也分享到了ata上. 在前面4篇笔记中, 已经实现了class文件加载器, 反汇编器,jvm的crash信息处理, 同时它已经能运行简单的java代码了. 在今天的这篇笔记中, 将开始分享ajvm的内存管理模块是如何编写的. 二.内存

JVM学习笔记(四)------内存调优

首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM. 对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量.特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC

JVM学习笔记(四)------内存调优【转】

转自:http://blog.csdn.net/cutesource/article/details/5907418 版权声明:本文为博主原创文章,未经博主允许不得转载. 首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM. 对JVM内存的系统级的调优主要的目的是减少GC的频率和Fu

深入了解JVM-----Inside JVM读书笔记

笔记   本文首先介绍一下Java虚拟机的生存周期,然后大致介绍JVM的体系结构,最后对体系结构中的各个部分进行详细介绍. (  首先这里澄清两个概念:JVM实例和JVM执行引擎实例,JVM实例对应了一个独立运行的java程序,而JVM执行引擎实例则对应了属于用户运行程序的线程:也就是JVM实例是进程级别,而执行引擎是线程级别的.) 一. JVM的生命周期 JVM实例的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String

如何从 Classic JVM 迁移到 IBM J9 JVM

简介 从 IBM i 7.1 开始,IBM Classic Java Virtual Machine 不再被 IBM i 支持了.IBM Technology for Java Virtual Machine(又名 IBM J9 JVM)成为了唯一被支持的 JVM.这篇文章旨在阐述这两种 JVM 的区别,同时帮助用户和开发人员把他们的应用程序从 Classic JVM 移植到 J9 JVM.在 IBM i 上,所有版本的 Java 开发包(JDK)都以 Java 许可程序的 option 的形式

PHP微信公众开发笔记(五)

PHP微信公众开发笔记系列 日期:2014.9.3 今天做了身份验证的功能,然后完善了下搜索功能.其实主要的是将整个代码结构整理了一番,应该可以说是模块化设计吧. 模块化设计我们的公众号. 因为我们之前提的功能需求中有: 1.菜单--查询功能.我考虑到后期功能的扩展,就想将这些分模块来实现:菜单模块(这样,今后我们需要添加新的菜单功能,可以直接在这个模块里操作,这样修正和维护也简单,在考虑到后期可能会分工协作的时候各开发者之间不会产生冲突): 2.数据库模块(这里就主要是负责数据库相关的工作,如