12月 042015
 

注意:本文章主要依据BCEL官方手册进行阐述,大部分内容都是从该手册直接翻译过来的,并做了一定的简化,同时还参考了《深入理解Java虚拟机》(周志明著)。笔者在下面阐述的时候也会给出相应的章节,如果读者有不清楚的地方请参考BCEL官方手册或是BCEL API,以及《深入理解Java虚拟机》。

BCEL是什么?

相信搜索到这篇文章的读者应该知道BCEL是啥,不过还是简要提一下吧:BCEL(Byte Code Engineering Library)原本是Apache Jakarta的一个子项目,目前已成为Apache Commons的一个子项目,主要用于分析、创建、操纵Java  class文件。
为了说明如何使用BCEL,需要简要介绍一下JVM结构和class文件的结构。

JVM结构

JVM
JVM是Java提供平台无关性的基础,它是一台抽象的机器,主要任务是装载class文件并且执行其中的字节码。Java虚拟机的体系结构如上图所示。这里仅给出JVM中比较重要的部分的说明,详细介绍请参考《深入理解Java虚拟机》(周志明著)和《Java Virtual Machiine Specification》(参见Oracle网站)。

  • 类装载子系统:每个Java虚拟机都有一个类装载子系统,它根据给定的全限定名来装入类或接口。
  • 运行时数据区:用于组织需要内存来存储的东西,如,字节码,程序创建的对象,传递给方法的参数,返回值,局部变量,运算的中间结果,……
  • Java堆:存放运行时创建的对象实例,以及对象间的引用关系
  • Java栈:其实更准确的称呼应该是“Java Virtual Machine Stack”,每当一个方法被执行的时候,都会创建一个Java栈帧(stack frame),栈帧中保存着该方法的操作数栈、局部变量表(local variable table)、动态链接、方法出口等信息,栈帧创建后就会放到Java栈上,执行完毕后抛弃该栈帧。我们平时所说的方法内的局部变量保存在“栈”上其实就是指Java栈。
  • 方法区:保存被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。注意方法区中包含有一个运行区常量池,class文件中的常量池(将在后面讲解)中的内容将在类加载后存放到这个运行时常量池中。
  • 程序计数器(PC计数器):PC寄存器的值总是指示下一条将被执行的指令
  • 本地方法栈:用于本地方法的执行,本地方法参考《Core Java》中有关JNI的阐述。

Java class文件格式

注:要完整的说明class文件的结构是个非常费劲的事情,JVM指令系统更是如此。限于篇幅这里不再详细阐述,仅给出简要的说明,详细说明请参考《深入理解Java虚拟机》和《Java Virtual Machiine Specification》
class文件的格式大体如下图所示
classfile
class文件的header部分包含有一个“魔数”(magic number)0xCAFEBABE(其实是”cafe baby”,此乃当年Java开发团队的幽默),然后是版本号,再之后是常量池,访问权限标志位,当前类(注意,这里的类是指类和接口,是比较泛泛的概念,下文同)所实现的接口列表,当前类所包含的域、方法,最后是当前类的属性(可以包含用户自定义的数据)。
由于需要在运行时动态解析的类、域、方法的符号引用实际都是以字符串常量的形式保存在常量池中,所以常量池实际上占了class文件的很大比重,一般是60%,而byte code一般只占12%。
常量池中一般包含有如下类型的常量:References to methods, fields and classes, strings, integers, floats, longs, and doubles.(以上参考 2.1 java class file format)
注意,class文件中,非抽象方法包含有一个属性“Code”,包含有栈帧所需的最大深度、局部变量的个数、byte code指令的数组。当在方法执行过程中发生异常时,JVM会查找异常表(table of exception handlers),该表标记了handler,也就是哪些异常处理代码负责处理具体哪个部分代码所抛出的异常,如果没有合适的handler,异常将会传递给该方法(callee)的调用者(caller)。handler的信息保存在“Code”属性中。(2.3 Method Code)

实例分析

(这一部分参考2.6 Code Example)
首先结合上述的内容,分析以下Java代码编译后所得到的class文件。(将就着看吧,wordpress里一行行调代码缩进实在太麻烦,就没怎么调整~~)

     import java.io.*;
public class Factorial {
       private static BufferedReader in = new BufferedReader(new
                                 InputStreamReader(System.in));
public static final int fac(int n) {
         return (n == 0)? 1 : n * fac(n - 1);
       }
public static final int readInt() {
         int n = 4711;
         try {
         System.out.print("Please enter a number> ");
         n = Integer.parseInt(in.readLine());
         } catch(IOException e1) { System.err.println(e1); }
         catch(NumberFormatException e2) { System.err.println(e2); }
         return n;
       }
public static void main(String[] argv) {
         int n = readInt();
         System.out.println("Factorial of " + n + " is " + fac(n));
       }
     }

fac()方法对应的字节码如下:
 

0:  iload_0
1:  ifne            #8
4:  iconst_1
5:  goto            #16
8:  iload_0
9:  iload_0
10: iconst_1
11: isub
12: invokestatic    Factorial.fac (I)I (12)
15: imul
16: ireturn
LocalVariable(start_pc = 0, length = 16, index = 0:int n)

每条指令的功能这里就不细说,需要注意的是,JVM是一种基于栈的虚拟机,任何操作都需要先将操作数放入栈帧中的操作数栈,然后调用相关方法,操作的结果也会自动的压入操作数栈。在本例中,fac()方法采用了递归的方式计算n!,第12行体现了这一点,在JVM执行到底12行时,会创建一个新的栈帧,以执行新的fac()方法调用。此外,ireturn会将结果返回给当前方法(callee)的调用者(caller),压入到caller栈帧中操作数栈的栈顶。
readInt()方法的字节码如下:
 

    0:  sipush        4711
 3:  istore_0
 4:  getstatic     java.lang.System.out Ljava/io/PrintStream;
 7:  ldc           "Please enter a number> "
 9:  invokevirtual java.io.PrintStream.print (Ljava/lang/String;)V
 12: getstatic     Factorial.in Ljava/io/BufferedReader;
 15: invokevirtual java.io.BufferedReader.readLine ()Ljava/lang/String;
 18: invokestatic  java.lang.Integer.parseInt (Ljava/lang/String;)I
 21: istore_0
 22: goto          #44
 25: astore_1
 26: getstatic     java.lang.System.err Ljava/io/PrintStream;
 29: aload_1
 30: invokevirtual java.io.PrintStream.println (Ljava/lang/Object;)V
 33: goto          #44
 36: astore_1
 37: getstatic     java.lang.System.err Ljava/io/PrintStream;
 40: aload_1
 41: invokevirtual java.io.PrintStream.println (Ljava/lang/Object;)V
 44: iload_0
 45: ireturn
Exception handler(s) =
 From    To      Handler Type
 4       22      25      java.io.IOException(6)
 4       22      36      NumberFormatException(10)

这个例子需要说明的是,平时我们调用System.out.println()打印输出时,虽然只是传入要打印的内容,但在字节码层次上,首先需要将System类的静态变量out压入到栈顶,然后将需要打印的内容(这里是字符串“”Please enter a number>”)压入栈,接着调用println()方法。之所以需要将out压入栈,是因为实例方法调用的时候,都会将该实力对象的引用隐式地作为第一个参数(Instance methods always implicitly take an instance reference as their first argument)。
另外,Java代码中的异常处理代码,try语句不会产生任何实际代码,只是规定了exception handler在哪些代码发生异常时会被调用。

BCEL API

BCEL API主要分为以下3个部分(3. The BCEL API):

  • bcel.classfile.*:主要用于查看class文件的结构(尤其是在没有源代码的情况下),一般不用作byte code的修改。
  • bcel.generic.*:动态产生或是修改class文件,可以插入代码、从class文件中剔除无用代码、实现一个Java编译器的代码生成器后端。
  • 其他:代码示例,使用工具

bcel.classfile.*的类设计如下图:
javaclass
通过下面的代码,即可访问一个class文件的JavaClass对象,然后通过这个对象的get/set方法,即可访问这个class文件的各个部分:

JavaClass clazz = Repository.lookupClass("java.lang.String");

比如说,下面的printCode()就能输出String类的所有方法:
 

System.out.println(clazz);
 printCode(clazz.getMethods());
...
public static void printCode(Method[] methods) {
for(int i=0; i < methods.length; i++) {
System.out.println(methods[i]);
Code code = methods[i].getCode();
if(code != null) // Non-abstract method
System.out.println(code);
}
}

3.3 ClassGen
bcel.generic.*的类设计如下图:
classgen
要创建一个类,需要使用ClassGen,而要给这个类添加其他部分,则需要使用上图中给出的其他类。在给出具体实例之前,还需要先确定如何表示类型——域需要指明其类型,方法则需要给出参数及其返回值的类型。
3.3.1 Types
使用BCEL的Type类用法如下:

Type   return_type = Type.VOID;
Type[] arg_types   = new Type[] { new ArrayType(Type.STRING, 1) }

3.3.2 Generic fields and methods
域需要使用FieldGen来创建,并且需要指定其访问权限。方法则需要添加可能需要的异常、local variables、exception handler。由于包含有byte code的地址的引用,这两者被称之为instruction targeter,这些targeter包含有updateTarget()方法,用于更新目标地址,这个地方是采用Observer模式实现的。一般方法(不是抽象方法)会指向一个instruction list(包含instruction对象),对byte code地址的引用由instruction对象处理,当instruction list更新时,instruction targeter也会被更新。
每个方法的maximum stack size和maximum number of local variables可以手动或是采用setMaxStack()和setMaxLocals()方法自动设置。
3.3.3 Instructions
instruction包含有 opcode(有时叫做tag),字节长度,在byte code中的偏移量。有些指令是不可变的(比如operators),InstructionConstants用于提供预定义的常量供用户使用(flyweight模式)。
指令分类:按照type hierarchy of instruction classes(附录有)。也可以按照所实现的接口分类。
重要的指令:branch instructions,比如goto,使得这些指令也可以看做instruction targeter。
所有指令都可以通过accept(Visitor v)方法来访问(visitor模式)。(3.3.3 Instructions)
3.3.4 Instruction lists
instruction list:对instructions的引用不是由直接指向instruction的指针,而是指向instruction handle的指针。这使得添加、插入、删除byte code很方便,同时允许重用不可变的指令对象(flyweight object)。由于使用符号引用,具体的bytecode偏移量的计算直到finalization才计算即可,即用户停止操作bytecode的时候。下文将instruction handle和instruction视为同一概念。instruction handle通过addAttribute()方法可以包含有用户自定义数据。
append操作
instructionlist的append方法会返回一个instruction handle,用作branch instruction的目标地址。

InstructionList il = new InstructionList();
...
GOTO g = new GOTO(null);
il.append(g);
...
// Use immutable fly-weight object
InstructionHandle ih = il.append(InstructionConstants.ACONST_NULL);
g.setTarget(ih);

insert操作
指令可以插入到现有list的任意位置,需要插入到给定instruction handle的前边,insert方法会返回一个instruction handle用于可能的异常处理的目标地址。

InstructionHandle start = il.insert(insertion_point,
InstructionConstants.NOP);
...
mg.addExceptionHandler(start, end, handler, "java.io.IOException");

delete操作
需要给出指定范围,该范围内的指令将被删除并释放资源(dispose)。但instruction targeter仍引用着将要被删除的指令时,delete方法可能抛出TargetLostException。用户需要自己处理这些异常。参见附录的窥孔优化的例子。

try {
il.delete(first, last);
} catch(TargetLostException e) {
InstructionHandle[] targets = e.getTargets();
for(int i=0; i < targets.length; i++) {
InstructionTargeter[] targeters = targets[i].getTargeters();
for(int j=0; j < targeters.length; j++)
targeters[j].updateTarget(targets[i], new_target);
}
};

finalize操作
当instruction list已经操作完毕、打算生成纯粹的字节码时,所有符号引用被映射为真实的byte code offset,这一操作由getByteCode()完成(默认由Method.getMethod())。生成字节码后用户应该调用dispose()一遍使得这些instrution handle能别重用。

InstructionList il = new InstructionList();
ClassGen  cg = new ClassGen("HelloWorld", "java.lang.Object",
"<generated>", ACC_PUBLIC | ACC_SUPER,
null);
MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC,
Type.VOID, new Type[] {
new ArrayType(Type.STRING, 1)
}, new String[] { "argv" },
"main", "HelloWorld", il, cp);
...
cg.addMethod(mg.getMethod());
il.dispose(); // Reuse instruction handles of list

3.3.6 Instruction Factories
为了简化某些指令的创建,用户可以使用InstructionFactory类创建指令(提供了很多有用的方法用于创建指令),也可以使用compound instruction:当产生byte code,某些“模式”出现的比较频繁,比如算术或是比较运算,可以用一个compound instruction(一个只有单一的一个getInstructionList()方法的接口),这可以用于任何位置,尤其是添加操作。
例子:将操作数压入栈,用PUSH可以自动产生合适的指令

InstructionFactory f  = new InstructionFactory(class_gen);
InstructionList    il = new InstructionList();
...
il.append(new PUSH(cp, "Hello, world"));
il.append(new PUSH(cp, 4711));
...
il.append(f.createPrintln("Hello World"));
...
il.append(f.createReturn(type));

regular expressions
可以用正则表达式来搜索特定模式的代码。——org.apache.bcel.util.InstructionFinder的search()方法,找到后会返回一个迭代器,其他的约束条件可以通过code constraint对象来表达。

CodeConstraint constraint = new CodeConstraint() {
public boolean checkCode(InstructionHandle[] match) {
IfInstruction if1 = (IfInstruction)match[0].getInstruction();
GOTO          g   = (GOTO)match[2].getInstruction();
return (if1.getTarget() == match[3]) &&
(g.getTarget() == match[4]);
}
};
InstructionFinder f    = new InstructionFinder(il);
String            pat = "IfInstruction ICONST_0 GOTO ICONST_1 NOP(IFEQ|IFNE)";
for(Iterator e = f.search(pat, constraint); e.hasNext(); ) {
InstructionHandle[] match = (InstructionHandle[])e.next();;
...
match[0].setTarget(match[5].getTarget()); // Update target
...
try {
il.delete(match[1], match[5]);
} catch(TargetLostException ex) { ... }
}

例子:优化boolean表达式

用BCEL生成HelloWorld

最后,举个简单的例子,说明具体如何使用BCEL直接生成class文件。比如说,我们现在要使用BCEL生成一个HelloWorld.class文件,实现如下代码的功能:

try {
 // BCEL Appendix A example
 // ClassGen(String class_name, String super_class_name, String
 // file_name, int access_flags, String[] interfaces)
 ClassGen cg = new ClassGen("HelloWorld", "java.lang.Object",
 "<generated>", ACC_PUBLIC | ACC_SUPER, null);
 ConstantPoolGen cp = cg.getConstantPool();
 InstructionList il = new InstructionList();
// create main method
 // MethodGen(int access_flags, Type return_type, Type[] arg_types,
 // String[] arg_names, String method_name, String class_name,
 // InstructionList il, ConstantPoolGen cp)
 MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC, Type.VOID,
 new Type[] { new ArrayType(Type.STRING, 1) },
 new String[] { "argv" }, "main", "HelloWorld", il, cp);
InstructionFactory factory = new InstructionFactory(cg);
// define some often used types
 ObjectType i_stream = new ObjectType("java.io.InputStream");
 ObjectType p_stream = new ObjectType("java.io.PrintStream");
 //%%
 //BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
 //%%
 // create variables in and name
 il.append(factory.createNew("java.io.BufferedReader"));
 il.append(InstructionConstants.DUP);
 il.append(factory.createNew("java.io.InputStreamReader"));
 il.append(InstructionConstants.DUP);
 //init input stream
 il.append(factory.createFieldAccess("java.lang.System", "in",
 i_stream, Constants.GETSTATIC));
 il.append(factory.createInvoke("java.io.InputStreamReader",
 "<init>", Type.VOID, new Type[] { i_stream },
 Constants.INVOKESPECIAL));
 il.append(factory.createInvoke("java.io.BufferedReader", "<init>",
 Type.VOID, new Type[] { new ObjectType("java.io.Reader") },
 Constants.INVOKESPECIAL));
 //add in into the local variable pool and get the index automatically
 LocalVariableGen lg = mg.addLocalVariable("in", new ObjectType(
 "java.io.BufferedReader"), null, null);
 int in = lg.getIndex();//index of "in" var
 lg.setStart(il.append(new ASTORE(in)));//store the reference into local variable
 //首先创建对象,并初始化,操作结果在JVM的“堆”里,还需要在本地变量表中创建引用,因此在本地变量表中添加一个“in”比那辆,
 //然后根据索引值调用“astore”指令,即可将对象引用赋值给本地变量
 /*
 0: new #8; //class java/io/BufferedReader
 3: dup
 4: new #10; //class java/io/InputStreamReader
 7: dup
 8: getstatic #16; //Field java/lang/System.in:Ljava/io/InputStream;
 11: invokespecial #20; //Method java/io/InputStreamReader."<init>":(Ljava/io/InputStream;)V
 14: invokespecial #23; //Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
 17: astore_1
 * */
//%%
 //String name = null;
 //%%
 // create local variable name and init it to null
 lg = mg.addLocalVariable("name", Type.STRING, null, null);
 int name = lg.getIndex();
 il.append(InstructionConstants.ACONST_NULL);//add "null" to the stack top
 lg.setStart(il.append(new ASTORE(name)));//"store" the value of "null" into "name" var
//%%
 //System.out.print("Please enter your name> ")
 //%%
 // create try_catch block
 InstructionHandle try_start = il.append(factory.createFieldAccess(
 "java.lang.System", "out", p_stream, Constants.GETSTATIC));
 //从常量池中取出“please .....”,压入栈顶:这里感觉有问题,这个字符串常量应该先压入常量池才可以(最好是在这之前加一句,
 //加一句添加常量池操作其实并不影响实际运行的效率)
 il.append(new PUSH(cp, "Please enter your name> "));
 il.append(factory.createInvoke("java.io.PrintStream", "print",
 Type.VOID, new Type[] { Type.STRING },
 Constants.INVOKEVIRTUAL));
 //%%
 //name = in.readLine();
 //%%
 //将本地变量“in”推送至栈顶
 il.append(new ALOAD(in));
 il.append(factory.createInvoke("java.io.BufferedReader",
 "readLine", Type.STRING, Type.NO_ARGS,
 Constants.INVOKEVIRTUAL));//调用readLine()方法
 il.append(new ASTORE(name));//接收的结果在栈顶,需要保存,因此加上保存到“name”slot的指令
//%%
 // } catch(IOException e) { return; }
 //%%
 GOTO g = new GOTO(null);
 InstructionHandle try_end = il.append(g);
 //add return:如果出异常,才会走到这条“return”指令,并返回到caller中
 InstructionHandle handler = il.append(InstructionConstants.RETURN);
 // add exception handler which returns from the method
 mg.addExceptionHandler(try_start, try_end, handler, null);
//%%
 //没有异常,继续执行:System.out.println("Hello, " + name);
 //%%
 // "normal" code continues, set the branch target of the GOTO
 InstructionHandle ih = il.append(factory.createFieldAccess(
 "java.lang.System", "out", p_stream, Constants.GETSTATIC));
 g.setTarget(ih);
// print "Hello":创建一个StringBuffer对象,通过调用StringBuffer的append操作,实现
 //string1 + string2的操作,并且操作结果调用toString方法
 il.append(factory.createNew(Type.STRINGBUFFER));
 il.append(InstructionConstants.DUP);
 il.append(new PUSH(cp, "Hello, "));
 il.append(factory.createInvoke("java.lang.StringBuffer", "<init>",
 Type.VOID, new Type[] { Type.STRING },
 Constants.INVOKESPECIAL));
 il.append(new ALOAD(name));
 il.append(factory.createInvoke("java.lang.StringBuffer", "append",
 Type.STRINGBUFFER, new Type[] { Type.STRING },
 Constants.INVOKEVIRTUAL));
 //
 il.append(factory.createInvoke("java.lang.StringBuffer",
 "toString", Type.STRING, Type.NO_ARGS,
 Constants.INVOKEVIRTUAL));
 il.append(factory.createInvoke("java.io.PrintStream", "println",
 Type.VOID, new Type[] { Type.STRING },
 Constants.INVOKEVIRTUAL));
 il.append(InstructionConstants.RETURN);
// finalization
 mg.setMaxStack();
 cg.addMethod(mg.getMethod());
 il.dispose();
 cg.addEmptyConstructor(ACC_PUBLIC);
// dump the class
cg.getJavaClass().dump("HelloWorld.class");
 System.out.println("dump successly");
 } catch (java.io.IOException e) {
 System.err.println(e);
 } catch (Exception e1) {
 e1.printStackTrace();
 }

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)