微调jvm源码,实现执行流在clion自由下断

修改JVM源码,方便执行流断点

一、 说明一下

1.只是基于我对jvm的熟悉程度,不知道有没有更直接的方法,如果哪位大神有请赐教
2.ubuntu16.4,x86_64,clion调试jdk1.8+
3.以monitorenter字节码为例,做下微调

二、前置条件

1.会汇编基础,不需要深基本指令和32,64位寄存器
2.会C稍微熟练,最起码的函数申明和使用要会
3.对jvm的执行流了解
…只是断点以上就行–附加…
4.想要理解原理,得了解指针、内存及内存偏移

三、我遇到的问题

1.使用GDB断点时在类比较复杂字节码比较多的情况下就比较蛋疼。需要通过字节码指令一个个找过去在GDB中要输入无数的si、ni、b *xxxx、c等执行命令,有时候还要做一些偏移计算才行。还要通过i r 、p /x 、x /xg等命令时刻关注进入的断点是不是你想要的信息,并且一旦错过就得蛋疼的重启。

2.使用-XX:+PrintInterpreter打印出字节码执行流地址直接GDB跳转。好是好就是有个问题java字节码中out vtos和in vtos的组合是必现的,在这种组合下有些字节码的入口地址并不是打印出来的地址

举例:

main方法中有字节码
..........................
         7: astore_1
         8: aload_1
..........................

通过控制台打印aload_1 “43 aload_1 [0x00007f3b987d5420, 0x00007f3b987d5480]“,0x00007f3b987d5420是aload_1的首地址,使用GDB时一般就直接命令b *0x00007f3b987d5420 ,然后c过去了,然后就没有然后,要重启了。因为这个地址并不是astore_1和aload_1两个字节码组合后的aload_1首地址在这里下断根本不会跳入这个断点。
看下汇编及地址:

/**这是首地址**/			0x00007f3b987d5420	push   %rax
						0x00007f3b987d5421	jmpq   0x7f3b987d5450

..........................................................................

/** 实际组合的首地址**/	0x00007f3b987d5450	mov    -0x8(%r14),%rax
						0x00007f3b987d5454	movzbl 0x1(%r13),%ebx

..........................................................................

						0x00007f3b987d547f	int3
/**这是尾地址**/			0x00007f3b987d5480	movabs 0x84cccccccc000000,%al

为什么不能在0x00007f3b987d5420位置下断,在“四、修改源码前的一些必须点“的3.2说明。
所以我花了两个小时微调了代码,让ide可以直接断点

四、修改源码前的一些必须点

1.先搞一个demo

// An highlighted block
    public class Test {
        public  int  exp() throws InterruptedException {
            synchronized(this) {
                exp();//死循环,这里不是重点,不用在意
            }
                
        }

        public static void main(String[] args) throws InterruptedException {
            Test test = new Test();
            test.exp();

        }
    }

2.javap -verbose

// An highlighted block
  public int exp() throws java.lang.InterruptedException;
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
...............省略.....................

  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #7                  // class Test
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #2                  // Method exp:()I

.................省略......................
}

3.必要的字节码执行过程

根据java字节码对这些字节码的堆栈及寄存器分析。还有一些额外知识补充

3.1 字节码new

在_new字节码的实现中会将这个对象所在的堆或者TLAB的地址放入rax寄存器,然后由dup字节码先将rax压栈(这里指的是os的堆栈)后在将栈顶的数据取出在压入,以做到复制栈顶并压栈的操作,先看下_new的部分汇编:

在这里插入图片描述
截图部分只是tlab的分配,如果是首次创建会走slow_case,通过InterpreterRuntime::_new创建,但是结果都是放到rax寄存器中

3.2 字节码dup

字节码是否对os堆栈进行压入取决上一个字节码的outToState和当前字节码的inTostate(如何判定和有什么用可以跳转到 (https://www.processon.com/view/link/62d7f0ac07912923e888d919)流程图右下半侧 )。简单的说就是outToState和inTostate是在字节码执行流前对栈必要操作
1.当前字节码的inTostate不是vtos的需要先pop rax。
2.上一个字节码的outTostate不是vtos但是当前字节码的inTostate是vtos就要push rax。
3.当前字节码的inTostate和上一个字节码的outToState都不是vtos就要先pop在push
4.两则都是vtos的不需要对栈做前置操作

_new字节码的outTostate是atos,而dup的inTostate是vtos,所以在_dup字节码执行前要先push rax也就是当前类的oop压栈。

dup汇编堆栈
在这里插入图片描述在这里插入图片描述
3.3 字节码invokespecial

这个展开的话有点复杂,从ConstatPool、ConstatPoolCache、ConstatPoolCacheEntry到klass再到oop,从元空间到堆再到GC都有涉及。
在这个dome中只要知道这个字节码就是new Test()的init,会跳转到构造方法的栈中将Klass和ConstatPool链接,将nonStatic的变量初始化、赋值等操作,最后执行_return字节码返回调用者方法的栈

3.4 字节码astore_1

astore_1:将引用类型或returnAddress类型值存入局部变量1
这里回到main方法的执行栈帧。为什么这里用astore_1,因为在局部变量表中参数先压栈,局部变量后压栈,非static方法的有一个隐式的this*参数第一个压栈,而static方法没有。main是static但是有一个args入参所以局部变量表第一位是这个入参。

main方法的栈和栈帧以及局部变量表的指向
在这里插入图片描述

局部变量表寄存器r14的状态

3.5 字节码aload_1

aload_1:从局部变量1中装载引用类型值,它的outTostate为atos,所以这里是将局部变量表的第二个变量放入rax在下一个字节码中push rax
汇编:
在这里插入图片描述

3.6 字节码invokevirtual

复杂,功能和invokespecial挺像字节码执行完后的栈和寄存器
在这里插入图片描述
局部变量表
在这里插入图片描述

4.转入exp()方法

4.1 字节码aload_0

取局部变量表的第一个变量。从方法上看,这是一个非static方法有一个隐式入参this指针,所以读取的就是当前类的oop地址。
汇编是 mov rax,r14;

4.2 字节码dup

上个字节码aload_0的outTostate是atos,dup的inTostate是vtos,所以push rax ;然后复制栈顶并压栈 mov rax,[rsp]; push rax;

4.3 字节码monitorenter

直接看汇编

	pop    %rax
	cmp    (%rax),%rax
	push   %rax
	mov    0x8(%rax),%eax
	............................省略...................................

第一步就是将栈顶弹出到rax,在上个字节码dup执行完时栈顶的数据是oop,这里pop后rax就是指向oop的指针有了这个指针就还有啥取不到
oop长这样在这里插入图片描述

五、断点代码实现

1.第一次修改

既已获取到oop那么,根据oop的数据结构可以整理出代码的构思及雏形。由于是ubunt所以使用AT&T

1.1 首先不能影响原先逻辑,那就要保存现场,而我这只需要一个标识可以让我断点所以只要一个寄存器就行了,那保存现场就直接push rax
1.2 x86_64下rax需要加+8才能拿到Klass*, 之所以用eax是因为默认开启了指针压缩,八字节数据中除了压缩后的四字节对象指针外可能还有nonStatic的类变量数据存在(看不懂先了解下大小端的存储和读取)
mov 0x8(%rax),eax; 

1.3 压缩的指针decode,rax得完整的指针(只适用Xmx参数小于等于4G,原因等我把元空间的文章写出来)

shl 0x3,rax;

1.4 这里可以分支一下,如果只是想要klass信息的rax就是.想要拿到类名就需要做内存偏移,klass类中Symbol* _name属性是存当前类的类名,偏移是24,汇编就是

mov  0x18(rax),rax;

1.5 代码的对称性,既然上面保存了现场这里就需要恢复现场,pop rax

完整汇编

	push rax;
	mov 0x8(%rax),eax;
	shl 3,rax;
	mov  0x18(rax),rax;
	pop rax

//转换成jvm的代码
    push(rax);
    movl(rax,Address(rax, 0x8));
    shlptr(rax,0x3);
    movptr(rax,Address(rax, 0x18));
    //这里留一个代码插入点
    pop(rax);

1.6 ide设置断点必须在源码上,汇编只能通过GDB调试(vs确实好用,可惜没有ubuntu版)。那就要一个函数来接收rax,我模仿了jvm的宏在

//在interpreterRuntime.hpp申明一个函数
static void    breakpoint(JavaThread* thread, Symbol* cname     /** klass *k **/);

//在interpreterRuntime.cpp中实现
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::breakpoint(JavaThread* thread, Symbol* cname))
if(strcmp(cname->as_class_string() ,"Test") == 0 ){
    int isBreakpoint  = 1;
}
IRT_END

//interpreterRuntime.hpp没有引入string.h strcmp用不了,所以要自己加一下
#include<string.h>//放上面去

//cname->as_class_string()这个函数也是自己定义的,因为Symbol的as_C_string()函数在调用时经常取不到当前线程的arean区域而报出段错误,暂时不知道啥原因,所以我就直接申请os内存了
//在symbol.hpp申明
char* as_class_string() const;

//在symbol.cpp实现
char* Symbol::as_class_string() const {
    int len = utf8_length();
    char* str = (char*)malloc(len+1);
    return as_C_string(str, len + 1);
}

1.6 函数搞定后将breakpoint函数插入到"代码插入点"。完整的代码:

    push(rax);
    movl(rax,Address(rax, 0x8));
    shlptr(rax,0x3);
    movptr(rax,Address(rax, 0x18));
    call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::breakpoint), rax);
    pop(rax);   

call_VM jvm自己实现了保存、恢复现场

1.7 进入断点

在这里插入图片描述

六、代码改进

我是一个javaer,所以魔数这玩意有点不太喜欢。

	/** movl(rax,Address(rax, 0x8)); **/movl(reg,Address(reg, wordSize));
    /** shlptr(rax,0x3);             **/shlptr(reg,LogMinObjAlignmentInBytes);

并且像_name在kalss中是private的,假如我要改成public以方便在其他函数中调用就要在其他位置重新申明,这样会造成offset的变化所以在这里可以改进一下

// klass.hpp中实现一个静态函数 
    static ByteSize cname_offset()                 { return in_ByteSize(offset_of(Klass, _name)); }

//这样 movptr(rax,Address(rax, 0x18));这段就可以改成
    movptr(reg,Address(reg, Klass::cname_offset()));

最后在封装一下方便其他地方用

//在interp_masm_x86_64.hpp中申明函数
  void breakpoint(Register reg);

//在interp_masm_x86_64.cpp中实现函数
void InterpreterMacroAssembler::breakpoint(Register reg){
    push(reg);
    movl(reg,Address(reg, wordSize));
    shlptr(reg,LogMinObjAlignmentInBytes);
    movptr(reg,Address(reg, Klass::cname_offset()));
    call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::breakpoint), reg);
    pop(reg);
}

最后的最后

void TemplateTable::monitorenter() {
    transition(atos, vtos);

    // check for NULL object
    __ null_check(rax);
    __ breakpoint(rax);//这里调用
    .......................省略部分............................
}

上面的是静态的写死类名为Test,每次换个类就要重新编译比较麻烦,想动态参数断点我的想法是:

在RUNTIME_FLAGS宏申明自己的宏,然后在启动参数中增加自己的参数就可以将strcmp(cname->as_class_string() ,“Test”)中的Test改成动态的了,例如:
在RUNTIME_FLAGS中加入一个MyFlagTest
在这里插入图片描述
在启动参数中加入了在这里插入图片描述在这里插入图片描述
这样就动态不需要重新编译了,也可以在在启动参数写入多个值,用标识符号分割,然后循环下比对,我懒得实现了

结语

我个人的想法是在试着改jvm源码,而不是只想搞一个方便断点的工具

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值