arthas再见swapper你好

10 min read,created at 2024-07-14
arthasasm字节码javaswapper

arthas

arthas是阿里开源的非常强大的java进程分析工具,它有着非常丰富的功能,从查看堆内存信息、线程信息、增强字节码等等。这个工具其实应该叫一个工具库,或者工具箱,tool kit更贴切。因为arthas其实是整合了网上很多开源的工具,给他们缝合到了一个系统中。

例如:profiler火焰图是直接用的async-profilerognl直接就是apache commons-ognlmc内存编译器使用的是SkaETL/compilerjad反编译器使用的是cfr等等。这么多功能集合在一起,确实也不容易,况且arthas中还有很多功能是基于阿里的bytekit实现的字节码增强实现的,这几个功能反倒是我使用最多的功能:

  • watch监控方法,被调用时打印出入参。最好用的debug工具。
  • trace监控方法中所有子方法的耗时。最好用的性能分析工具。

arthas缺点

现阶段我感觉arthas已经非常好了,但是在使用中还有觉得有些操作太麻烦:

  • watch等功能都是阻塞窗口的,多个函数的watch需要新开窗口。
  • 没有一个UI页面,都是shell运行,长时间不用就忘了指令语法了。
  • 想要热替换整个类,或整个方法体,需要非常复杂的,编译,替换等流程,且经常无缘无故失败。
  • ognl主动触发的表达式,在spring项目不符合预期,因为用了默认类加载器;需要用vmtool配合一堆复杂的语法,spring项目非常受限。

swapper

JVMByteSwapTool或者简称swapper,是我自己写的一个工具,实现了个人比较常用的功能,和arthas有一部分功能重叠,也有一些是arthas不具有的功能,总体而言更加容易上手。

下载demo.jarswapper.jar,这里使用的是v0.0.5版本。

$ wget https://github.com/sunwu51/jbs-demo/releases/download/1.0.0/demo.jar

$ wget https://github.com/sunwu51/JVMByteSwapTool/releases/download/v0.0.5/swapper.jar

接下来启动demo服务,这是个spring boot的简单项目,源码可以参考sunwu51/jbs-demo

然后启动swapper,选择attach刚才的demo进程。

此时访问8000(如果已经占用会自动切换8001)端口可以得到这样一个页面:

img

默认会连接同域名的18000端口的Websocket服务,因为这里使用的gitpod服务,端口在域名中,这里修改域名重新点击连接,如果你是本地应该直接右侧状态是绿色正常链接成功了。

img

Decompile

先介绍这个功能,可以方便理解其他功能。输入类名进行反编译得到源码。

img

绿色按钮clear log可以清理日志区域,effected classes按钮可以展示当前被影响的类列表,reset按钮则是把所有的影响都删除。

Watch

刚才的/base64对应com.example.demo.DemoApplication#base64方法。输入类名#方法名即可对方法进行增强,监听并打印方法的出入参和耗时,如下:

img

此时effected列表有这个被增强的类。

img

可以通过反编译查看增强后的代码。

img

默认情况下会监听100次,100次之后,自动注销监听功能。可以修改系统属性来修改这个次数,后面Exec介绍。

这里为了避免干扰其他功能测试,先reset

OuterWatch

监听方法中子方法的调用,子方法支持*匹配任意类。

img

ChangeBody

修改某个方法的body,要和原方法有一样的返回值,在方法中$1 $2 ... 分别代表第1、第二..个方法的入参。

这里提供了javassistasm两种底层实现,后者是对前者的逆向实现,两者都支持$1 $2 ..这种参数的表达。但是javassist可能在java17以上有兼容性问题,所以提供了asm作为备选方案,此外asm引擎是使用janino作为编译器支持较多的语法,而javassist使用内置编译器仅支持java4之前的语法,并且不支持int等基础类型的自动装箱。

img

ChangeResult

修改某个方法中调用子方法的返回内容,相比ChangeBody来说,ChangeResult影响面更小,用法也更灵活。与OuterWatch类似,这里的子方法也支持*匹配所有类,但是当InnerMethod匹配到多个有不同签名的方法时,就会check报错提醒。

这里支持$_作为当前子方法的返回值,直接$_=xx;即可跳过原函数执行,返回一个指定值。

img

同样也提供了javassist和ASM两种底层引擎,他们都支持$_是子函数返回值,$1 $2..是子函数入参,还支持$proceed是调用原方法,这里两个引擎稍有区别。

  • javassist引擎,$proceed($$)是调用原方法。
  • ASM引擎,$proceed()是调用原方法。

encodeToString为例,我们调用原方法编码之后,在追后加一个0.0,如下:

img

再举个例子encodeToString的入参是byte[],字符1的ASCII是49,入参123对应的byte[]就是49,50,51。我们把第三个位置改成固定的52,然后运行base64,此时我们入参传入12x,第三位任意传什么字符,都会被换成52也就是4。

img

为什么两个引擎共存 当前ASM的功能已经完成可以替代javassist为什么还保留了两个引擎选项,主要是没有做充分的测试,如果有一些边界情况ASM不好使的话,可以切回javassist,如果一段时间使用后,没有发现ASM有问题,就会只保留ASM引擎了。

Exec

主动触发一段函数执行,替代arthasognl功能,但是后者只能写ognl表达式,swapper这里可以用java代码编辑一个方法,并且内置了丰富的辅助功能:

  • 模板代码中的ctx变量,是获取当前spring的上下文,ctx.getBean(name|class)可以获取bean,这样就可以触发bean中的方法了。
  • Global类内置了一些辅助方法:
    • info(obj) error(obj, e)打印并传递到页面日志
    • ognl(str)执行ognl表达式,与arthas类似,但是这是在spring类加载器下执行,可以访问项目中的类
    • beanTarget(bean)获取spring增强的代理对象的原始对象
    • readFile(str)读取文本文件,返回行列表List<String>

demo.jar中有/user/{id}路径是读取h2中的mock的5条数据,假如我们把id=1的数据的name改为faker,如下直接通过ctx获取bean然后修改数据库。

img

maxHit 上面提到的watch只能监听100次上限,也包括outerwatchtrace,这个限制其实是存到了System.getProperty("maxHit")中,可以通过两种方法修改这个限制。一种就是直接在Exec中执行System.setProperty("maxHit", "200"),另一种方法是在启动swapper.jar的时候,传递-Dw_maxHit=200swapper.jarw_开头的属性,都会被去掉w_,剩下的部分作为属性设置到目标jvm进程中。

ReplaceClass

上传class文件替换整个类,本地修改代码编译后,找到生成的class文件上传,并指定类名,即可完成替换。

但是注意,匿名类等,可能会生成一个XXX$1.class,如果修改的是这部分内容,尽量就不要使用这个功能了。

img