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

简介: Bistoury是由去哪儿网开源的一款应用诊断工具,适用于Java应用的在线调试。通过增强字节码,Bistoury能够在不停止应用的情况下设置断点并获取执行信息。启动被调试应用后,使用`quick_start.sh`命令启动Bistoury,并通过浏览器访问`localhost:9091`进行调试。默认账号密码为admin。Bistoury通过ASM字节码增强技术确保行号一致性,并利用行增强技术收集局部变量及调用栈信息。尽管社区已不活跃,但其设计理念仍具参考价值。

一、什么是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效果

选择应用
image.png
选择需要debug的类
image.png
如果没有配置代码仓库的地址,那就会展示Fernflower反编译后的代码
image.png
打断点,只能打1个断点,打多个需要先移除之前的。前端有校验行号对不对。
image.png
每隔几秒,会自动发请求获取断点的执行结果。触发成功后自动移除断点集合中的断点
image.png

四、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中的行号是一样的,如下图所示
image.png

还有一个问题,假如我在某一行增强了这些debug逻辑,那.class的行号是不是就发生了变化?如果我要增强别的行,我是不是要先还原之前的增强逻辑,保证行号的正确🤔?
不用的,增强某一行可以不添加行号,大概的效果就是这样,可以看到,bistoury虽然增强了17行,但是这一大坨代码都算17行
image.png

相当于这样的代码

@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的思路还是很值得我们学习的。后续我阅读源码有什么新的理解,还会继续在这补充。😄

目录
相关文章
|
6月前
|
监控 前端开发 JavaScript
Qt Quick调试之道:跟踪、输出与打印信息的全面攻略
Qt Quick调试之道:跟踪、输出与打印信息的全面攻略
305 0
|
小程序 NoSQL JavaScript
【易售小程序项目】”我的“界面实现+“信息修改“界面实现+登出账号实现+图片上传组件【基于若依管理系统开发】
【易售小程序项目】”我的“界面实现+“信息修改“界面实现+登出账号实现+图片上传组件【基于若依管理系统开发】
104 0
|
6月前
|
前端开发 开发工具 iOS开发
mPaaS常见问题之真机预览与调试扫码调式 release包开启日志如何解决
mPaaS(移动平台即服务,Mobile Platform as a Service)是阿里巴巴集团提供的一套移动开发解决方案,它包含了一系列移动开发、测试、监控和运营的工具和服务。以下是mPaaS常见问题的汇总,旨在帮助开发者和企业用户解决在使用mPaaS产品过程中遇到的各种挑战
127 0
|
6月前
|
小程序 开发者
移动端修改小程序基础信息
移动端修改小程序基础信息
69 11
|
6月前
|
自然语言处理 小程序 API
10月开发者日回顾|订单中心、排查工具、验收工具、搜索BOX全面升级,observers 支持配置化监听
10月开发者日回顾|订单中心、排查工具、验收工具、搜索BOX全面升级,observers 支持配置化监听
184 11
|
小程序 前端开发
【易售小程序项目】修改“我的”界面前端实现;查看、重新编辑、下架自己发布的商品【后端基于若依管理系统开发】
【易售小程序项目】修改“我的”界面前端实现;查看、重新编辑、下架自己发布的商品【后端基于若依管理系统开发】
90 0
|
运维 Android开发
使用logcat让Android应用支持查看实时日志并输出至界面显示功能
使用logcat让Android应用支持查看实时日志并输出至界面显示功能
|
小程序 前端开发 Java
代小程序实现业务开发,99%还原公众号后台对服务类目管理的功能
关于微信开放平台上的代小程序实现业务,之前就写了相关模块的代码,包括快速注册小程序、代码上传、提交审核、发布小程序等主要功能。
122 0
代小程序实现业务开发,99%还原公众号后台对服务类目管理的功能
|
移动开发 大数据
页面埋点H5 大数据uniapp 按需要更改代码就行
页面埋点H5 大数据uniapp 按需要更改代码就行
393 0
|
Python Windows
保姆级别指导给UI应用添加菜单【实战分享】
正式的Python专栏第16篇,同学站住,别错过这个从0开始的文章!
231 0
保姆级别指导给UI应用添加菜单【实战分享】