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。