-10 +

byteman

场景描述

生产服务器上出现了一个 bug,通过现象无法定位这个 bug 产生的原因,这个时候要求修复这个 bug,你的一般做法是?

很多时候在 online 的应用出现问题时,我们需要知道更多的程序的运行细节,但又不可能在开发的时候就把程序中所有的运行细节都打印到日志上, 通常这个时候能采取的就是修改代码,重新部署,然后再观察,但这种方法对于 online 应用来说不是很好,另外一方面如果碰到不好改的代码,例如引用的其他的外部的包什么的,就很麻烦了。使用远程 debug 会使得线程挂起,对于 online 环境是不可接受的。虽然使用 btrace 字节码工具可以避免上面两种方式的缺点,但是 btrace 的限制太多了,例如

我使用了一段时间 btrace,一直没有找到得到局部变量的方法(若是有网友知道,可以教我一下),这是我最不能接受的,因为这个场景使用很普遍。若是你也有这个需求和困扰,那么 byteman 是你理想的选择。

Byteman 简介

byteman是jboss下的一个项目,是一个非常方便的java分析工具,能够拦截字节码执行,执行代码和修改变量,是一个诊断问题的利器。在linux下使用起来非常方便,不用对目标应用做任何修改,可以动态打开目标应用的监听端口,当然仅限于openjdk,hotspot 和 jrockit,ibm jdk 不支持。

byteman 使用

byteman 安装

官方网站 下载最新的 byteman,解压。设置 BYTEMAN_HOME,并且将 BYTEMAN_HOME/bin 加入到 path 环境变量中,方便我们使用 byteman。

byteman 使用

现在我们通过一个列子来详细讲解 byteman 的使用,我以平时使用的最多 btrace 又不能做到的读取局部变量的值作为列子。下面的程序是我们将要使用。

package edu.hushi.byteman;

/**
 *
 * @author -10
 *
 */
class AppMainTest {

	public static void main(String[] args) {

		new Test().run();

	}

}
package edu.hushi.byteman;

import java.io.DataInputStream;
import java.io.IOException;

/**
 *
 * @author -10
 *
 */
public class Test {

		public void run() {
			try {

				DataInputStream in = new DataInputStream(System.in);

				String next = in.readLine();

				while (next != null && !next.contains("end")) {

					task(in, next);
					next = in.readLine();

				}

			} catch (Exception e) {

			}
		}

		private String task(DataInputStream in, String next) throws IOException {
			final String arg = next;

			Thread thread = new Thread(arg) {

				public void run() {

					System.out.println(arg);

				}

			};

			thread.start();

			try {

				thread.join();

			} catch (Exception e) {

			}

			return next;
		}
}

程序比较简单,就是不断的标准输入流中接受数据,然后打印在标准输出流中。输入:

this is byteman test !
this is byteman test !
this is byteman test !

现在我们假设程序中出现 bug,现在不知道什么原因导致标准输入流中的打印的不对,我们要看看是不是传入 task 后值发生了改变,即 Test 类中的 33 行 arg 的值。

attach agent

加载 byteman 到运行的 JVM 中

bminstall -b -Dorg.jboss.byteman.transform.all -Dorg.jboss.byteman.verbose edu.hushi.byteman.AppMainTest

在 liunx 中可以 attach 到进程中,具体使用可以 bminstall –help 查看。在 windows 上实验 attach pid 时,我不知道什么原因,一直不能成功(若果有朋友知道,可以告诉我!)。

编写脚本

编写 byteman 的 btm 脚本,LocationVar.btm 如下:

RULE trace line location var

CLASS edu.hushi.byteman.Test

METHOD task(DataInputStream , String )

AFTER WRITE $arg

IF true
	DO traceln("*** next value is : " + $arg)
ENDRULE

这里没有在 run 方法上注入,是因为当时实验时,确实是在 run 方法上的,但是就是不能够打印 next 的值,我猜测是 run 方法是死循环,JVM 无法修改正在运行中方法的字节码,或者是修改了要等到下次运行才能生效。

安装脚本

cd 到 btm 脚本的目录,加载 byteman 脚本,我们现在讲 LocationVar.btm 加载进 JVM:

bmsubmit -l LocationVar.btm

执行完上面两个命令后,控制台上会有如下显示:

Setting org.jboss.byteman.transform.all=
Setting org.jboss.byteman.verbose=
TransformListener() : accepting requests on localhost:9091
TransformListener() : handling connection on port 9091
retransforming edu.hushi.byteman.Test
org.jboss.byteman.agent.Transformer : possible trigger for rule trace line location var in class edu.hushi.byteman.Test
RuleTriggerMethodAdapter.injectTriggerPoint : inserting trigger into edu.hushi.byteman.Test.task(java.io.DataInputStream,java.lang.String) java.lang.String for rule trace line location var
org.jboss.byteman.agent.Transformer : inserted trigger for trace line location var in class edu.hushi.byteman.Test

现在我们再标准输入中再次输入:

this is byteman test !!!!!

这是控制台的输出为:

Rule.execute called for trace line location var_0
HelperManager.install for helper class org.jboss.byteman.rule.helper.Helper
calling activated() for helper class org.jboss.byteman.rule.helper.Helper
Default helper activated
calling installed(trace line location var) for helper classorg.jboss.byteman.rule.helper.Helper
Installed rule using default helper : trace line location var
trace line location var execute
*** next value is : this is byteman test !!!!!
this is byteman test !!!!!

请注意第 8 行的显示,表明 btm 脚本生效了。

卸载脚本

我们调试完成后,可以将脚本卸载掉:

bmsubmit -u LocationVar.btm

byteman 原理介绍

上图表明了 byteman 就是 javaagent 和 DSL。

byteman DSL

DSL 是 domain specific language 缩写。

byteman 脚本结构

######################################
# Example Rule Set
#
# a single rule definition
RULE example rule
# comment line in rule body
. . .
ENDRULE

规则事件

# rule skeleton
RULE (rule name)
CLASS (class name)
METHOD (method name)
BIND (bindings)
IF (condition)
DO (actions)
ENDRULE

注入点如下:

AT ENTRY
AT EXIT
AT LINE number
AT READ [type .] field [count | ALL ]
AT READ $var-or-idx [count | ALL ]
AFTER READ [ type .] field [count | ALL ]
AFTER READ $var-or-idx [count | ALL ]
AT WRITE [ type .] field [count | ALL ]
AT WRITE $var-or-idx [count | ALL ]
AFTER WRITE [ type .] field [count | ALL ]
AFTER WRITE $var-or-idx [count | ALL ]
AT INVOKE [ type .] method [ ( argtypes ) ] [count | ALL ]
AFTER INVOKE [ type .] method [ ( argtypes ) ][count | ALL ]
AT SYNCHRONIZE [count | ALL ]
AFTER SYNCHRONIZE [count | ALL ]
AT THROW [count | ALL ]

若是熟悉 JVM 字节码技术上面的注入点很容易理解,以后我可以写一篇 JVM 字节码相关的文章。

byteman helper 类

我们在脚本中使用的打印语句

DO traceln("*** next value is : " + $arg)

其实使用的 byteman 的默认的 helper 类,若是觉得内置的 helper 不够用,可以自定 helper,如下:

# helper example
RULE help yourself
CLASS com.arjuna.wst11.messaging.engines.CoordinatorEngine
METHOD commit
HELPER com.arjuna.wst11.messaging.engines.FailureTester
AT EXIT
IF doWrongState($0)
DO throw new WrongStateException()
ENDRULE

byteman 中环境变量的设置

由于环境变的总共有

现在解释我们上面用到的两个

更加详细的解释可以到官方网站中的 Environment Settings 查看。

javaagent 技术介绍

利用 Java 代码,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:

等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。

在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

另外,对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。

最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path 的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。

javaagent 技术演示

“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。

上面我使用 byteman 动态注入 JVM 得到运行时程序中的某个局部变量的值,现在我们来看看支撑这个技术使用的东西。现在我们不是用 byteman 来达到这个目的。

premain 方式

  1. 编写 premain 函数

编写一个 Java 类,包含如下两个方法当中的任何一个

public static void premain(String agentArgs, Instrumentation inst);[1]
public static void premain(String agentArgs);[2]

其中,[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。 在这个 premain 函数中,开发者可以进行对类的各种操作。

2.jar 文件打包

将这个 Java 类打包成一个 jar 文件,并在其中的 manifest 属性当中加入“Premain-Class”来指定步骤 1 当中编写的那个带有 premain 的 Java 类。(可能还需要指定其他属性以开启更多功能)

3.运行

用如下方式运行带有 Instrumentation 的 Java 程序:

java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]

对 Java 类文件的操作,可以理解为对一个 byte 数组的操作(将类文件的二进制字节流读入一个 byte 数组)。开发者可以在“ClassFileTransformer”的 transform 方法当中得到,操作并最终返回一个类的定义(一个 byte 数组)。

agentmain 方式

在 Java SE 5 当中,开发者只能在 premain 当中施展想象力,所作的 Instrumentation 也仅限与 main 函数执行前,这样的方式存在一定的局限性。 在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序。 在 Java SE 6 的 Instrumentation 当中,有一个跟 premain“并驾齐驱”的“agentmain”方法,可以在 main 函数开始运行之后再运行。 跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类:

public static void agentmain (String agentArgs, Instrumentation inst);[1]
public static void agentmain (String agentArgs);[2]

同样,[1] 的优先级比 [2] 高,将会被优先执行。 跟 premain 函数一样,开发者可以在 agentmain 中进行对类的各种操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。

与“Premain-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。

可是,跟 premain 不同的是,agentmain 需要在 main 函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢?

在 Java SE 6 文档当中,开发者也许无法在 java.lang.instrument 包相关的文档部分看到明确的介绍,更加无法看到具体的应用 agnetmain 的例子。不过,在 Java SE 6 的新特性里面,有一个不太起眼的地方,揭示了 agentmain 的用法。这就是 Java SE 6 当中提供的 Attach API。

Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。

Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

package edu.hushi.agent;

import java.io.IOException;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;

/**
 *
 * @author -10
 *
 */
public class LoadedAgentLauncher {

	public static void main(String[] args)
			throws AttachNotSupportedException,
            IOException, AgentLoadException,
            AgentInitializationException {
        VirtualMachine vm = VirtualMachine.attach(args[0]);
        vm.loadAgent("D:\\worktool\\Git\\Workspaces\\BTrace\\libs\\my-agent.jar");
    }

}

其中 args[0] 传入需要 attach 的进程号。

参考

关于我

85 后程序员, 比较熟悉 Java,JVM,Golang 相关技术栈, 关注 Liunx kernel,目前痴迷于分布式系统的设计和实践。 研究包括但不限于 Docker Kubernetes eBPF 等相关技术。

Blog

Code

Life

Archive