Su的技术博客

  • 首页
  • Java
  • MySQL
  • DDD
  • 事故复盘
  • 架构方案
  • Other
  • 工具
  • 打赏
  • 关于
  1. 首页
  2. Java
  3. 正文
                           

【八戒】JAVA字节码增强解密(下)

2023-02-19 73点热度 0人点赞 0条评论

在字节码增强解密(上)的章节中,给大家介绍了字节码的基本结构、主流的字节码增强构架、以及各个架构的优缺点和应用建议。在本章节中,将从字节码的重载、JavaAgent、Dynamic Attach、以及对在线Debug的高级应用展开分析,给大家详细分解下字节码增强的实际应用。让你真正体会下字节码增强技术带来的丝般顺滑体验。


一、字节码的重载

在上一章节中,我们介绍了使用ASM、CGlib和Javassit字节码增强框架来对Class字节码文件进行增强,并且在我们的示例中,使用新的Main函数来启动程序并进行功能增强。但在实际的应用当中,这似乎并不符合我们的要求,因为我们的系统一直都在生产环境运行着,一直都在不间断的提供着服务;我们不可以为了让其生效而重启我们整个系统;那么我们应该如何做呢?是的没错,字节码的重载技术,正是让已增强的字节码文件生效。那么具体的重载技术都有哪些呢?

1、自定义ClassLoader来重新加载到JVM

如果我们使用同一个ClassLoader多次加载相同名称的Class时,并不能如愿的让动态增强后的类生效,但是我们却可以使用一个新的ClassLoader来加载增强后的类到JVM,然后做好相应的状态恢复,对旧ClassLoader进行卸载等动作。Tomcat的动态部署其实就是监听war变化,然后调用StandardContext.reload(),用新的WebContextClassLoader实例来加载新的war包,最后再初始化servlet。

2、生成新类然后直接加载到JVM(就是更改类名,包括继承)

与上面的方法类似,我们可以将增强后的类进行重命名,然后使用现有的ClassLoader来加载到JVM。通常这种方式适用于继承或者接口实现的场景,便于在程序中调用。使用的方法就是加载到JVM后,直接newInstance创建对象即可。这种方式最简单直接,易于使用。

3、替换原来的Class定义后重新载入JVM

除去换ClasssLoader和更改类名外,我们也可以在JVM加载Class时进行拦截,然后返回修改后的字节码;或者是在系统运行时动态的替换掉原来的字节码;而在实际应用中,该方法则是使用最为普遍的方法,同时也是最为复杂的一种。接下来我们也将重点介绍该方案的具体实现。需要使用到的技术包括:JVMTI、JavaAgent、Attach技术

二、JVMTI

即JVM工具接口,从JDK1.5开始支持,是一套对JVM进行操作的工具接口,可以通过这些工具接口注册JVM各种事件,在JVM事件触发时被同时触发执行(钩子),这里的事件包括:类文件加载、异常产生与捕获、线程启动和结束、成员变量修改、GC开始和结束、方法调用/进入/退出、VM启动与退出等等。

有了它才真正具备给JVM“开挂”的基础。有了它才能真正发挥字节码增强技术的最大价值。

三、Java Agent

Agent其实就是JVMTI的一种实现,可以在启动JVM时使用-javaagent入参指定一个Agent的jar包。会在启动JVM的时候同时运行你的Agent应用。

命令如:java -javaagent:myAgent.jar=xxxx -jar myProgram.jar

Agent根据启动的时机可分为2种:

第一种是跟随JVM启动而启动(上面命令行则是属于这种方式)

第二种是在JVM运行时通过Attach技术载入。

看到这里,你或许已然明白,正是因为有Agent的存在,以及它的两种启动方式,可以让我们根据实际的情况灵活的选择字节码增强方式:前者可以拦截Class的加载,后者可以动态的替换原来的Class。

如何开发一个Agent应用???

Agent主要由2部分组成:MANIFEST.MF文件以及Agent的入口类。

MANIFEST文件:

是在jar包内的META-INF/MANIFEST.MF文件,可以在工程内手动创建,也可以在Maven的build插件里进行配置。

文件最终配置的内容如下:

图片

Agent的入口类:

注:该入口类中必须至少实现以下2个main方法的其中之一,或者2者全部实现,根据实际启动时机来定,如果2者都实现了则2种启动方式同时支持。

图片

上面实现的2个方法中,会接收一个Instrumentation类对象,而该类的最大作用则是类定义的动态改变和操作,需要重点关注的方法有:

图片

四、Dynamic Attach

Attach:简单点说Attach就是jdk提供的一种jvm进程间通信的能力,能让一个进程传命令给另外一个进程,并让它执行一些操作,将结果返回。

根据操作的方式Attach又分2种方式:

方式一、通过sun.tools进行Attach(以arthas诊断工具为例)

图片

方式二:直接和JVM进行通信式的Attach(由于太原生了,这种方式更直接但也更复杂,因为它够原生,简单介绍下原理即可)

JVM提供了一种特殊机制,允许使用Socket的方式与其他进程进行通信。具体的方式如下:

1、首先建立/tmp/.attach_pid的空文件

2、然后向目标JVM发送一个SIGQUIT信号,JVM会主动异步创建一个/tmp/.java_pid的Socket文件

3、当前主程序当然是等待.java_pid文件的创建,创建成功后则可连接这个地址,建立与JVM的通信。连接的方式则是socket方式。

4、连接建立成功后就可以向JVM发起执行命令了。

五、字节码增强的高级应用(在线Debug系统)

截止到目前,您已经基本掌握了字节码增强的要点,接下来我们也从实际需求出发,一起分析下字节码增强技术的强大功能实现 - 在线Debug系统。

你应该经常在IDE上进行断点调试(除非你不是一个合格的程序猿),这种非常直观的、高效的排错体验,让我产生了将Debug功能搬到线上,对生产环境系统进行在线Debug的想法,为快速排查线上问题提供有效手段。经过一个月的努力,系统终于发布,并且支持内网穿透、跨机房、跨环境、安装即用的在线诊断平台。接下来就和大家一起分析下具体的实现细节。

具备的能力/功能模块:

1、跨机房、跨环境的能力

由于公司的各业务系统均为多机房部署,同时线下还有测试环境、预发环境,为了尽量多的覆盖适用范围,我们决定支持跨机房、跨环境的能力。

2、“内网穿透”的能力

由于Debug系统部署于生产环境,如要对线下环境/预发环境(内网)的机器进行Debug,则必须具有“内网穿透”的能力,可以让诊断命令从线上机器发往公司内网环境各机器。

3、在线Debug的核心模块 - 我们称之为“诊断器”

要能够对目标机器的JVM进行Debug,就必须要有一段程序运行于目标机器,并且Aattach到目标JVM,对其类进行增强,接收来自前台的诊断指令,实现Debug能力。

4、安装即用的能力

所有业务系统都在正常的跑着,不间断的提供着服务,有的甚至已经好几年没有重启过服务。如果需要进行在线Debug,不能对业务系统有侵入性,操作简便,因此需要安装即用、超时卸载。方便使用的同时,最大化的减少目标机器的资源占用。

5、数据中心

由于诊断的目标代码被执行的时机是不确定的,所以当在目标机器安装“诊断器”,并向其发起Debug指令后,Debug的诊断结果也并非是实时的,整个系统大部分为异步交互,然后在产生诊断结果后上报到该数据中心,后输出到前端页面呈现给用户。

图片

接下来分析下我们的核心所在-“诊断器”的实现:(注:暂不介绍其他模块,并非本篇文章重点)

毫无疑问,字节码增强技术将是本系统的核心所在。需要动态的对目标类进行增强:添加断点钩子。当程序执行到这些钩子时,实时的记录下当前所在断点行的全局变量、局部变量、静态变量、以及调用堆栈信息。然后上报给数据中心,最终呈现给用户。

1、源码反编

为了提供友好的交互界面,需要反编对应类的源码供查看,然后在该类的行号上点击并添加断点。

那如何反编源码呢?JDK有自带的javap,但都并不能输出代码块内容啊~~~,别着急,这是有现成的三方实现的,JAVA反编工具CRF,几行代码快速搞定。

2、类的行号解析

要在某类上添加断点,当然需要知道哪一行执行什么代码,以及该行号是否有可执行代码。我们不允许你添加一个没有可执行代码的断点的。

如果你看过字节码增强解密的上半部分,你应该就知道字节码是按约定的结构进行的存储,按指定的方式进行解析就能得知某一行对应的指令(代码块),你需要做的则是将它们暂时存起来,在添加断点时做为判断的依据。实现的方式则是(以ASM框架为例):继承自ClassVisitor、MethodVisitor可分别对类和方法进行操作,重写ClassVisitor内部的visitMethod方法,使用MethodVisitor类可对行号进行解析,并记录下对应行号和代码块。参见代码如下:

 
@Override
public MethodVisitor visitMethod(final int access,
                                 final String methodName,
                                 final String desc,
                                 final String signature,
                                 final String[] exceptions) {
    final MethodVisitor superMV = super.visitMethod(access, methodName, desc, signature, exceptions);
 
    final String methodUniqueName = methodName + desc;
    return new MethodVisitor(ASM_VERSION, superMV) {
        private final Map<String, Integer> labelLineMapping = new HashMap<>();
 
        @Override
        public void visitLineNumber(final int line, final Label start) {
            labelLineMapping.put(start.toString(), line);
        }
 
        @Override
        public void visitLocalVariable(final String name,
                                       final String desc,
                                       final String signature,
                                       final Label start,
                                       final Label end,
                                       final int index) {
            super.visitLocalVariable(name, desc, signature, start, end, index);
            classMetadata.addVariable(methodUniqueName,
                    new LocalVariable(name, desc, labelLine(start), labelLine(end), index));
        }
 
        private int labelLine(final Label label) {
            final String labelId = label.toString();
            if (labelLineMapping.containsKey(labelId)) {
                return labelLineMapping.get(label.toString());
            }
            return Integer.MAX_VALUE;
        }
    };
}

3、添加断点

到此准备工作已基本做好,很简单是不是?接下则是添加断点了;添加断点其实是对类进行增强,在指定行埋点一个钩子,当发现执行到当前代码行时获取当前信息并记录。

具体的实现方法:新增XXClassVisitor继承自AdviceAdpter,通过重写其中的visitLineNumber方法添加断点钩子,钩子的代码当然则是获取并记录当前全局变量、类变量、局部变量、调用堆栈的信息。参见代码如下:

图片

4、数据上报

定期扫描各断点的执行结果,并上报到数据中心。到此核心功能已基本完成。


结语:字节码增强技术作为众多三方框架/开源框架的底层核心技术,足见其功能的强大和应用的普遍。在日常的业务系统研发中,或许你并不直接使用到字节码增强,但实际它却在系统的某个角落默默的担负着核心的作用。你不应该选择忽视它,你需要将它擅长的事情全都交给它。

标签: 八戒 字节码 动态代理 JVM 编译 Java
最后更新:2023-02-19

coder

分享干货文章,学习先进经验。

打赏 点赞
< 上一篇
下一篇 >
最新 热点 推荐
最新 热点 推荐
殷浩详解DDD 第四讲:领域层设计规范 既生@Resource,何生@Autowired? Go整洁架构实践 接口优化的常见方案实战总结 QQ音乐高可用架构体系 构建一个布隆过滤器 —— Building a Bloom filter
殷浩详解DDD 第四讲:领域层设计规范Redis为什么这么快?构建一个布隆过滤器 —— Building a Bloom filterQQ音乐高可用架构体系接口优化的常见方案实战总结Go整洁架构实践
线上问题处理案例1:出乎意料的数据库连接池 Eureka 客户端配置注册地址为什么要加eureka做后缀? Arthas实战-线上热更新代码只需3步 一次 Redis 事务使用不当引发的生产事故 全链路压测之影子库及ShardingSphere实现影子库源码剖析 京东购物车如何提升30%性能

@Autowired (1) @Resource (1) API网关 (1) ddd (6) DP (1) ElasticSearch (1) eureka (7) go (1) HTTP (1) IDEA (1) iOS (1) Java (8) JSR (1) QQ音乐 (1) repository (1) Spring (1) SQL优化 (1) 代理 (1) 依赖注入 (1) 同城双活 (1) 垃圾回收 (1) 定时任务 (1) 容灾 (1) 布隆过滤器 (1) 异地双活 (1) 接口优化 (1) 故障转移 (1) 数据库 (2) 整洁架构 (1) 文件网关 (1) 方案 (2) 服务续约 (1) 注册中心 (7) 流水账 (1) 流量 (1) 第五 (1) 线上案例 (1) 线上问题 (2) 缓存 (1) 缓存击穿 (1) 编译 (3) 网络 (3) 聊聊 (1) 订单 (1) 设计规范 (1) 详解 (1) 连接池 (1) 限流 (1) 领域驱动设计 (4) 高可用 (1)

COPYRIGHT © 2014-2023 verysu.com . ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

粤ICP备15033072号-2