如何阅读 Java 字节码以获得乐趣和收益
开始探索 Java 字节码的世界?本文涵盖了您入门所需了解的所有内容。
什么是字节码?
早在 1995 年,Java 编程语言的创造者 Sun Microsystems 就曾大胆宣称。他们说 Java 可以让你“编写一次,随处运行”。这意味着编译后的二进制文件可以在任何系统架构上运行,而 C 语言却做不到这一点,至今它仍然是 Java 编写的核心原则。
为了实现这种跨平台能力,Java 在编译时采用了一种独特的方法。Java 不会将源代码直接转换为机器代码(机器代码特定于每个系统架构),而是将其程序编译为一种中间形式,称为字节码。字节码是一组指令,既不与特定机器语言绑定,也不依赖于任何特定的硬件架构。这种抽象是 Java 可移植性的关键。
解释并执行 Java 字节码指令的程序称为 Java 虚拟机 (JVM)。JVM 将每个字节码指令转换为其所运行的特定系统架构的本机机器码。此过程通常称为“即时”(JIT) 编译,它允许 Java 字节码在任何给定平台上尽可能高效地执行。
查看字节码
不过,字节码不仅仅对 JVM 有用。由于 Java 类的字节码对逆向工程、性能优化、安全研究和其他静态分析功能很有帮助,因此 JDK 附带了一些实用程序来帮助你和我检查它。
为了看清楚字节码的示例,请考虑 `java.lang.Boolean` 中的以下两种方法,`booleanValue` 和 `valueOf(boolean)`,它们分别对 `boolean` 原始类型进行拆箱和装箱:
java
public boolean booleanValue() {
return value;
}
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
使用 JDK 附带的 `javap` 命令,我们可以看到每个字节码。您可以通过使用 `-c` 命令和类的完全限定名称运行 `javap` 来执行此操作,如下所示:
bash
javap -c java.lang.Boolean
结果是 `java.lang.Boolean` 中所有公共方法的字节码。这里我只复制了 `booleanValue` 和 `valueOf(boolean)` 的字节码:
java
public boolean booleanValue();
Code:
0: aload_0
1: getfield #7 // Field value:Z
4: ireturn
public static java.lang.Boolean valueOf(boolean);
Code:
0: iload_0
1: ifeq 10
4: getstatic #27 // Field TRUE:Ljava/lang/Boolean;
7: goto 13
10: getstatic #31 // Field FALSE:Ljava/lang/Boolean;
13: areturn
解析字节码
乍一看,这是一门全新的语言。然而,当你了解了每条指令的作用以及 Java 使用堆栈进行操作后,它很快就会变得简单。
以 booleanValue 的三个字节码指令为例:
“aload_n” 表示将局部变量的引用放入堆栈。在类实例中,“aload_0” 引用的是“this”。
`getfield` 表示从 `this` (堆栈中较低的项)读取成员变量并将该值放入堆栈
`#7` 指的是常量池中的引用索引
`//字段值:Z` 告诉我们 `#7` 指的是什么,一个名为 `value` 的字段,类型为 `boolean`(Z)
`ireturn` 表示从堆栈中弹出一个原始值并返回它
长话短说,这三条指令查找实例的“值”字段并返回它。
作为第二个例子,看一下下一个方法“valueOf(boolean)”:
`iload_n` 表示将一个原始局部变量放入堆栈。`iload_0` 指的是第一个方法参数(因为第一个方法参数是原始的)
`ifeq n` 表示从堆栈中弹出值并查看其是否为真;如果是,则继续下一行,否则跳转到行 `n`
`getstatic #n` 表示将静态成员读入堆栈
`#27` 指的是常量池中的静态成员索引
`// Field TRUE:Ljava/lang/Boolean` 告诉我们 `#27` 指的是什么,一个名为 `TRUE` 的静态成员,类型为 `Boolean`
`goto n` 表示现在跳转到字节码中的第 `n` 行
“areturn” 表示从堆栈中弹出一个引用并返回它
换句话说,这些指令说,取第一个方法参数,如果为真,则返回“Boolean.TRUE”;否则,返回“Boolean.FALSE”。
利用字节码分析
我之前提到过,这对于逆向工程、性能优化和安全研究很有帮助。现在让我们进一步阐述一下。
逆向工程
使用第三方库或闭源组件时,字节码分析会成为一种强大的工具。反编译字节码可以让我们了解这些库的内部工作原理,有助于集成、故障排除和确保兼容性。
在遇到专有或闭源 Java 代码的情况下,阅读字节码可能是了解其功能的唯一可行方法。字节码分析允许您逆向工程并理解闭源应用程序的行为,从而促进互操作性或定制。
举一个现实生活中的例子,我最近试图将第三方软件包纠结分析工具集成到我们的 Ci 系统中。不幸的是,该供应商是闭源的,只有关于如何通过其专有 UI 访问库的文档。通过分析字节码,我能够对底层分析引擎的预期输入和输出进行逆向工程。
性能优化
借助字节码洞察,您可以做出明智的决策来优化特定的代码段。例如,如果字节码显示冗余操作,您可以重构代码以消除效率低下的问题,从而打造出更精简、性能更高的应用程序。
考虑使用增强型 for 循环与管理自己的计数器的简单场景。在 JMH 等其他低级工具中,“javap”可以帮助您了解哪些方法可以创建更少或更优化的字节码指令。
如果针对执行以下两个操作的类运行“javap”:
java
for (int i = 0; i < list.size(); i++) {
sum += list.get(i);
}
for (Integer i : list) {
sum += i;
}
从字节码中可以看到,第一个循环每次都会计算 `.size()`,而增强型 for 循环会做一些更优化的事情:
java
4: iload_2
5: aload_0
6: getfield #19 // Field list:Ljava/util/List;
9: invokeinterface #25, 1 // InterfaceMethod java/util/List.size:()I
14: if_icmpge 42
17: iload_1
18: aload_0
19: getfield #19 // Field list:Ljava/util/List;
22: iload_2
23: invokeinterface #29, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
对阵
java
2: aload_0
3: getfield #19 // Field list:Ljava/util/List;
6: invokeinterface #25, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
11: astore_2
12: aload_2
13: invokeinterface #29, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
18: ifeq 41
21: aload_2
22: invokeinterface #35, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
或者,简而言之,更喜欢增强的 for 循环或至少一个“迭代器”;这就是 JDK 所做的。
安全研究
安全性是软件开发中的重中之重。字节码分析可通过揭示不安全的编码实践或无意泄露的敏感信息来帮助识别潜在的安全漏洞。
看看是否可以找到给定字节码的问题:
java
public boolean verifyLogin(java.lang.String, java.lang.String);
Code:
0: ldc #7 // String josh
2: aload_1
3: invokevirtual #9 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
6: ifeq 20
9: ldc #15 // String password
11: aload_2
12: invokevirtual #9 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
15: ifeq 20
18: iconst_1
19: ireturn
20: iconst_0
21: ireturn
您认为这里发生了什么?前四条指令将第一个方法参数与值“josh”进行比较,接下来的四条指令将第二个方法参数与“password”进行比较。如果其中一个未通过,则返回“iconst_0”。如果两者都通过,则返回“iconst_1”。
如果您猜测成功登录的是 josh/password,那么您是对的!
结论
在不断发展的软件开发领域,阅读和分析 Java 字节码的能力是一项强大的技能。正如我们在本文中所探讨的那样,字节码不仅仅是 Java 编译过程的副产品;它是了解您和他人的 Java 应用程序内部工作原理的窗口。通过揭开字节码的复杂性,我们可以解锁大量优化性能、增强安全性甚至逆向工程的机会。
您觉得这篇文章有用吗?那么请查看Josh Cumming在 Pluralsight 上的许多视频课程,这些课程深入介绍了 Java 和 Spring 框架,例如“ Java 应用程序中的安全编码实践”和“保护 Spring Data REST API ” 。
免责声明:本内容来源于第三方作者授权、网友推荐或互联网整理,旨在为广大用户提供学习与参考之用。所有文本和图片版权归原创网站或作者本人所有,其观点并不代表本站立场。如有任何版权侵犯或转载不当之情况,请与我们取得联系,我们将尽快进行相关处理与修改。感谢您的理解与支持!
请先 登录后发表评论 ~