欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

【译文】BCEL的介绍
(last modified Jun 26, 2023, 2:04 PM )
by
侧边栏壁纸
  • 累计撰写 176 篇文章
  • 累计创建 61 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

【译文】BCEL的介绍

橙序员
2022-08-07 / 0 评论 / 0 点赞 / 638 阅读 / 10,611 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

2.1 介绍Java 语言已经变得非常流行,许多研究项目都致力于进一步改进该语言或其运行时行为。用新概念扩展语言的可能性无疑是一个可取的特性,但应该对用户隐藏实现问题。幸运的是,Java 虚拟机的概念允许用户以相对较少的努力透明地实现此类扩展。由于 Java 的目标语言是一种解释性语言,具有少量且易

2.1 介绍

Java 语言已经变得非常流行,许多研究项目都致力于进一步改进该语言或其运行时行为。用新概念扩展语言的可能性无疑是一个可取的特性,但应该对用户隐藏实现问题。幸运的是,Java 虚拟机的概念允许用户以相对较少的努力透明地实现此类扩展。

由于 Java 的目标语言是一种解释性语言,具有少量且易于理解的指令集(字节码),因此开发人员可以以非常优雅的方式实现和测试他们的概念。可以为系统的类加载器编写一个插件替代品,该类加载器负责在运行时动态加载类文件并将字节码传递给虚拟机(参见第 2 节)。因此,类加载器可用于拦截加载过程并在类被 JVM 实际执行之前对其进行转换。虽然原始类文件始终保持不变,但类加载器的行为可以为每次执行重新配置或动态检测。

BCEL API(字节码工程库),原名JavaClass,是用于Java类文件的静态分析和动态创建或转换的工具包。它使开发人员能够在高级抽象上实现所需的功能,而无需处理 Java 类文件格式的所有内部细节,因此每次都重新发明轮子。 BCEL 完全用 Java 编写,根据 Apache 软件许可条款免费提供。

本手册的结构如下: 我们在第 2 节中简要介绍了 Java 虚拟机和类文件格式。第 3 节介绍了 BCEL API。第 4 节描述了一些典型的应用领域和示例项目。附录包含将在本文主要部分中呈现的代码示例。所有示例都包含在可下载的发行版中。

2.2 JVM虚拟机

已经熟悉 Java 虚拟机和 Java 类文件格式的读者可能想跳过本节并继续第 3 节。

用 Java 语言编写的程序被编译成一种可移植的二进制格式,称为字节码。 每个类都由一个包含类相关数据和字节码指令的类文件表示。 这些文件被动态加载到解释器(Java 虚拟机,又名 JVM)中并执行。

图 1 说明了编译和执行 Java 类的过程:源文件(HelloWorld.java)被编译成 Java 类文件(HelloWorld.class),由字节码解释器加载并执行。 为了实现附加功能,研究人员可能希望在类文件实际执行之前转换类文件(用粗线绘制)。 这个应用领域是本文的主要问题之一。

img

请注意,使用通用术语“Java”实际上意味着两个含义:一方面,Java 作为一种编程语言,另一方面,Java 虚拟机,不一定是 Java 语言的专有目标,但 也可以被其他语言使用。 我们假设读者熟悉 Java 语言并对虚拟机有一个大致的了解。

JAVA类文件格式

全面概述 Java 类文件格式和相关字节码指令的设计问题超出了本文的范围。我们将简要介绍一下理解本文其余部分所必需的细节。类文件的格式和字节码指令集在 Java 虚拟机规范中有更详细的描述。特别是,我们不会处理 Java 虚拟机在运行时必须检查的安全约束,即字节码验证器。

图 2 显示了一个 Java 类文件内容的简化示例:它以包含“魔法值”(0xCAFEBABE)和版本号的标头开头,然后是常量池,可以粗略地认为是文本段可执行文件的访问权限,由位掩码编码的类的访问权限,类实现的接口列表,包含类的字段和方法的列表,最后是类属性,例如,告诉名称的 SourceFile 属性源文件。属性是一种将附加的、用户定义的信息放入类文件数据结构的方法。例如,自定义类加载器可以评估此类属性数据以执行其转换。 JVM 规范声明任何虚拟机实现都必须忽略未知的,即用户定义的属性。

img

因为在运行时动态解析对类、字段和方法的符号引用所需的所有信息都是用字符串常量编码的,所以常量池实际上包含了平均类文件的最大部分,大约 60%。 事实上,这使得常量池成为代码操作问题的简单目标。 字节码指令本身仅占 12%。

右上方的框显示了常量池的“缩放”摘录,而下面的圆形框描绘了示例类的方法中包含的一些指令。 这些指令代表了众所周知的陈述的直接翻译:

System.out.println("Hello, world");

第一条指令将 java.lang.System 类中的字段内容加载到操作数堆栈中。这是类 java.io.PrintStream 的一个实例。 ldc(“加载常量”)将对字符串“Hello world”的引用压入堆栈。下一条指令调用实例方法 println,它将两个值都作为参数(实例方法总是隐式地将实例引用作为它们的第一个参数)。

类文件中的指令、其他数据结构和常量本身可以引用常量池中的常量。这种引用是通过直接编码到指令中的固定索引来实现的。图中的某些项目用周围的方框强调了这一点。

例如,invokevirtual 指令引用 MethodRef 常量,该常量包含有关被调用方法的名称、签名(即编码的参数和返回类型)以及方法所属的类的信息。事实上,正如装箱值所强调的那样,MethodRef 常量本身只是引用保存真实数据的其他条目,例如,它引用包含对 java.io.PrintStream 类的符号引用的 ConstantClass 条目。为了保持类文件紧凑,这些常量通常由不同的指令和其他常量池条目共享。类似地,字段由 Fieldref 常量表示,该常量包含有关字段的名称、类型和包含类的信息。

常量池基本上保存以下类型的常量:对方法、字段和类、字符串、整数、浮点数、长整数和双精度数的引用。

字节码指令集

JVM 是一个面向堆栈的解释器,它为每个方法调用创建一个固定大小的本地堆栈帧。本地堆栈的大小必须由编译器计算。值也可以中间存储在包含局部变量的帧区域中,这些局部变量可以像一组寄存器一样使用。这些局部变量的编号从 0 到 65535,即每个方法最多有 65536 个局部变量。调用者和被调用者方法的堆栈帧是重叠的,即调用者将参数压入操作数堆栈,被调用方法在局部变量中接收它们。

字节码指令集目前由 212 条指令组成,44 个操作码被标记为保留,可用于虚拟机内的未来扩展或中间优化。指令集大致可以分为以下几类:

堆栈操作:常量可以通过使用 ldc 指令从常量池加载它们或使用特殊的“快捷”指令将它们压入堆栈,其中操作数被编码到指令中,例如,iconst_0 或 bipush(推送字节值) .

算术运算:Java 虚拟机的指令集使用不同的指令对特定类型的值进行运算来区分其操作数类型。例如,以 i 开头的算术运算表示整数运算。例如,iadd 将两个整数相加并将结果推回堆栈。 Java 类型 boolean、byte、short 和 char 由 JVM 作为整数处理。

控制流:有 goto 和 if_icmpeq 之类的分支指令,它们比较两个整数是否相等。还有一个 jsr(跳转到子程序)和 ret 指令对,用于实现 try-catch 块的 finally 子句。使用 throw 指令可能会引发异常。分支目标被编码为与当前字节码位置的偏移量,即使用整数。

加载和存储操作:对 iload 和 istore 等局部变量的加载和存储操作。还有像 iastore 这样的数组操作,它将整数值存储到数组中。

字段访问:实例字段的值可以用 getfield 检索并用 putfield 写入。对于静态字段,有 getstatic 和 putstatic 对应项。

方法调用:静态方法可以通过invokestatic调用,也可以与invokevirtual指令虚拟绑定。超类方法和私有方法使用invokespecial 调用。一种特殊情况是使用invokeinterface 调用的接口方法。

对象分配:类实例使用 new 指令分配,基本类型数组如 int[] 使用 newarray,引用数组如 String[][] 使用 anewarray 或 multianewarray。

转换和类型检查:对于基本类型的堆栈操作数,存在像 f2i 这样的转换操作,它将浮点值转换为整数。可以使用 checkcast 检查类型转换的有效性,并且可以将 instanceof 运算符直接映射到同名指令。

大多数指令具有固定长度,但也有一些可变长度指令:特别是用于实现 switch() 语句的 lookupswitch 和 tableswitch 指令。由于 case 子句的数量可能不同,因此这些指令包含可变数量的语句。

我们不会在这里列出所有字节码指令,因为这些在 JVM 规范中有详细说明。操作码名称大多是不言自明的,因此理解以下代码示例应该相当直观。

方法区

非抽象(和非本地)方法包含一个属性“代码”,该属性包含以下数据:方法堆栈帧的最大大小、局部变量的数量和字节码指令数组。 可选地,它还可能包含有关局部变量名称和调试器可以使用的源文件行号的信息。

每当在执行期间引发异常时,JVM 通过查看异常处理程序表来执行异常处理。 该表将处理程序(即代码块)标记为负责在字节码的给定区域内引发的某些类型的异常。 当没有适当的处理程序时,异常会传播回方法的调用者。 处理程序信息本身存储在包含在 Code 属性中的属性中。

字节码偏移

goto 等分支指令的目标被编码为字节码数组中的相对偏移量。 异常处理程序和局部变量引用字节码中的绝对地址。 前者包含对 try 块的开始和结束的引用,以及对指令处理程序代码的引用。 后者标记了局部变量有效的范围,即它的作用域。 这使得在这个抽象级别上插入或删除代码区域变得困难,因为每次都必须重新计算偏移量并更新引用对象。 我们将在 3.3 节中看到 BCEL 如何解决这个限制。

类型信息

Java 是一种类型安全的语言,有关字段类型、局部变量和方法的信息存储在所谓的签名中。 这些是存储在常量池中并以特殊格式编码的字符串。 例如 main 方法的参数和返回类型

public static void main(String[] argv)

会表示为:

([java/lang/String;)V

类在内部由字符串表示,如“java/lang/String”,基本类型如浮点数由整数表示。 在签名中,它们由单个字符表示,例如 I,表示整数。 数组在签名的开头用 [ 表示。

代码示例

下面的示例程序提示输入一个数字并打印它的阶乘。 从标准输入读取的 readLine() 方法可能会引发 IOException,如果将拼写错误的数字传递给 parseInt(),则会引发 NumberFormatException。 因此,代码的关键区域必须封装在 try-catch 块中。

import java.io.*;

public class Factorial {
    private static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    public static int fac(int n) {
        return (n == 0) ? 1 : n * fac(n - 1);
    }

    public static 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));
    }
}

此代码示例通常编译为以下字节码块:

        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)
      

fac():方法 fac 只有一个局部变量,参数 n,存储在索引 0 处。该变量的范围从字节码序列的开头到结尾。 如果 n 的值(通过 iload_0 获取的值)不等于 0,则 ifne 指令跳转到偏移量 8 处的字节码,否则将 1 压入操作数堆栈,控制流程跳转到最终返回。 为了便于阅读,分支指令的偏移量实际上是相对的,在这些示例中显示为绝对地址。

如果必须继续递归,则计算乘法的参数(n 和 fac(n - 1))并将结果压入操作数堆栈。 执行乘法运算后,该函数从堆栈顶部返回计算值。

        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)
      

readInt():首先将局部变量 n(在索引 0 处)初始化为值 4711。下一条指令 getstatic 将静态 System.out 字段持有的引用加载到堆栈中。然后加载并打印一个字符串,从标准输入读取一个数字并分配给 n。

如果调用的方法之一(readLine() 和 parseInt())抛出异常,Java 虚拟机将调用已声明的异常处理程序之一,具体取决于异常的类型。 try 子句本身不会产生任何代码,它只是定义后续处理程序处于活动状态的范围。在该示例中,指定的源代码区域映射到一个字节码区域,范围从偏移量 4(包括)到 22(不包括)。如果没有发生异常(“正常”执行流程),则 goto 指令在处理程序代码后面分支。在那里,n 的值被加载并返回。

java.io.IOException 的处理程序从偏移量 25 开始。它只是打印错误并分支回到正常的执行流程,即好像没有发生异常一样。

2.3 BCEL API

BCEL API 从 Java 虚拟机的具体情况以及如何读写二进制 Java 类文件中抽象出来。 API主要由三部分组成:

  1. 包含描述类文件的“静态”约束的类的包,即反映类文件格式并且不用于字节码修改。 这些类可用于从文件读取类文件或将类文件写入文件。 这对于在手头没有源文件的情况下分析 Java 类特别有用。 主要的数据结构称为 JavaClass,其中包含方法、字段等。
  2. 用于动态生成或修改 JavaClass 或 Method 对象的包。 它可用于插入分析代码,从类文件中去除不必要的信息,或实现 Java 编译器的代码生成器后端。
  3. 各种代码示例和实用程序,例如类文件查看器、将类文件转换为 HTML 的工具以及从类文件到 Jasmin 汇编语言的转换器。

java 类文件

BCEL API 的“静态”组件位于包 org.apache.bcel.classfile 中,并且紧密地表示类文件。 在 JVM 规范中声明并在第 2 节中描述的所有二进制组件和数据结构都映射到类。 图 3 显示了 BCEL API 的类层次结构的 UML 图。 附录中的图 8 还显示了 ConstantPool 组件的详细图表。

img

顶级数据结构是 JavaClass,它在大多数情况下由能够解析二进制类文件的 ClassParser 对象创建。 JavaClass 对象基本上由字段、方法、对超类的符号引用和实现的接口组成。

常量池充当某种中央存储库,因此对所有组件都非常重要。 ConstantPool 对象包含一个固定大小的常量条目数组,可以通过将整数索引作为参数的 getConstant() 方法检索。 常量池的索引可以包含在指令中,也可以包含在类文件的其他组件中,也可以包含在常量池条目本身中。

方法和字段包含一个签名,象征性地定义了它们的类型。 像 public static final 这样的访问标志出现在几个地方,并由整数位掩码编码,例如,public static final 与 Java 表达式匹配

int access_flags = ACC_PUBLIC | ACC_STATIC | ACC_FINAL;

正如 2.1 节已经提到的,几个组件可能包含属性对象:类、字段、方法和代码对象(在 2.3 节中介绍)。 后者是一个属性本身,它包含实际的字节码数组、最大堆栈大小、局部变量的数量、处理的异常表以及一些编码为 LineNumberTable 和 LocalVariableTable 属性的可选调试信息。 属性通常特定于某些数据结构,即没有两个组件共享同一种属性,尽管这没有明确禁止。 在图中,属性类是用它们所属的组件来构造的。

类仓库(Class repository)

使用提供的 Repository 类,将类文件读入 JavaClass 对象非常简单:

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

该存储库还包含提供 instanceof 运算符的动态等效项的方法,以及其他有用的例程:

if (Repository.instanceOf(clazz, super_class)) {
    ...
}
      

访问类文件数据

类文件组件中的信息可以像 Java Bean 一样通过直观的 set/get 方法访问。 它们都定义了一个 toString() 方法,因此实现一个简单的类查看器非常容易。 事实上,这里使用的所有示例都是这样生成的:

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);
    }
}

解析类文件数据

最后但同样重要的是,BCEL 支持访问者设计模式,因此可以编写访问者对象来遍历和分析类文件的内容。 发行版中包含一个 JasminVisitor 类,它将类文件转换为 Jasmin 汇编语言。

类生成

API 的这一部分(包 org.apache.bcel.generic)为动态创建或转换类文件提供了一个抽象级别。 它使 Java 类文件的静态约束,如硬编码的字节码地址“通用”。 例如,通用常量池由类 ConstantPoolGen 实现,它提供了添加不同类型常量的方法。 因此,ClassGen 提供了一个接口来添加方法、字段和属性。 图 4 概述了 API 的这一部分。

img

类型

我们通过引入 Type 类从类型签名语法的具体细节(见 2.5)中抽象出来,例如,方法使用它来定义它们的返回和参数类型。 具体的子类是 BasicType、ObjectType 和 ArrayType,它们由元素类型和维数组成。 对于常用类型,该类提供了一些预定义的常量。 例如2.5节中main方法的方法签名表示为:

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

Type 还包含将类型转换为文本签名的方法,反之亦然。 子类包含 Java 语言规范指定的例程和约束的实现。

通用字段和方法

字段由 FieldGen 对象表示,用户可以自由修改。如果它们具有访问权限 static final ,即是常量并且是基本类型,它们可以选择具有初始化值。

泛型方法包含添加方法可能抛出的异常、局部变量和异常处理程序的方法。后两者也由用户可配置的对象表示。因为异常处理程序和局部变量包含对字节码地址的引用,所以它们在我们的术语中也起到了指令目标器的作用。指令目标器包含一个方法 updateTarget() 来重定向引用。这在某种程度上与观察者设计模式有关。通用(非抽象)方法是指由指令对象组成的指令列表。对字节码地址的引用由指令对象的句柄实现。如果列表更新,指令目标者将被告知。这将在以下各节中更详细地解释。

该方法所需的最大堆栈大小和使用的局部变量的最大数量可以手动设置,也可以通过 setMaxStack() 和 setMaxLocals() 方法自动计算。

指令

将指令建模为对象乍一看可能有些奇怪,但实际上使程序员无需处理具体字节码偏移等细节即可获得控制流的高级视图。指令由操作码(有时称为标签)、它们的字节长度和字节码内的偏移量(或索引)组成。由于许多指令是不可变的(例如堆栈运算符),InstructionConstants 接口提供了可共享的预定义“fly-weight”常量以供使用。

指令通过子类进行分组,指令类的类型层次由附录中的(不完整)图说明。最重要的指令系列是分支指令,例如 goto,它分支到字节码内的某个目标。显然,这也使他们成为了担任 InstructionTargeter 角色的候选人。指令按它们实现的接口进一步分组,例如,与特定类型(如 ldc)相关联的 TypedInstructions,或在执行时可能引发异常的 ExceptionThrower 指令。

所有指令都可以通过 accept(Visitor v) 方法进行遍历,即访问者设计模式。然而,这些方法中有一些特殊的技巧可以合并某些指令组的处理。 accept() 不只是调用对应的 visit() 方法,而是先调用各自超类和实现的接口的 visit() 方法,即最具体的 visit() 调用在最后。因此,可以将所有 BranchInstructions 的处理分组到一个方法中。

出于调试目的,“发明”您自己的指令甚至可能是有意义的。在复杂的代码生成器中,例如用作 Barat 框架的后端进行静态分析的代码生成器,通常必须插入临时 nop(无操作)指令。在检查生成的代码时,可能很难追溯 nop 实际插入的位置。可以想到包含附加调试信息的派生 nop2 指令。当指令列表转储为字节码时,多余的数据会被简单地丢弃。

人们还可以考虑对复数进行操作的新字节码指令,这些指令在加载时被普通字节码替换或被新的 JVM 识别。

指令集

指令列表由封装指令对象的指令句柄列表实现。因此,对列表中指令的引用不是通过指向指令的直接指针而是通过指向指令句柄的指针来实现的。这使得附加、插入和删除代码区域非常简单,并且还允许我们重用不可变指令对象(轻量对象)。由于我们使用符号引用,因此直到最终确定,即直到用户完成生成或转换代码的过程之前,才需要计算具体的字节码偏移量。在本文的其余部分,我们将使用术语指令句柄和指令同义词。指令句柄可以包含使用 addAttribute() 方法的附加用户定义数据。

追加:可以将指令或其他指令列表附加到现有列表的任何位置。指令附加在给定的指令句柄之后。所有追加方法都返回一个新的指令句柄,然后可以将其用作分支指令的目标,例如

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);

插入:指令可以插入到现有列表的任何位置。 它们被插入到给定的指令句柄之前。 所有插入方法都返回一个新的指令句柄,然后可以将其用作异常处理程序的起始地址,例如

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

删除:指令的删除也很直接; 给定范围内的所有指令句柄和包含的指令都从指令列表中删除并处置。 但是,当指令目标器仍在引用已删除指令之一时,delete() 方法可能会引发 TargetLostException。 用户被迫在 try-catch 子句中处理此类异常,并将这些引用重定向到其他地方。 附录中描述的窥视孔优化器为此提供了详细示例。

try {
    il.delete(first, last);
} catch (TargetLostException e) {
    for (InstructionHandle target : e.getTargets()) {
        for (InstructionTargeter targeter : target.getTargeters()) {
            targeter.updateTarget(target, new_target);
        }
    }
}

常量化:当指令列表准备好转储为纯字节码时,所有符号引用都必须映射到实际字节码偏移量。 这是由 MethodGen.getMethod() 默认调用的 getByteCode() 方法完成的。 之后您应该调用 dispose() 以便可以在内部重用指令句柄。 这有助于提高内存使用率。

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

重新审视代码示例:

使用指令列表为我们提供了代码的通用视图:在图 5 中,我们再次展示了第 2.6 节中阶乘示例的 readInt() 方法的代码块:局部变量 n 和 e1 都持有对指令的两个引用,定义它们的 范围。 在方法结束时有两个 goto 分支到 iload。 还显示了一个异常处理程序:它引用了 try 块的开始和结束以及异常处理程序代码。img

指令工厂

为了简化某些指令的创建,用户可以使用提供的 InstructionFactory 类,它提供了许多有用的方法来从头开始创建指令。或者,他也可以使用复合指令:在生成字节码时,某些模式通常会非常频繁地出现,例如算术或比较表达式的编译。您当然不想在它们可能出现的每个地方重写将这些表达式转换为字节码的代码。为了支持这一点,BCEL API 包含一个复合指令(一个带有单个 getInstructionList() 方法的接口)。这个类的实例可以用在任何会出现正常指令的地方,特别是在附加操作中。

示例:压入常量 将常量压入操作数堆栈可以用不同的方式进行编码。如第 2.2 节所述,有一些“捷径”指令可用于使生成的字节码更紧凑。将单个 1 压入堆栈的最小指令是 iconst_1,其他可能性是 bipush(可用于压入 -128 和 127 之间的值)、sipush(介于 -32768 和 32767 之间)或 ldc(从常量池加载常量) .

无需在例如开关中重复选择最紧凑的指令,只要按下常数或字符串,就可以使用复合 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));

使用正则表达式的代码模式

在转换代码时,例如在优化期间或插入分析方法调用时,通常会搜索某些代码模式来执行转换。 为了简化这种情况的处理,BCEL 引入了一项特殊功能:可以使用正则表达式在指令列表中搜索给定的代码模式。 在这样的表达式中,指令由它们的操作码名称表示,例如 LDC,也可以使用它们各自的超类,例如“IfInstruction”。 +、* 和 (…|…) 等元字符具有它们通常的含义。 因此,表达式

"NOP+(ILOAD|ALOAD)*"

表示一段代码,由至少一个 NOP 后跟可能为空的 ILOAD 和 ALOAD 指令序列组成。

org.apache.bcel.util.InstructionFinder 类的 search() 方法获取一个正则表达式和一个起点作为参数,并返回一个描述匹配指令区域的迭代器。 不能通过正则表达式实现的对指令匹配区域的附加约束可以通过代码约束对象来表达。

示例:解析boolean表达式

在 Java 中,布尔值分别映射到 1 和 0。 因此,计算布尔表达式的最简单方法是根据表达式的真值将 1 或 0 压入操作数堆栈。 但是这样一来,布尔表达式的后续组合(与 &&,例如)会产生很长的代码块,将大量的 1 和 0 推入堆栈。

当代码完成后,可以使用窥孔算法优化这些块:一个 IfInstruction(例如,两个整数的比较:if_icmpeq)在堆栈上产生 1 或 0,然后是 ifne 指令(分支 if 堆栈值 0) 可以由 IfInstruction 替换,其分支目标由 ifne 指令的目标替换:

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) {
        ...
    }
}

应用的代码约束对象确保匹配的代码真正对应于目标表达式模式。 该算法的后续应用从字节码中删除了所有不必要的堆栈操作和分支指令。 如果任何已删除的指令仍被 InstructionTargeter 对象引用,则必须在 catch 子句中更新该引用。

2.4 应用领域

BCEL 有许多可能的应用领域,从类浏览器、分析器、字节码优化器和编译器到复杂的运行时分析工具和 Java 语言扩展。

像 Barat 编译器这样的编译器使用 BCEL 来实现一个字节码生成后端。 其他可能的应用领域是字节码的静态分析或通过在代码中插入对分析方法的调用来检查类的运行时行为。 进一步的示例是使用类似 Eiffel 的断言、自动委托或使用面向方面编程的概念来扩展 Java。

类加载器

类加载器负责从文件系统或其他资源加载类文件并将字节码传递给虚拟机。 自定义 ClassLoader 对象可用于拦截加载类的标准过程,即系统类加载器,并在将字节码实际传递给 JVM 之前执行一些转换。

图 7 描述了一种可能的情况:在运行时,虚拟机请求自定义类加载器来加载给定的类。 但在 JVM 真正看到字节码之前,类加载器会做出一个“回避”并对类执行一些转换。 为了确保修改后的字节码仍然有效并且不违反任何 JVM 规则,它会在 JVM 最终执行之前由验证器进行检查。

img

使用类加载器是用新特性扩展 Java 虚拟机而不实际修改它的一种优雅方式。 这个概念使开发人员能够使用加载时反射来实现他们的想法,而不是 Java 反射 API 支持的静态反射。 加载时转换为用户提供了一个新的抽象级别。 他不受类原始作者的静态约束的严格约束,但可以使用第三方代码自定义应用程序,以便从新功能中受益。 这种转换可以按需执行,既不会干扰其他用户,也不会改变原始字节码。 事实上,类加载器甚至可以在不加载文件的情况下临时创建类。
BCEL 已经内置了对动态创建类的支持,例如 ProxyCreator 类。

示例:通用泛型

例如,用参数化类扩展 Java 的前“穷人的通用性”项目在两个地方使用 BCEL 来生成参数化类的实例:在编译时(使用标准 javac 和一些稍微改变的类)和运行时 使用自定义类加载器。 编译器将一些额外的类型信息放入类文件(属性)中,类加载器在加载时对其进行评估。 类加载器对加载的类执行一些转换并将它们传递给 VM。 以下算法说明了类加载器的 load 方法如何满足对参数化类的请求,例如 Stack

  1. 搜索类 Stack,加载它,并检查包含附加类型信息的特定类属性。该属性定义了类的“真实”名称,即 Stack
  2. 将所有出现和对正式类型 A 的引用替换为对实际类型 String 的引用。 例如方法
 void push(A obj) { ... }

​ 变为

 void push(String obj) { ... }
          
  1. 将生成的类返回给虚拟机。

2.5 附录

HelloWorldBuilder

下面的程序从标准输入中读取一个名字并打印一个友好的“Hello”。 由于 readLine() 方法可能会抛出 IOException,因此它包含在 try-catch 子句中。

import java.io.*;

public class HelloWorld {
    public static void main(String[] argv) {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String name = null;

        try {
            System.out.print("Please enter your name> ");
            name = in.readLine();
        } catch (IOException e) {
            return;
        }

        System.out.println("Hello, " + name);
    }
}

我们将在这里概述如何使用 BCEL API 从头开始创建上述 Java 类。 为了便于阅读,我们将使用文本签名而不是动态创建它们。 例如,签名

"(Ljava/lang/String;)Ljava/lang/StringBuffer;"

实际会通过以下方式创建

Type.getMethodSignature(Type.STRINGBUFFER, new Type[] { Type.STRING });

初始化:首先创建一个空的类文件和指令集:

ClassGen cg = new ClassGen("HelloWorld", "java.lang.Object",
                            "<generated>", ACC_PUBLIC | ACC_SUPER, null);
ConstantPoolGen cp = cg.getConstantPool(); // cg creates constant pool
InstructionList il = new InstructionList();

然后我们创建 main 方法,提供方法的名称和使用 Type 对象编码的符号类型签名。

MethodGen  mg = new MethodGen(ACC_STATIC | ACC_PUBLIC, // access flags
                              Type.VOID,               // return type
                              new Type[] {             // argument types
                              new ArrayType(Type.STRING, 1) },
                              new String[] { "argv" }, // arg names
                              "main", "HelloWorld",    // method, class
                              il, cp);
InstructionFactory factory = new InstructionFactory(cg);

我们现在定义一些常用的类型:

ObjectType i_stream = new ObjectType("java.io.InputStream");
ObjectType p_stream = new ObjectType("java.io.PrintStream");

创建变量和名称:我们调用构造函数,即执行 BufferedReader(InputStreamReader(System.in))。 对 BufferedReader 对象的引用保留在堆栈顶部,并存储在新分配的 in 变量中。

il.append(factory.createNew("java.io.BufferedReader"));
il.append(InstructionConstants.DUP); // Use predefined constant
il.append(factory.createNew("java.io.InputStreamReader"));
il.append(InstructionConstants.DUP);
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));

LocalVariableGen lg = mg.addLocalVariable("in",
                        new ObjectType("java.io.BufferedReader"), null, null);
int in = lg.getIndex();
lg.setStart(il.append(new ASTORE(in))); // "i" valid from here

创建局部变量名并将其初始化为空。

lg = mg.addLocalVariable("name", Type.STRING, null, null);
int name = lg.getIndex();
il.append(InstructionConstants.ACONST_NULL);
lg.setStart(il.append(new ASTORE(name))); // "name" valid from here
      

创建try-catch块:我们记住块的开头,从标准输入中读取一行并将其存储到变量名中。

InstructionHandle try_start =
  il.append(factory.createFieldAccess("java.lang.System", "out", p_stream, Constants.GETSTATIC));

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));
il.append(new ALOAD(in));
il.append(factory.createInvoke("java.io.BufferedReader", "readLine",
                                Type.STRING, Type.NO_ARGS,
                                Constants.INVOKEVIRTUAL));
il.append(new ASTORE(name));

在正常执行时,我们跳到异常处理程序后面,目标地址还不知道。

GOTO g = new GOTO(null);
InstructionHandle try_end = il.append(g);

我们添加了简单地从方法返回的异常处理程序。

InstructionHandle handler = il.append(InstructionConstants.RETURN);
mg.addExceptionHandler(try_start, try_end, handler, "java.io.IOException");

“正常”代码继续,现在我们可以设置 GOTO 的分支目标。

InstructionHandle ih =
  il.append(factory.createFieldAccess("java.lang.System", "out", p_stream, Constants.GETSTATIC));
g.setTarget(ih);

打印hello:&字符串连接编译为 StringBuffer 操作。

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);

常量化:最后,我们必须设置堆栈大小,通常必须动态计算,并向类添加一个默认构造函数方法,在这种情况下它是空的。

mg.setMaxStack();
cg.addMethod(mg.getMethod());
il.dispose(); // Allow instruction handles to be reused
cg.addEmptyConstructor(ACC_PUBLIC);

最后但同样重要的是,我们将 JavaClass 对象转储到文件中。

try {
    cg.getJavaClass().dump("HelloWorld.class");
} catch (IOException e) {
    System.err.println(e);
}

Peephole优化器

这个类实现了一个简单的窥视孔优化器,它从给定的类中删除任何 NOP 指令。

import java.io.*;

import java.util.Iterator;
import org.apache.bcel.classfile.*;
import org.apache.bcel.generic.*;
import org.apache.bcel.Repository;
import org.apache.bcel.util.InstructionFinder;

public class Peephole {

    public static void main(String[] argv) {
        try {
            // Load the class from CLASSPATH.
            JavaClass clazz  = Repository.lookupClass(argv[0]);
            Method[] methods = clazz.getMethods();
            ConstantPoolGen cp = new ConstantPoolGen(clazz.getConstantPool());
    
            for (int i = 0; i < methods.length; i++) {
                if (!(methods[i].isAbstract() || methods[i].isNative())) {
                    MethodGen mg = new MethodGen(methods[i], clazz.getClassName(), cp);
                    Method stripped = removeNOPs(mg);
        
                    if (stripped != null)      // Any NOPs stripped?
                        methods[i] = stripped; // Overwrite with stripped method
                  }
            }
    
            // Dump the class to "class name"_.class
            clazz.setConstantPool(cp.getFinalConstantPool());
            clazz.dump(clazz.getClassName() + "_.class");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Method removeNOPs(MethodGen mg) {
        InstructionList il = mg.getInstructionList();
        InstructionFinder f = new InstructionFinder(il);
        String pat = "NOP+"; // Find at least one NOP
        InstructionHandle next = null;
        int count = 0;
        
        for (Iterator iter = f.search(pat); iter.hasNext();) {
            InstructionHandle[] match = (InstructionHandle[]) iter.next();
            InstructionHandle first = match[0];
            InstructionHandle last  = match[match.length - 1];
            
            // Some nasty Java compilers may add NOP at end of method.
            if ((next = last.getNext()) == null) {
                break;
            }
            
            count += match.length;
            
            /**
             * Delete NOPs and redirect any references to them to the following (non-nop) instruction.
             */
            try {
                il.delete(first, last);
            } catch (TargetLostException e) {
                for (InstructionHandle target : e.getTargets()) {
                    for (InstructionTargeter targeter = target.getTargeters()) {
                        targeter.updateTarget(target, next);
                    }
                }
            }
        }

        Method m = null;

        if (count > 0) {
            System.out.println("Removed " + count + " NOP instructions from method " + mg.getName());
            m = mg.getMethod();
        }

        il.dispose(); // Reuse instruction handles
        return m;
    }
}
0

评论区