博文精译|使用代理(Agent)的Java Bytecode Instrumentation:在运行时侵入Java应用程序(1)

介绍

这篇博客中,我想描述一种技术,它可以用来灵活地更改由Java应用服务器执行的应用程序逻辑—或者更准确地说,在其服务器节点的Java虚拟机(JVM)中执行的应用程序逻辑。JVM执行预先编译和部署的与平台无关的字节码(这是Java源代码编译的结果),下面描述的技术基于字节码操作的概念。使用这种技术,几乎可以通过在已经部署的Java应用程序的字节码级别(JVM在运行时对其进行解释)上操作而对其进行任何更改,而无需修改应用程序的源代码(因为后者意味着需要重新编译、重新组装和重新部署应用程序)。

本博客还将说明为什么从原始资源(如位于应用程序服务器上由Java类加载器加载的类文件)获得的反编译代码的静态分析有时会产生误导以及为什么Java应用程序静态逆向工程结果可能不同于其观察到的运行时行为。

这种技术可能有用,那为什么不简单地对Java应用程序的源代码进行必要的更改并将其部署到应用程序服务器呢?以下是一些例子:

  • 我们没有相同的原始开发项目——例如,如果原始项目不可获得,并且反编译无法再现可成功构建和组装的完整项目结构和制品;

  • 我们需要对已运行的应用程序生成临时的补丁/逻辑修改,以便在开发和组装完整的补丁之前进行快速测试;

  • 我们需要收集关于已执行类(所有或仅选择的类)的特定运行时信息;

  • 或者我们只是想侵入已经部署的应用程序并侵入其逻辑。

该博客主要包含说明字节码instrumentation和操作的示例。关于应用程序demo,有几点需要提前说明:

  • 为了避免不相关的复杂性,示例基于一个独立的Java应用程序。由于所描述的功能是JVM特性的一部分,并不特定于应用服务器实现,因此可以在实际场景中与各种应用服务器一起使用它(SAP应用服务器就是其中之一);

  • 所有的开发都被简化了,因此代码行数被减少到合理的最小值,让我们可以关注核心主题,虽然这会导致大量使用硬编码值和简单的类模型设计。在实际开发中,大部分硬编码的值应作为可配置参数;

  • 在一个独立的程序和补充开发的类,当调用相应的对象及查看其状态时,控制台的输出被大量使用,以使信息充分方便地展示。在实际开发中,这种详细输出应该禁用,或使用具有相应日志级别/重要性的应用服务器日志框架实现。

出于可读性和清晰度,在控制台输出相应的日志条目插入以下值:

  • 程序主类调用的输出前加“[Application - main]”;

  • 来自负责在控制台显示文本的类的调用,以及来自程序main类的调用的输出前面有“[Application - text display]”;

  • 来自后续instrumentation示例的调用的输出前面有“[instrumentation]”;

  • 来自后续代理(agent)示例的调用的输出前面有“[agent]”。

为了使演示中使用的功能隔离更加明显,开发的类位于以下包中:

  • 我们将要侵入的Java应用程序和工具,位于vadim.demo.jvm.app包中;
  • Java agent位于包vadim.demo.jvm.agent中,该agent通过agent加载来演示instrumentation;
  • Java agent loader应用程序位于包vadim.demo.jvm.agent.loader中,用于演示从外部应用程序连接运行中的JVM。

相应地,instrumented的Java应用程序、Java代理和Java代理加载程序位于三个不同的Java项目中,项目结构如下:

image.png

我将从一个基本的应用程序开始,逐步增强已实现的特性,以说明讨论的主题和技术的各个实际方面,所以项目和内容将在这个博客中将逐步变化。

应用程序Demo

让我们使用以下独立的小Java程序作为未来扩充和操作的起点。该程序由两个类组成:主类DemoApplication和从主类调用的类Text。

类DemoApplication实现方法main(),是被调用Java程序的入口点:

package vadim.demo.jvm.app;
public class DemoApplication {
  public static void main(String[] args) {
  System.out.println("[Application - Main] Start application");
  String value = "Demonstration of Java bytecode manipulation capabilities";
  Text text = new Text();
  System.out.println("[Application - Main] Value passed to text display: " + value);
  text.display(value);
  System.out.println("[Application - Main] Complete application");
  }
}

从主类调用Text类,等待一秒钟后将给定的文本发送到控制台输出:

package vadim.demo.jvm.app;
public class Text {
  public void display(String text) {
  long sleepTime = 1000;
  long sleepStartTime;
  long sleepEndTime;
  System.out.println("[Application - Text display] Text display is going to sleep for " + sleepTime + " ms");
  sleepStartTime = System.nanoTime();
  try {
  Thread.sleep(sleepTime);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
  sleepEndTime = System.nanoTime();
  System.out.println("[Application - Text display] Text display wakes up");
  System.out.println("[Application - Text display] Text display sleep time: "
  + ((sleepEndTime - sleepStartTime) / 1000000) + " ms");
  System.out.println("[Application - Text display] Output: " + text);
  }
}

程序执行后控制台打印如下输出:

[Application - Main] Start application
[Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities
[Application - Text display] Text display is going to sleep for 1000 ms
[Application - Text display] Text display wakes up
[Application - Text display] Text display sleep time: 1000 ms
[Application - Text display] Output: Demonstration of Java bytecode manipulation capabilities
[Application - Main] Complete application

现在让我们讲讲什么是bytecode instrumentation。

字节码instrumentation和操作

JDK从Java 5开始为开发人员提供所谓的字节码instrumentation功能。这种技术的目标是修改加载到JVM并由其执行的字节码——例如,扩展附加指令或对原始字节码的其他更改。需要注意的是,字节码instrumentation不会对字节码的原始资源(类文件)造成任何更改。当类加载器试图访问并将相应的被查找的类的字节码装入JVM时,它会动态地操纵字节码,扩展或替换从原始资源中获得的字节码,并带有instrumented版本。用于instrumentation的主接口是java.lang.instrument.Instrumentation。Instrumentation接口提供了添加自定义转换器实现类的功能,该实现类将在类字节码加载到JVM时被触发,并且可以用动态提交的自定义字节码扩展或替换类的原始字节码。请注意,如果类已经加载,则对其字节码进行操作是无关紧要的。在技术上instrument所需的类仍然是可行,但这意味着必须为该类开发增加版的类加载器逻辑,并使用可回调的类进行扩展以实现重加载或卸载——这可能不是一项轻松任务,因为标准类加载器不提供类卸载功能。

操作字节码不同于编辑原始Java源代码,因为我们需要对编译后的JVM指令进行操作,而不是使用原始Java语句。对字节码的低干扰要求对包含字节码的class文件的结构有很好的了解。幸运的是,有几个库可以简化对字节码操作—下面是其中最常用的几个库,按生成的字节码的抽象级别分类:

字节码抽象层次 描述 示例
库需要直接在字节码级别进行操作。它们通常提供最丰富的功能,但与其他字节码操作工具相比,它们的使用也最复杂。 ASM (ASM – Home Page) BCEL (https://commons.apache.org/proper/commons-bcel/)
库根据字节码提供了一定程度的抽象,并简化了对字节码的修改。例如,不必修改字节码,可以使用类似java的语法进行更改,然后将其编译为字节码,并由使用的库将其修改为原始字节码。通常,它们缺乏对被修改代码验证的功能——这意味着,错误可能在修改准备过程中被忽略,然后在运行时被观察到。 Javassist (Javassist by jboss-javassist)
库使用高级别的指令进行操作,通常配有用于语法验证的工具集。不幸的是,对被修改的字节码进行高级别的抽象通常会丢失一些特性,这些特性通过对字节码的直接修改才会获得。 AspectJ (The AspectJ Project)

在本博客的后面的示例中,我将在修改底层字节码的必要性和抽象之间作一个折衷,使用Javassist库

让我们增强演示程序的基本逻辑并对其instrument。所提供的示例合并了几个不同的instrumentation,并说明了我们如何实现以下修改:

  • 在被instrumented的类的给定方法执行之前插入额外的代码;

  • 在被instrumented的类的给定方法执行之后插入额外的代码;

  • 在被instrumented的类的给定方法中间注入额外的代码;

  • 修改被instrumented的类的给定方法的现有代码。

几个关键的地方需要考虑

  • Javassist提供了访问编译时类定义(它是字节码的呈现版本)的功能;

  • 然后就可以迭代类方法,通过名称和描述符访问方法。请注意方法描述符的表示法——它对应的是兼容字节码的表示法,而不是Java语言规范中定义的表示法;

  • 对于给定的方法,可以在方法之前或之后插入任意代码,或者在给定的代码行插入代码。请注意语法-注入的代码行是经过一些修改的类似java的字符串(如适当转义某些特殊字符、可能使用占位符等)。在调用System.output.println()之前,我们将另一个值的赋值注入到使用的变量中,这样控制台输出的值就与从程序主类传递的值不同;

  • 也可以通过引入所谓的表达式编辑器实现类改变已经存在的字节码,它可以拦截和取代构造函数和方法调用,访问类字段,异常处理,等。在这个例子中,我们废止sleep()调用,所以这个程序不需要等待就输出文本。

有关库功能的完整文档及其使用示例,请参阅官方网站上的API参考资料。

类DemoApplication得到了相应的增强——字节码instrumentation是在方法enableInstrumentation()中实现的

package vadim.demo.jvm.app;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
public class DemoApplication {
  public static void main(String[] args) {
  System.out.println("[Application - Main] Start application");
  String value = "Demonstration of Java bytecode manipulation capabilities";
  enableInstrumentation();
  Text text = new Text();
  System.out.println("[Application - Main] Value passed to text display: " + value);
  text.display(value);
  System.out.println("[Application - Main] Complete application");
  }
  private static void enableInstrumentation() {
  String instrumentedClassName = "vadim.demo.jvm.app.Text";
  String instrumentedMethodName = "display";
  String instrumentedMethodDescriptor = "(Ljava/lang/String;)V";
  try {
  ClassPool cPool = ClassPool.getDefault();
  CtClass ctClass = cPool.get(instrumentedClassName);
  CtMethod ctClassMethod = ctClass.getMethod(instrumentedMethodName, instrumentedMethodDescriptor);
  ctClassMethod.insertBefore("System.out.println(\"[Instrumentation] Entering instrumented method\");");
  ctClassMethod.insertAfter("System.out.println(\"[Instrumentation] Exiting instrumented method\");");
  ctClassMethod.insertAt(24, true, "text = \"Original text was replaced by instrumentation from agent\";");
  ExprEditor instrumentationExpressionEditor = new DemoExpressionEditor();
  ctClassMethod.instrument(instrumentationExpressionEditor);
  ctClass.toClass();
  } catch (NotFoundException e) {
  e.printStackTrace();
  } catch (CannotCompileException e) {
  e.printStackTrace();
  }
  }
}

(SCN中的语法高亮显示功能有时会废止空行,所以请记住,上面代码中提到的代码24行对应于System.out.println()调用之前的空行,System.out.println()输出给定的文本到控制台)

此外,类DemoExpressionEditor继承了Javassist的类Javassist.expr.ExprEditor。DemoExpressionEditor实现了instrument被调用方法的逻辑

package vadim.demo.jvm.app;
import javassist.CannotCompileException;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
public class DemoExpressionEditor extends ExprEditor {
  @Override
  public void edit(MethodCall method) throws CannotCompileException {
  if (method.getMethodName().contains("sleep")) {
  System.out.println("[Instrumentation] Suppressing sleep for " + method.getClassName() + "."
  + method.getMethodName() + " called from " + method.getEnclosingClass().getName());
  method.replace("{}");
  }
  }
}

我们再次执行DemoApplication,比较和原来的版本有什么不同。

[Application - Main] Start application
[Instrumentation] Suppressing sleep for java.lang.Thread.sleep called from vadim.demo.jvm.app.Text
[Application - Main] Value passed to text display: Demonstration of Java bytecode manipulation capabilities
[Instrumentation] Entering instrumented method
[Application - Text display] Text display is going to sleep for 1000 ms
[Application - Text display] Text display wakes up
[Application - Text display] Text display sleep time: 0 ms
[Application - Text display] Output: Original text was replaced by instrumentation from agent
[Instrumentation] Exiting instrumented method
[Application - Main] Complete application

从这个输出,可以看到什么时候instrumentation实现逻辑被调用,以及它如何影响执行程序——特别是负责显示文本的类:自定义代码在被instrumented方法之前和之后执行,线程没有运行进入睡眠状态,控制台输出不是最初演示程序设计的。这一切演示了我们如何不对该类源代码进行更改,在运行时引入对某个应用程序类逻辑的较大的更改。通常我们并不局限于让调用instrumented类的应用程序触发instrumentation逻辑——它可以是运行在相同JVM中的任何其他应用程序。这里让这个程序触发instrumentation逻辑,目的是为了简化,避免过分复杂。

Java Agent 与 Attach API

到目前为止,我们已经熟悉了字节码instrumentation的一些基本原理,但是上面提供的示例仍然不够灵活——我们需要将额外的逻辑嵌入到应用程序中,或者需要部署其他应用程序instrument所需的类字节码。让我们更进一步,探索如何将instrumenting应用程序与instrumented应用程序(上面使用的Java应用程序)解耦。这种概念在JVM中已经存在了一段时间,称为Java代理。Java agent是一种以特定方式捆绑的应用程序,通常作为一个独立的JAR文件(它可能还需要额外的依赖项)交付,它包含instrumentation逻辑的实现,并且可以为了instrumentation而附加到Java应用程序。

后续翻译敬请期待......

java达人

ID:drjava

(长按或扫码识别)

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容