自学内容网 自学内容网

【Bistoury】Bistoury功能分析-在线debug

一、什么是Bistoury

去哪儿开源的应用诊断工具
详细可以看看这篇:
去哪儿一站式 Java 应用诊断解决方案 - Bistoury

二、快速开始

启动需要被debug的应用

 java -jar web-quick-practice-1.0-SNAPSHOT.jar

启动bistoury

./quick_start.sh -p <pid> -i 127.0.0.1 start

然后就可以在localhost:9091访问,启动的东西比较多,所以还是要等下的。账号密码默认admin

三、Bistoury在线debug效果

选择应用在这里插入图片描述
选择需要debug的类
在这里插入图片描述
如果没有配置代码仓库的地址,那就会展示Fernflower反编译后的代码
在这里插入图片描述
打断点,只能打1个断点,打多个需要先移除之前的。前端有校验行号对不对。
[图片]

每隔几秒,会自动发请求获取断点的执行结果。触发成功后自动移除断点集合中的断点
[图片]

四、Bistoury在线debug具体实现

行增强,增强了什么?

如果源代码是下面这样的话,我们在第四行添加断点

@GetMapping("/hello")
public String hello() {
    String str = "hello";
    return str;        // 在这里添加断点
}

代码就会被增强为这样。增强的逻辑大概是,判断这个断点是否存在 --> 存储局部变量 --> 命中断点(这里会将断点从集合中移除)–> 之后把局部变量、静态变量、全局变量、调用栈存到DefaultSnapshotStore中(后续QDebugSearchCommand,会去查这些debug数据)

@GetMapping({"/hello"})
public String hello() {
    String str = "hello";
    if (BistourySpys1.hasBreakpointSet("org/example/HelloContoller.java", 4)) {
        BistourySpys1.putLocalVariable("this", this);
        BistourySpys1.putLocalVariable("str", str);
        if (BistourySpys1.isHit("org/example/HelloContoller.java", 4)) {
            BistourySpys1.dump("org/example/HelloContoller.java", 4);
            BistourySpys1.fillStacktrace("org/example/HelloContoller.java", 4, new Throwable());
            BistourySpys1.endReceive("org/example/HelloContoller.java", 4);
        }
    }
    return str;
}

怎么保证反编译后的行号和.class记录的行号一样?

传给前端的时候,会带上行号之间的映射,前端的行号校验也是根据这个映射来的

// 
// Source code recreated from a .class file by Bistoury
// (powered by Fernflower decompiler)
// 

package org.example;

import java.lang.Exception;
import java.lang.Integer;
import java.lang.RuntimeException;
import java.lang.String;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.example.HelloContoller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloContoller {
    @GetMapping({"/hello"})
    public String hello() {
        String str = "hello";
        return str;
    }

    @GetMapping({"/hasError"})
    public String hasError() {
        String str = "hello";
        int i = 1 / 0;
        str = str + i;
        return str;
    }

    @GetMapping({"/throwE"})
    public String throwE() {
        String str = "hello";

        try {
            if ("hello".equals(str)) {
                throw new RuntimeException("哈哈哈哈");
            } else {
                return str;
            }
        } catch (Exception var3) {
            throw var3;
        }
    }

    @GetMapping({"/stream"})
    public String stream() {
        Integer a = 111;
        Integer b = 222;
        List<Integer> list = Arrays.asList(a, b, 5);
        List<Integer> numberList = (List)list.stream().filter((number) -> {
            return number < 100;
        }).collect(Collectors.toList());
        return numberList.toString();
    }

    @GetMapping({"/streams"})
    public String streams() {
        Integer a = 111;
        Integer b = 222;
        List<Integer> list = Arrays.asList(a, b, 5);
        List<Integer> res = (List)list.stream().map((num) -> {
            num = num + 100;
            return num;
        }).collect(Collectors.toList());
        return res.toString();
    }
}

Lines mapping:
17 <-> 23
19 <-> 24
25 <-> 29
27 <-> 30
29 <-> 31
30 <-> 32
35 <-> 37
38 <-> 40
39 <-> 41
41 <-> 45
42 <-> 46
44 <-> 43
49 <-> 52
50 <-> 53
51 <-> 54
52 <-> 54
54 <-> 54
55 <-> 55
56 <-> 55
57 <-> 57
59 <-> 58
65 <-> 63
66 <-> 64
67 <-> 65
68 <-> 65
70 <-> 65
72 <-> 66
73 <-> 67
74 <-> 68
75 <-> 69
77 <-> 70

五、行debug原理

行增强

怎么找到要增强的行?怎么样保证我们增强的行号是正确的?
我们发现,源代码里的行号和.class中的行号是一样的,如下图所示
[图片]
还有一个问题,假如我在某一行增强了这些debug逻辑,那.class的行号是不是就发生了变化?如果我要增强别的行,我是不是要先还原之前的增强逻辑,保证行号的正确🤔?
不用的,增强某一行可以不添加行号,大概的效果就是这样,可以看到,bistoury虽然增强了17行,但是这一大坨代码都算17行
[图片]
相当于这样的代码

@GetMapping({"/hello"})
public String hello() {
    if (BistourySpys1.hasBreakpointSet("org/example/HelloContoller.java", 17)) {BistourySpys1.putLocalVariable("this", this);if (BistourySpys1.isHit("org/example/HelloContoller.java", 17)) {BistourySpys1.dump("org/example/HelloContoller.java", 17);BistourySpys1.fillStacktrace("org/example/HelloContoller.java", 17, new Throwable());BistourySpys1.endReceive("org/example/HelloContoller.java", 17);}}String str = "hello"; //17行
                 // 18行
    return str; //19行
}

在asm中,我们可以通过MethodVisitor的visitLineNumber方法来获取行号

public void visitLineNumber(int line, Label start) {

获取指定行号前的所有局部变量

以我们的hello方法举例子

@GetMapping("/hello")
public String hello() {
    String str = "hello";    // 17行
                             // 18行
    return str;              // 19行
}

执行javap -c -l HelloContoller.class之后,我们可以看到局部变量表(LocalVariableTable),这里我们需要关注的是Start、Length和Name这三个字段。
Start: 表示局部变量在字节码中开始可见的位置,这个值是一个字节码偏移量,从方法开始计算。
Length: 表示局部变量在字节码中可见的范围长度。
Name:表示局部变量的名称。

  public java.lang.String hello();
    Code:
       0: ldc           #2                  // String hello
       2: astore_1
       3: aload_1
       4: areturn
    LineNumberTable:
      line 17: 0
      line 19: 3
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   Lorg/example/HelloContoller;
          3       2     1   str   Ljava/lang/String;

我们可以看到,我们java中的str字段偏移量为3变为可用,也是对应了代码中的第19行,有效长度是2(为什么长度是2?这个length是字节长度),下面是这个指令的占字节大小。我们可以看到 aload_1 和 areturn 的长度就是2

0: ldc #2 // 这个指令实际上占两个字节:一个字节是操作码,一个字节是常量池索引
2: astore_1 // 占一个字节
3: aload_1 // 占一个字节
4: areturn // 占一个字节

上面的东西了解就可以了,在ASM中的MethodVisitor类有一个visitLocalVariable方法,我们可以很轻松的获取到局部变量的开始和结束行号

public void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {

结尾

Bistoury是一个很优秀的debug工具,可惜社区已经不活跃了,但是它实现debug的思路还是很值得我们学习的。后续我阅读源码有什么新的理解,还会继续在这补充。😄


原文地址:https://blog.csdn.net/flzjcsg3/article/details/142885138

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!