作者:王智通
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与我讨论。