在前文中已经介绍了JVM的premain和attach功能, 本文介绍下JBOSS开源的代码注入工具: byteman. 与之前讲的premain和attach不同, byteman是通过规则文件完成代码注入等功能.
一. Hello World
1.1
byteman安装
byteman的安装非常简单, 从官网中下载安装包 https://byteman.jboss.org/downloads.html
设置BYTEMAN_HOME
代码语言:javascript复制BYTEMAN_HOME=/xxx/byteman-download-4.0.13
添加path路径
代码语言:javascript复制PATH=$PATH:$BYTEMAN_HOME/bin
1.2
hello world源代码
代码语言:javascript复制public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
编译
代码语言:javascript复制javac HelloWorld.java
运行
代码语言:javascript复制java HelloWorld
执行结果
代码语言:javascript复制Hello, world
1.3
appmain.btm 规则文件
编写注入规则文件, 分别在main()执行开始和结束时, 打印日志.
代码语言:javascript复制RULE trace main entry
CLASS HelloWorld
METHOD main
AT ENTRY
IF true
DO traceln("entering main")
ENDRULE
RULE trace main exit
CLASS HelloWorld
METHOD main
AT EXIT
IF true
DO traceln("exiting main")
ENDRULE
1.4
验证byteman文件语法
验证规则文件语法是否正确:
代码语言:javascript复制bmcheck.sh -cp . -v scripts/helloworld.btm
验证结果
代码语言:javascript复制Checking rule trace main entry against class HelloWorld
Parsed rule "trace main entry" for class HelloWorld
Type checked rule "trace main entry"
Checking rule trace main exit against class HelloWorld
Parsed rule "trace main exit" for class HelloWorld
Type checked rule "trace main exit"
TestScript: no errors
1.5
javaagent方式运行
按javaagent方式方式运行程序:
代码语言:javascript复制java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:scripts/helloworld.btm HelloWorld
运行结果:
代码语言:javascript复制entering main
Hello, world!
exiting main
1.6
简便运行方式
代码语言:javascript复制bmjava.sh -l scripts/helloworld.btm HelloWorld
运行结果:
代码语言:javascript复制entering main
Hello, world!
exiting main
以上, 成功修改main()代码逻辑.
二. byteman规则文件
2.1
规则文件基本格式
byteman规则文件的基本格式如下:
代码语言:javascript复制RULE <规则名>
CLASS/INTERFACE <类名或接口名>
METHOD <方法名>
HELPER <规则增强类>
BIND <绑定事件>
IF <条件>
DO <动作>
ENDRULE
2.2
hello world规则文件简析
以main()进入时注入规则为例, 了解下规则文件.
代码语言:javascript复制# 规则名称
RULE trace main entry
# 要进行注入的类全名
CLASS HelloWorld
# 方法名
METHOD main
# 绑定事件: 方法进入时进行代码注入
AT ENTRY
# 执行条件
IF true
# 以sout方式打印日志
DO traceln("entering main")
ENDRULE
2.3
绑定事件
byteman除了上述方法进入和退出时能进行注入外, 还有其他很多事件可以进行绑定事件.
代码语言:javascript复制AT ENTRY //目标方法的第一个指令之前
AT EXIT //目标方法的所有正常返回点
AT LINE number //目标方法行号
AT READ [type .] field [count | ALL ] //读取field之前
AFTER READ [ type .] field [count | ALL ] //读取field之后
AT READ $var [count | ALL ] //读取local variable、parameter variable之前
AFTER READ $var [count | ALL ] //读取local variable、parameter variable之后
AT WRITE [ type .] field [count | ALL ] //写入field之前
AFTER WRITE [ type .] field [count | ALL ]//写入field之后
AT WRITE $var [count | ALL ] //写入local variable、parameter variable之前
AFTER WRITE $var [count | ALL ] //写入local variable、parameter variable之后
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 ] //抛出异常之前
2.4
规则变量
代码语言:javascript复制$0 被注入类 $this
$1 $2.. 方法参数
$* 目标方法参数列表, 具体含义与$0 $1 $2相同
$# 目标方法参数个数
$! 方法返回值
$@ 方法参数, 与$1 $2相同
$^ 方法异常时, 异常信息
三. byteman的attach模式与helper
byteman除了上述的agent方式外, 还有attach模式, 能够在服务正常运行的情况下, 进行代码注入.
3.1
目标源代码
代码语言:javascript复制package com.in;
import java.lang.management.ManagementFactory;
public class Main2 {
public int add(int x, int y) {
return x y;
}
public static void main(String[] argv) {
String name = ManagementFactory.getRuntimeMXBean().getName();
System.out.println(name);
Main2 m = new Main2();
while (true) {
System.out.println(m.add(1, 2));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.2
注入规则文件
在add()方法刚执行时, 打印方法参数; 在add()方法执行完毕后, 借助TraceHelper类打印方法执行结果.
代码语言:javascript复制RULE trace arg1
CLASS com.in.Main2
METHOD add(int,int)
AT ENTRY
IF true
DO traceln("arg1=" $1 " arg2=" $2)
ENDRULE
RULE trace return value1
CLASS com.in.Main2
METHOD add(int,int)
HELPER com.in.TraceHelper
AT EXIT
IF true
DO myprint($!)
ENDRULE
3.3
TraceHelper类
代码语言:javascript复制package com.in;
import org.jboss.byteman.rule.Rule;
import org.jboss.byteman.rule.helper.Helper;
public class TraceHelper extends Helper {
protected TraceHelper(Rule rule) {
super(rule);
}
public boolean myprint(String message) {
return super.traceln("result:" message);
}
}
3.4
pom依赖
代码语言:javascript复制<dependency>
<groupId>org.jboss.byteman</groupId>
<artifactId>byteman</artifactId>
<version>4.0.13</version>
</dependency>
3.5
服务启动
编译
代码语言:javascript复制javac com.in.Main2.java
运行
代码语言:javascript复制java com.in.Main2
执行结果, 程序PID为47712
代码语言:javascript复制47712@xxx-Pro.local
3
3
3
3.6
byteman注入
绑定JVM进程
代码语言:javascript复制bminstall.sh 47712
提交规则
代码语言:javascript复制bmsubmit.sh -l scripts/appmain2.btm
install rule trace arg1
install rule trace return value1
3.7
服务运行变化
查看执行结果, 已经成功向服务注入代码
代码语言:javascript复制arg1=1 arg2=2
result:3
3
3.8
注意事项
在mac环境中运行时, 需要对bminstall.sh进行修改和完善. 替换tools.jar和java ops变量
代码语言:javascript复制function print_java_version()
{
typeset java_version
# grep output for line : 'java/openjdk version "AAAAAAAA"' where A is alpha, num, '''. or '-'
java_version=`java -version 2>&1 | grep "version" | cut -d'"' -f2`
# format for JDK8- is 1.N.[n_]
if [ ${java_version%%.*} == 1 ] ; then
echo $java_version | cut -d'.' -f2
else
# format may JDK9 may be N.n.[n_] for proper
# release or N-aaa for internal/ea build
echo ${java_version%%[.-]*}
fi
}
source ~/.bash_profile
# use BYTEMAN_HOME to locate installed byteman release
if [ -z "$BYTEMAN_HOME" ]; then
# use the root of the path to this file to locate the byteman jar
BYTEMAN_HOME="${0%*/bin/bminstall.sh}"
# allow for rename to plain bminstall
if [ "$BYTEMAN_HOME" == "$0" ]; then
BYTEMAN_HOME="${0%*/bin/bminstall}"
fi
if [ "$BYTEMAN_HOME" == "$0" ]; then
echo "Unable to find byteman home"
exit
fi
fi
# check that we can find the byteman jar via BYTEMAN_HOME
# the Install class is in the byteman-install jar
if [ -r "${BYTEMAN_HOME}/lib/byteman.jar" ]; then
BYTEMAN_JAR="${BYTEMAN_HOME}/lib/byteman.jar"
else
echo "Cannot locate byteman jar"
exit
fi
# the Install class is in the byteman-install jar
if [ -r "${BYTEMAN_HOME}/lib/byteman-install.jar" ]; then
BYTEMAN_INSTALL_JAR="${BYTEMAN_HOME}/lib/byteman-install.jar"
else
echo "Cannot locate byteman install jar"
exit
fi
# for jdk6/7/8 we also need a tools jar from JAVA_HOME
JAVA_VERSION=$(print_java_version)
if [ $JAVA_VERSION -le 8 ]; then
if [ -z "$JAVA_HOME" ]; then
echo "please set JAVA_HOME"
exit
fi
# on Linux we need to add the tools jar to the path
# this is not currently needed on a Mac
OS=`uname`
if [ ${OS} != "Darwin" ]; then
if [ -r "${JAVA_HOME}/lib/tools.jar" ]; then
TOOLS_JAR="${JAVA_HOME}/lib/tools.jar"
CP="${BYTEMAN_INSTALL_JAR}:${TOOLS_JAR}"
else
echo "Cannot locate tools jar"
CP="${BYTEMAN_INSTALL_JAR}"
fi
else
if [ $JAVA_VERSION -gt 6 ]; then
if [ -r "${JAVA_HOME}/lib/tools.jar" ]; then
TOOLS_JAR="${JAVA_HOME}/lib/tools.jar"
CP="${BYTEMAN_INSTALL_JAR}:${TOOLS_JAR}"
else
echo "Cannot locate tools jar"
CP="${BYTEMAN_INSTALL_JAR}"
fi
else
CP="${BYTEMAN_INSTALL_JAR}"
fi
fi
else
CP="${BYTEMAN_INSTALL_JAR}"
fi
# allow for extra java opts via setting BYTEMAN_JAVA_OPTS
# attach class will validate arguments
BYTEMAN_JAVA_OPTS="-Dorg.jboss.byteman.home=${BYTEMAN_HOME}"
java ${BYTEMAN_JAVA_OPTS} -classpath "$CP" org.jboss.byteman.agent.install.Install $*
四. byteman bash命令
bmcheck.sh 验证规则文件是否有语法错误 bminstall.sh 绑定JVM进程 bmsubmit.sh 将规则文件绑定到运行的JVM中或从JVM中解绑规则 bmjava.sh java agent运行方式的简写方式
五. IDE中开发
为了方便运行, 每次在命令行中执行是非常低效的. 所以, 在IDE中能开发运行是非常必要的. 在服务启动VM参数中添加启动参数:
代码语言:javascript复制-javaagent:/xxx/byteman-download-4.0.13/lib/byteman.jar=script:/xxx/scripts/test.btm
小结
byteman可以将Java代码注入运行时服务, 而无需重新编译, 打包或重新部署应用程序, 可以很容易地跟踪, 监视和测试应用程序和JDK运行时代码的行为.
后文会介绍如何利用byteman进行流量记录和回复.