BCEL简明教程

BCEL简明教程

注意:本文章主要依据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[](http://www.prayer-laputa.com/blog/wp-content/uploads/2013/02/classfile.gif)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里一行行调代码缩进实在太麻烦,就没怎么调整~~)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
     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()方法对应的字节码如下:\

1
2
3
4
5
6
7
8
9
10
11
12
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()方法的字节码如下:\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    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文件的各个部分:

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

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

1
2
3
4
5
6
7
8
9
10
11
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类用法如下:

1
2
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的目标地址。

1
2
3
4
5
6
7
8
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用于可能的异常处理的目标地址。

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

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

1
2
3
4
5
6
7
8
9
10
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能别重用。

1
2
3
4
5
6
7
8
9
10
11
12
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可以自动产生合适的指令

1
2
3
4
5
6
7
8
9
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对象来表达。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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文件,实现如下代码的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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();
}