18

字节码增强:原理与实战

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA4NzA5NzE5Ng%3D%3D&%3Bmid=2650229804&%3Bidx=2&%3Bsn=f4c9bab87cc59f3982e652cdb02875a7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文由一个拦截器逻辑的使用场景及演变历程,引入字节码增强技术。介绍字节码的本质,字节码增强的原理及JVM 启动过程中的 Agent 加载、生效流程,并对常见字节码操作工具进行了简单应用。

注:本文仅讨论 javaagent “启动时加载”。

一、技术为业务需求服务

技术是工具,是解决问题的途径。针对不同的业务需求场景,可以使用不同的技术实现。

一个简单的demo

通过一部拦截器的流浪史来引入主题:

1、基础版:新建一个Dog对象,然后调用成员方法输出到控制台

被调用方

640?wx_fmt=jpeg

调用方

640?wx_fmt=jpeg

2、加强版:需要统计方法执行的时间

常规开发:

被调用方

640?wx_fmt=jpeg

调用方

640?wx_fmt=jpeg

3、从被调用方剥离非业务逻辑

面向对象设计原则,对象应该尽可能专注自己职责范围内的事情,狗只负责叫,不负责统计自己叫了多长时间,因此统计代码应该移出Dog类。

3.1 方法提取

640?wx_fmt=jpeg

3.2 类提取–(参考SpringMVC-Interceptor)

640?wx_fmt=jpeg

640?wx_fmt=jpeg

3.3 类解耦合(使用动态代理方式-CGLib/JDK Proxy,这里Dog类没有实现接口,使用CGLib)

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

至此,非业务逻辑由从被调用方剥离出来了,同时我们也发现调用方代码却遭到改变,Main class里面需要添加动态代理类的处理逻辑。假如不允许改变调用方代码,进一步处理。

4、调用方代码剥离(切面–AspectJ)

切面

640?wx_fmt=jpeg

被调方

640?wx_fmt=jpeg

调用方

640?wx_fmt=jpeg

注意:此时直接运行Main class切面不会生效,运行前先进行编译期织入 java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT  -sourceroots src/main/java/ -d target/classes ...

至此,调用方不用显式地调用动态代理逻辑,编译期织入到class中去了(这里已经闻到了代码增强的气味了)。

切面逻辑虽然与具体的业务逻辑解耦合了,独立出切面类。但是是否生效仍然由业务代码(切面类)去控制。无论如何,都需要业务方改造,添加切面逻辑代码。

能不能更进一步,连切面都不写,也让切面逻辑生效呢?

5、javaagent 版本–隐式地,无侵入地添加切面逻辑

  • 新建独立的agent工程

  • 添加MANIFEST.MF文件以及Premain-Class,premain属性

  • 编译包含目标逻辑的源文件生成class文件

  • 注册ClassFileTransfer,在transform方法中替换byte[]

  • MANIFEST.MF指定premain函数和打开类增强开关

  • 编译输出jar包

640?wx_fmt=jpeg

MANIFEST.MF文件。

640?wx_fmt=jpeg

待替换的新class文件(忽略中文乱码)。

640?wx_fmt=jpeg

class转换器,将新的Dog.class替换旧的Dog.class。

640?wx_fmt=jpeg

maven打包输出Agent.jar。

640?wx_fmt=jpeg

上面的javaagent实现细节可以先存疑,后面会深入描述,只需要知道按照这样的步骤可以实现我们的需求。

对于业务方而言:代码完全没有变化:

被调用方

640?wx_fmt=jpeg

调用方

640?wx_fmt=jpeg

想要使切面逻辑生效,只需要在启动命令参数中加入-javaagent 选项,指向 Agent 的 jar 包。

640?wx_fmt=jpeg

这样,拦截器逻辑以一种插件的形式抽取出来了,使用的时候加载插件就可以了。

小结一下

  1. 不同需求场景下,可以不同的方式实现切面拦截逻辑;

  2. AspectJ或者SpringAop只是一种对开发者友好的快捷方式,本质上还是修改的业务代码,只不过隐藏了调用逻辑,并不能真正“无侵入“;

  3. javaagent可以无侵入的修改一个已发布的java组件的运行逻辑。

二、什么是字节码?

byte[]

1、回归原始:JDK 里面提供了很多有用的工具

640?wx_fmt=jpeg

在我们刚开始学习 Java 语言时候的 demo 运行:

编写原始 Java 文件:

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

使用 Javac 编译字节码文件:

640?wx_fmt=jpeg

Javac生产的 class 文件有什么作用呢?

Java 语言一次编译,到处运行的核心基础-JVM。

640?wx_fmt=jpeg

2、class文件到底是个什么东西?

先用文本编辑器暴力打开看看:

640?wx_fmt=jpeg

看不懂?换个方式:

640?wx_fmt=png

想看个明白?继续整:使用010editor打开。

640?wx_fmt=png

各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。

主要包含的信息:

(1)魔数

(2)版本号(参考文末例子:JRE版本错误)

(3)常量池容量

(4)常量池:

  • 文字字符串, 常量值

  • 当前类的类名, 字段名, 方法名, 各个字段和方法的描述符

  • 对当前类的字段和方法的引用信息, 当前类中对其他类的引用信息等等

(5)其他属性

常量池如何索引:

相互索引:

例如方法索引,获取classIndex和nameAndTypeIndex,通过数组下标,可以找到该方法所属的class和方法名称。

MethodRef

|-----|classIndex
|-----|-----|nameIndex --→ classNmae
|-----|nameAndTypeIndex
|-----|-----|nameIndex --→ methodName

(滑动可查看)

常量池索引和字节码指令的执行。

使用jre自带工具javap反编译class文件如下:

Main.class字节码:

640?wx_fmt=jpeg

Dog.class字节码:

640?wx_fmt=jpeg

可以看到字节码具备一定的可读性,对照着源码,可以按照执行逻辑走一遍字节码执行流程,相关指令的含义很容易从网上查询到。

640?wx_fmt=png

至此,我们通过一个简单的demo执行流程,大致了解了常量的引用以及一个简单java方法对应的字节码指令执行过程。

注:

  • stack:最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度。

  • locals:局部变量所需的存储空间,单位为Slot。

  • args_size:方法参数的个数。

  • 压栈:字节码指令执行过程中涉及到了很多压栈操作:JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。

    这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。

小结一下

  1. class文件即字节码是所有属性,方法逻辑的合集。

  2. 通过字节码二进制文件将开发者与虚拟机进行了“解耦”。

  3. 推理:修改某些字节或者替换整个二进制流可以修改运行时逻辑 。

三、如何增强字节码?

byte[] → byte[]

思路:

  1. 如前述方式直接替换为目标逻辑编译后的字节码。

  2. 手术刀式精准操作,修改/添加某些位置的byte。

  3. 高级API。

工具集:/ASM/javaassist/ByteBuddy 等等。

640?wx_fmt=jpeg

示例:

  • ASM

指令级别的字节码操作(性能强悍)。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

指令→ASM api 对应关系(这里将原始类做了简化,将字符串拼接逻辑去掉,仅仅输出时间。因为一个简单的字符串拼接过程,转换成字节码指令可能需要很多行)。

先看看 目标源码与字节码指令 的一一映射关系。

640?wx_fmt=jpeg

再看看 增强字节码逻辑与目标源码的字节码 的一一映射关系。

640?wx_fmt=png

通过对比我们可以发现,ASM的API精确到字节码指令级别,所有的临时变量存储,压栈操作,静态/实例方法的调用都有对应的API操作。

  • javassist:(dubbo)

提供字节码级别的API,类似ASM,不再赘述。

提供源码级别的API,针对本文的案例,实现如下:

640?wx_fmt=jpeg

  • ByteBuddy

基于ASM的高级API,使我们对字节码的操作提升到更抽象层次。开发者只需要知道要实现什么目标,如何使用对应的API,不用关心底层的字节码指令排列,甚至可以不用了解字节码指令。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

关于相关框架的API不详细说,有兴趣的同学可以自行查询相关资料。

小结一下

  1. 各种级别的API可以帮助开发者轻松实现字节码增强,实现特定逻辑。

  2. 不论什么奇技淫巧,都离不开Instrumentation机制。

四、增强的 byte[] 是如何影响 JVM 的?

Event --> CallBack

由前文总结,引入Instrumentation机制。

1、铺垫知识点:

(1)JVMTI 

JVM 暴露出来的一些供用户扩展的接口集合。

JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。

JVMTI 提供了许多事件(Event)的回调,包括虚拟机初始化、开始运行、结束,类的加载,方法出入,线程始末等等。

JVMTIAgent 使用JVMTI来查询或控制JVM,JVMTIAgent与目标JVM运行在同一个进程中,通过JVMTI进行通信,最大化控制能力,最小化通信成本。

典型场景下,JVMTI代理会被实现的非常紧凑,其他的进程会与JVMTI代理进行通信。比如jdwp(IDEA远程调试)。

(2)JVMTIAgent

1、表现形式:

(1)linux:        .so文件

  • windows:  .dll文件

  • c/c++    动态链接库

(2)JPLISAgent:      .jar文件

2、命令行参数

(1)-agentlib:agent-lib-name=options

(2)-agentpath:path-to-agent=options

(3)-javaagent:/data/../../Agent.jar

  • 可加载多个,通过options区分

    java -javaagent:agent1.jar=agent1

    -javaagent:agent2.jar=agent2 ...  mainclass

3、实现接口

(1)JNIEXPORT jint JNICALL

  • Agent_OnLoad(JavaVM *vm, char *options, void *reserved);

(2)JNIEXPORT jint JNICALL

  • Agent_OnAttach(JavaVM* vm, char* options, void* reserved);

(3)JNIEXPORT void JNICALL

  • Agent_OnUnload(JavaVM *vm);

(3)JPLISAgent(Java Programming Language Instrumentation Services Agent)--   Instrumentation机制。

(1)JavaSE1.5 启动时加载(本文重点)。

(2)JavaSE1.6 运行时加载。

2、简化了的核心流程逻辑

命令参数

-javaagent:/data/../../Agent.jar=optoions。

虚拟机创建-构建并初始化Agent-注册VMInit事件。

虚拟机初始化-触发VMInit事件-Agent start方法-注册回调函数并监听ClassFileLoadHook。

类加载-触发jvmtiEventClassFileLoadHook事件-替换byt[]-ClassLoader解析。

3、Java 虚拟机启动过程中 Agent 相关的流程:

640?wx_fmt=jpeg

(1)创建JVM的时候初始化Agent

  1. 启动时读取jvm命令,-agentlib -agentpath -javaagent,并构建了Agent Library链表构建了Agent Library链表。

  2. 对agent链表中的每个agent,加载所指定的动态库(如instrument.so), 并调用里面的Agent_OnLoad方法。

  3. 创建并初始化 JPLISAgent,初始化了Premain class和包里的配置文件。

  4. 注册VMInit事件。

|Agent_onLoad
|-----|createNewJPLISAgent
|-----|-----|initializeJPLISAgent
|-----|-----|-----|eventHandlerVMInit ---- > VMInit

(滑动可查看)

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

(2)虚拟机初始化

640?wx_fmt=jpeg

post_vm_initialized ---- > |eventHandlerVMInit


|-----|processJavaStart


|-----|-----|setLivePhaseEventHandlers ------ > eventHandlerClassFileLoadHook


|-----|-----|startJavaAgent


|-----|-----|-----|invokeJavaAgentMainMethod


|-----|-----|-----|-----|CallVoidMethod


|-----|-----|-----|-----|-----|sun.instrument.InstrumentationImpl.loadClassAndCallPremain


|-----|-----|-----|-----|-----|-----|premain/addTransformer

(滑动可查看)

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

实际上是调用java类

sun.instrument.InstrumentationImpl 类里的方法loadClassAndCallPremain。

640?wx_fmt=png

(3)触发ClassFileLoadHook事件

640?wx_fmt=jpeg

|parseClassFile
|-----|post_class_file_load_hook
|-----|-----|post_to_env
|-----|-----|-----|eventHandlerClassFileLoadHook(jvmtiEventClassFileLoadHook回调函数)
|-----|-----|-----|-----|transformClassFile
|-----|-----|-----|-----|-----|CallObjectMethod
|-----|-----|-----|-----|-----|-----|sun.instrument.InstrumentationImpl.transform()

(滑动可查看)

640?wx_fmt=png

640?wx_fmt=png

实际调用的java方法

Instrumentationimpl.transform。

640?wx_fmt=png

debug过程中通过ClassFileTransformer的

transform函数的执行堆栈印证。

640?wx_fmt=png

到这里,增强的byte[]如何生效并影响运行时class的过程基本可以串起来。

小结一下

  1. 虚拟机创建阶段,初始化agent,解析,加载javaagent jar,注册回调函数监听VMInt事件。

  2. 虚拟机初始化阶段,触发VMInt回调函数,注册回调函数监听ClassFileHook事件,同时执行loadClassAndCallPremain函数,注册transformer。

  3. ClassLoader加载类的时候触发tranform回调,判断是否目标类,进行对应字节码替换。

五、应用

  • 监控

  • 调试

  • 混淆

  • AOP增强

  • 日志记录

非常规应用:IDEA破解。

部分破解教程里面下载插件jar后,会要求你在IDEA的启动参数文件idea.vmoptions中添加一行,就是javaagent参数。

640?wx_fmt=png

我们可以反编译这个插件jar包看看,发现很多class因为加了混淆,反编译后无法正常识别,但是核心入口Agent.class的主要工作就是注册Transformer,可以推测这些Transformer的功能就是在IDEA启动时之前修改某些鉴定Lisence的逻辑。

640?wx_fmt=png

六、总结回顾

本文通过一个简单案例,使用不同技术方式实现一个拦截器功能需求,引入javaagent的使用。

通过介绍字节码,字节码操作工具以及openJDK关于Instrumention机制的部分源码,探索了字节码增强的实现原理。

简单介绍了相关技术的应用场景。

七、附录

  • SpringMVC-Interceptor

640?wx_fmt=png

640?wx_fmt=png

  • IDEA 远程调试

640?wx_fmt=png

640?wx_fmt=png

  • JRE版本错误

640?wx_fmt=png

640?wx_fmt=png

点一下,代码无 Bug

640?wx_fmt=gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK