本文内容
- 如何使用 JNI
- 如何在 JNI 中调用 JVMTI 在heap上根据class查找对象
- 对于如何在实践中使用本篇内容,可以参考:
3.1 使用Instrumentation和Javassist修改web应用字节码
3.2 查找WebServer中各个App的ClassLoader
前言
- 在 java 中,获取对象的class很容易,但是反过来,要获取class所产生的对象却是没有直接办法的。
- 为什么需要查找class的对象实例呢?因为很多时候,我们需要在运行时,动态修改对象的字节码来完成一些有趣的事情,但往往我们只知道这些对象的类型,也就是class,却缺少查找它们的手段。
- 其实JVM提供了一套丰富的API给我们来使用,也就是JVMTI(JVM Tool Interface)。这套API可以在Instrumentation中来使用,也可以通过JNI来调用。本文将介绍如何通过JNI来调用JVMTI,从而在heap上根据class来查找对象。
- 可能会有人担心使用了JNI会不会丧失了平台可移植性,答案是肯定的。但是对于同样的系统,其实可移植性还在,如果是不同的系统,那就只能重新编译了。
生成native方法的头文件
java代码:
package test.jni;
public class JvmtiTest {
public native Object findObjectByClass(Class<?> clazz);
}
在某个目录输入以下命令:
~/test_jni/jvmti_test $ javac -h . $YourJavaDir/JvmtiTest.java
就会在该目录下生成一个 .h 文件
drwxrwxr-x 2 helowken helowken 4096 Jul 23 23:14 ./
drwxrwxr-x 6 helowken helowken 4096 Jul 23 23:14 ../
-rw-rw-r-- 1 helowken helowken 486 Jul 23 23:14 test_jni_JvmtiTest.h
你可以查看它的内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class test_jni_JvmtiTest */
#ifndef _Included_test_jni_JvmtiTest
#define _Included_test_jni_JvmtiTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: test_jni_JvmtiTest
* Method: findObjectByClass
* Signature: (Ljava/lang/Class;)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL Java_test_jni_JvmtiTest_findObjectByClass
(JNIEnv *, jobject, jclass);
#ifdef __cplusplus
}
#endif
#endif
如果你没接触过JNI,可能会觉得有些迷糊,但不用担心,我们需要关注的东西不多,只要了解:
- .h 文件中包含了对应java类方法的native方法
a) java 类方法:findObjectByClass(Class<?> clazz)
b) native方法:Java_test_jni_JvmtiTest_findObjectByClass() - 类型的映射
a) java的Object -> native的 jobject
b) java的Class -> native的 jclass - native方法的参数
a) 第一个参数是 JNIEnv 的指针,指向包含所有虚方法的结构
b) 第二个参数类型是 jobject,对应的是 java 中的 this,如果java方法为static,则这个参数为NULL
生成native方法的实现文件
在同一个目录下增加 .h 的实现文件:test_jni_JvmtiTest.c
当前实现先留空
#include "jvmti.h"
#include "test_jni_JvmtiTest.h"
JNIEXPORT jobject JNICALL
Java_test_jni_JvmtiTest_findObjectByClass(JNIEnv* env, jobject thisObj, jclass targetClass) {
return NULL;
}
其中 jvmti.h 头文件是存放在 $JAVA_HOME/include 目录下的,这里用 “” 而不是 <>,是因为它不在系统搜索路径上,编译的时候需要自行加入。
这里附带一份 jvmti demo 的 makefile:
#
# Copyright (c) 2004, 2012, Oracle and/or its affiliates. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# - Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# - Neither the name of Oracle nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
########################################################################
#
# Sample GNU Makefile for building JVMTI Demo
#
# Example uses:
# gnumake JDK=<java_home> OSNAME=solaris [OPT=true] [LIBARCH=sparc]
# gnumake JDK=<java_home> OSNAME=solaris [OPT=true] [LIBARCH=sparcv9]
# gnumake JDK=<java_home> OSNAME=linux [OPT=true]
# gnumake JDK=<java_home> OSNAME=win32 [OPT=true]
#
########################################################################
# Source lists
LIBNAME=$(FILE)
SOURCES=$(FILE).c
# Solaris Sun C Compiler Version 5.5
ifeq ($(OSNAME), solaris)
# Sun Solaris Compiler options needed
COMMON_FLAGS=-mt -KPIC
# Options that help find errors
COMMON_FLAGS+= -Xa -v -xstrconst -xc99=%none
# Check LIBARCH for any special compiler options
LIBARCH=$(shell uname -p)
ifeq ($(LIBARCH), sparc)
COMMON_FLAGS+=-xarch=v8 -xregs=no%appl
endif
ifeq ($(LIBARCH), sparcv9)
COMMON_FLAGS+=-xarch=v9 -xregs=no%appl
endif
ifeq ($(OPT), true)
CFLAGS=-xO2 $(COMMON_FLAGS)
else
CFLAGS=-g $(COMMON_FLAGS)
endif
# Object files needed to create library
OBJECTS=$(SOURCES:%.c=%.o)
# Library name and options needed to build it
LIBRARY=lib$(LIBNAME).so
LDFLAGS=-z defs -ztext
# Libraries we are dependent on
LIBRARIES= -lc
# Building a shared library
LINK_SHARED=$(LINK.c) -G -o $@
endif
# Linux GNU C Compiler
ifeq ($(OSNAME), linux)
# GNU Compiler options needed to build it
COMMON_FLAGS=-fno-strict-aliasing -fPIC -fno-omit-frame-pointer
# Options that help find errors
COMMON_FLAGS+= -W -Wall -Wno-unused -Wno-parentheses
ifeq ($(OPT), true)
CFLAGS=-O2 $(COMMON_FLAGS)
else
CFLAGS=-g $(COMMON_FLAGS)
endif
# Object files needed to create library
OBJECTS=$(SOURCES:%.c=%.o)
# Library name and options needed to build it
LIBRARY=lib$(LIBNAME).so
LDFLAGS=-Wl,-soname=$(LIBRARY) -static-libgcc
# Libraries we are dependent on
LIBRARIES=-lc
# Building a shared library
LINK_SHARED=$(LINK.c) -z noexecstack -shared -o $@
endif
$(info $(SOURCES))
$(info $(OBJECTS))
# Windows Microsoft C/C++ Optimizing Compiler Version 12
ifeq ($(OSNAME), win32)
CC=cl
# Compiler options needed to build it
COMMON_FLAGS=-Gy -DWIN32
# Options that help find errors
COMMON_FLAGS+=-W0 -WX
ifeq ($(OPT), true)
CFLAGS= -Ox -Op -Zi $(COMMON_FLAGS)
else
CFLAGS= -Od -Zi $(COMMON_FLAGS)
endif
# Object files needed to create library
OBJECTS=$(SOURCES:%.c=%.obj)
# Library name and options needed to build it
LIBRARY=$(LIBNAME).dll
LDFLAGS=
# Libraries we are dependent on
LIBRARIES=
# Building a shared library
LINK_SHARED=link -dll -out:$@
endif
# Common -I options
CFLAGS += -I.
CFLAGS += -I$(JDK)/include -I$(JDK)/include/$(OSNAME)
# Default rule
all: $(LIBRARY)
# Build native library
$(LIBRARY): $(OBJECTS)
$(LINK_SHARED) $(OBJECTS) $(LIBRARIES)
# Cleanup the built bits
clean:
rm -f $(LIBRARY) $(OBJECTS)
# Simple tester
test: all
LD_LIBRARY_PATH=`pwd` $(JDK)/bin/java -agentlib:$(LIBNAME) -version
# Compilation rule only needed on Windows
ifeq ($(OSNAME), win32)
%.obj: %.c
$(COMPILE.c) $<
endif
你可以不用管它的内容,甚至不用看明白它,如果你想知道怎么使用,可以看看它的注释:Example uses。
为了方便地运行,增加以下脚本 run_make:
#!/bin/bash
FILE=$1
args="-f ../makefile JDK=/usr/jdk1.8 OSNAME=linux FILE=$FILE"
make clean $args
echo ------------------------------
make $args
我的目录结构:
~/test_jni $ tree
.
├── jvmti_test
│ ├── test_jni_JvmtiTest.c
│ └── test_jni_JvmtiTest.h
├── makefile
└── run_make
在目录 jvmti_test 里面运行命令:
~/test_jni/jvmti_test $ ../run_make test_jni_JvmtiTest
输出:
test_jni_JvmtiTest.c
test_jni_JvmtiTest.o
rm -f libtest_jni_JvmtiTest.so test_jni_JvmtiTest.o
------------------------------
test_jni_JvmtiTest.c
test_jni_JvmtiTest.o
cc -g -fno-strict-aliasing -fPIC -fno-omit-frame-pointer -W -Wall -Wno-unused -Wno-parentheses -I. -I/usr/jdk1.8/include -I/usr/jdk1.8/include/linux -c -o test_jni_JvmtiTest.o test_jni_JvmtiTest.c
cc -g -fno-strict-aliasing -fPIC -fno-omit-frame-pointer -W -Wall -Wno-unused -Wno-parentheses -I. -I/usr/jdk1.8/include -I/usr/jdk1.8/include/linux -Wl,-soname=libtest_jni_JvmtiTest.so -static-libgcc -z noexecstack -shared -o libtest_jni_JvmtiTest.so test_jni_JvmtiTest.o -lc
目录下会多出两个文件:
test_jni_JvmtiTest.o
libtest_jni_JvmtiTest.so (这个就是我们将要使用的库文件)
测试JNI的调用
修改 JvmtiTest.java的代码:
package test.jni;
public class JvmtiTest {
static {
System.loadLibrary("test_jni_JvmtiTest");
}
public native Object findObjectByClass(Class<?> clazz);
public static void main(String[] args) {
System.out.println(new JvmtiTest().findObjectByClass(JvmtiTest.class));
}
}
注意:static 代码区在运行时会加载名为 “test_jni_JvmtiTest” 的库文件
运行会报错:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no test_jni_JvmtiTest in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at test.jni.JvmtiTest.<clinit>(JvmtiTest.java:5)
其实你也会好奇:jvm怎么知道 “test_jni_JvmtiTest” 这个库文件在哪里?
确实,jvm是不知道的,所以我们需要告诉它从哪里加载。
加入jvm启动参数,把库文件所在的目录传递给它:
-Djava.library.path=/home/helowken/test_jni/jvmti_test
再次运行,发现没有报错,返回结果:null,那是因为我们的 native 方法实现直接返回了 NULL。
上面这种加载方式,需要jvm启动的时候就指定库文件目录,不够灵活,你可以换一种更好的方式,修改 JvmtiTest.java:
package test.jni;
public class JvmtiTest {
public native Object findObjectByClass(Class<?> clazz);
public static void main(String[] args) {
System.load("/home/helowken/test_jni/jvmti_test/libtest_jni_JvmtiTest.so");
JvmtiTest jvmtiTest = new JvmtiTest();
System.out.println(jvmtiTest);
System.out.println(jvmtiTest.findObjectByClass(JvmtiTest.class));
}
}
改用 System.load 方法,传入库文件的绝对路径,就可以实现动态加载。
在native方法中调用JVMTI
回到 test_jni_JvmtiTest.c 文件,增加以下实现代码:
#include "jvmti.h"
#include "test_jni_JvmtiTest.h"
static jint JNICALL
TagObjectByClass(jlong classTag, jlong size, jlong* tagPtr, jint length, void* userData) {
*tagPtr = 1;
return JVMTI_VISIT_ABORT;
}
JNIEXPORT jobject JNICALL
Java_test_jni_JvmtiTest_findObjectByClass(JNIEnv* env, jobject thisObj, jclass targetClass) {
JavaVM* vm;
(*env)->GetJavaVM(env, &vm);
jvmtiEnv* jvmti;
(*vm)->GetEnv(vm, (void**) &jvmti, JVMTI_VERSION_1_0);
jvmtiCapabilities capabilities = {0};
capabilities.can_tag_objects = 1;
(*jvmti)->AddCapabilities(jvmti, &capabilities);
jvmtiHeapCallbacks callbacks = {};
callbacks.heap_iteration_callback = TagObjectByClass;
(*jvmti)->IterateThroughHeap(jvmti, 0, targetClass, &callbacks, NULL);
jlong tag = 1;
jint count;
jobject* instances;
(*jvmti)->GetObjectsWithTags(jvmti, 1, &tag, &count, &instances, NULL);
if (instances != NULL) {
jobject result = *instances;
(*jvmti)->Deallocate(jvmti, (unsigned char*) instances);
return result;
}
return NULL;
}
看不懂没关系,保存后,重新运行命令:
~/test_jni/jvmti_test $ ../run_make test_jni_JvmtiTest
然后运行 JvmTest.java,你会看到:
test.jni.JvmtiTest@4dd8dc3
test.jni.JvmtiTest@4dd8dc3
表明 native 方法已经成功从heap上根据class来查找对象实例了。
JVMTI代码分析
- 获取 jvmtiEnv 的指针,它指向一个包含了所有 jvmti 虚方法的结构
JavaVM* vm;
(*env)->GetJavaVM(env, &vm);
jvmtiEnv* jvmti;
(*vm)->GetEnv(vm, (void**) &jvmti, JVMTI_VERSION_1_0);
- IterateThroughHeap api 的调用需要 can_tag_objects 的 capability,其他功能我们暂时不需要,所以 capabilities 初始化为 {0}。
jvmtiCapabilities capabilities = {0};
capabilities.can_tag_objects = 1;
(*jvmti)->AddCapabilities(jvmti, &capabilities);
- 设置在heap上遍历对象时的callback方法
jvmtiHeapCallbacks callbacks = {};
callbacks.heap_iteration_callback = TagObjectByClass;
callback方法在命中对象时,给对象打上一个标记,用于后续根据标记获取对象,这里打上的标记为1。
JVMTI_VISIT_ABORT 的意思是终止后续的遍历,因为我们的例子只有一个对象,所以只要callback方法被调用,就可以结束heap的遍历了。
static jint JNICALL
TagObjectByClass(jlong classTag, jlong size, jlong* tagPtr, jint length, void* userData) {
*tagPtr = 1;
return JVMTI_VISIT_ABORT;
}
方法原型:
typedef jint (JNICALL *jvmtiHeapIterationCallback)
(jlong class_tag,
jlong size,
jlong* tag_ptr,
jint length,
void* user_data);
参数说明:
class_tag: 对象的class的标记
size: 对象所占内存的字节数
tag_ptr: 用于给对象打标记的指针
length: 如果对象是数组,则此为数组长度,否则为-1
user_data: 用户在遍历heap时传入的自定义参数
- 开始遍历heap,在这里我们遍历所有对象,只对类型为 targetClass 的对象应用callback方法,不传递用户自定义数据。
(*jvmti)->IterateThroughHeap(jvmti, 0, targetClass, &callbacks, NULL);
方法原型:
jvmtiError
IterateThroughHeap(jvmtiEnv* env,
jint heap_filter,
jclass klass,
const jvmtiHeapCallbacks* callbacks,
const void* user_data)
参数说明:
heap_filter: heap 过滤标志,可根据class和对象有没有被标记过,从而过滤掉不需要的对象
klass: callback只应用于类型为 klass 的对象,不会应用于 klass 的子类对象,如果 klass 是接口,那么 callback 不起作用。如果 klass 为 NULL,则 callback 方法应用于所有对象。
callbacks: 指向callback方法表的指针
user_data: 传递给callback的用户自定义参数
- 根据标记查找对象,标记是在 callback 方法中打上的,只打了一个标记,且值为1。
jlong tag = 1;
jint count;
jobject* instances;
(*jvmti)->GetObjectsWithTags(jvmti, 1, &tag, &count, &instances, NULL);
方法原型:
jvmtiError
GetObjectsWithTags(jvmtiEnv* env,
jint tag_count,
const jlong* tags,
jint* count_ptr,
jobject** object_result_ptr,
jlong** tag_result_ptr)
参数说明:
tag_count: 标记数量,限制 tags 的遍历
tags:指向标记数组的指针
count_ptr: 指向“返回的对象指针数组的长度”的指针,限制 object_result_ptr 的遍历
object_result_ptr: 指向对象指针数组的指针
tag_result_ptr: 对于返回的每个对象,指向对象标记对应标记数组中的index
以上的代码省略了错误的处理,如果需要完全看懂上面 JVMTI 的代码,我建议你参考官方文档,有很详细的说明