我对Java字节码很陌生。根据我的理解,当分解JAR文件时,结果将是JVM直接解释的字节码(数字)。每个字节或2个字节的数字都与实际Java源文件中的Java方法相关联。我在哪里可以找到这些地图?
此外,假设我想知道一个变量是否在类中初始化过,但之后再也没有使用过。我可以简单地检查它是何时实例化的,然后如果它在初始化后再也没有出现在字节码中,就认为它从未使用过吗?为了使这种逻辑工作,JVM必须按顺序执行字节码,这样初始化的变量就不能跳到另一个函数,等等。函数边界的定义不同于通用汇编代码(intel,MIPS)。
提前谢谢。
理解JVM字节码需要一些时间。为了让你开始这里有两件事你需要知道:
-
JVM是一个堆栈机器:当它需要对表达式求值时,它首先将表达式的输入推入堆栈,然后对表达式求值本质上是将所有输入从堆栈中弹出,并将结果推回堆栈顶部。反过来,这个结果可以用作另一个表达式求值的输入。
-
所有参数和局部变量都存储在局部变量数组中。
让我们在实践中看看。这是一个源代码:
package p1;
public class Movie {
public void setPrice(int price) {
this.price = price;
}
}
正如EJP所说,您应该运行javap-c来查看字节码:javap -c bin/p1/Movie.class
。这是输出:
public class p1.Movie {
public p1.Movie();
Code:
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
public void setPrice(int);
Code:
0: aload_0
1: iload_1
2: putfield #18 // Field price:I
5: return
}
从输出中可以看到,在字节码中,我们看到了默认的构造函数和setPrice
方法。
第一条指令aload_0
取局部变量0的值并将其推入堆栈(完整的指令列表)。在非静态方法中,局部变量0始终是this
参数,因此在指令0之后,我们的堆栈是
| this |
+------+
下一条指令是aload_1
,它取局部变量1的值并将其推入堆栈。在我们的局部变量中,1是方法的参数(价格)。我们的堆栈现在看起来如下:
| price |
| this |
+-------+
下一个指令CCD_ 6是执行分配CCD_。此指令从堆栈中弹出两个值。第一个弹出的值是字段的新值。第二个弹出的值是指向保存要分配的字段的对象的指针。要分配的域的名称被编码在指令中(这就是为什么指令需要三个字节:它从位置2开始,但下一条指令从位置5开始)。编码到指令中的额外值是"#18"。这是常量池的索引。要查看常量池,您应该运行:javap -v bin/p1/Movie.class
:
Classfile /home/imaman/workspace/Movie-shop/bin/p1/Movie.class
...
Constant pool:
#1 = Class #2 // p1/Movie
...
#5 = Utf8 price
#6 = Utf8 I
...
#18 = Fieldref #1.#19 // p1/Movie.price:I
#19 = NameAndType #5:#6 // price:I
...
因此,#18
指定要分配的字段是p1.Movie
类的price
字段(如您所见,#18引用#1、#19,后者又引用#5和#6。分配给字段的实际名称显示在常量池中)
回到我们对putfield
指令的执行:JVM从堆栈中弹出了两个值,现在将第一个弹出的值分配到this
对象的price
字段(由#18
指示)中(第二个弹出值)。
评估堆栈现在为空。
最后一条指令简单地返回。
这里还不完全清楚你在问什么,所以让我回答一些问题:
方法边界是定义良好的,不同于"正常"的程序集代码。类型在任何地方都定义良好。字段定义良好。类是定义良好的。指令边界是明确定义的(跳到指令中间是非法的)。代码和数据很容易区分。方法不能访问彼此的变量;仅字段。这些东西使分析Java字节码比分析机器代码容易得多。
要从Java程序中读取和写入类文件,我建议使用ASM库。它将负责理解类文件格式,并将其转换为更易于使用的格式(Java对象树或方法调用序列)。还有其他具有类似用途的库,如BCEL、cgLib和Javassist。我对其他图书馆还不够熟悉,无法与它们进行比较。
对于"大多数"指令,方法中的字节码是按顺序执行的。有几个指令会导致执行不按顺序进行——通常这就是指令的意图(例如,用于实现if
/while
/等的条件跳转)。许多指令也可以抛出异常,这会导致执行跳转到异常处理程序或退出当前方法。
以下说明影响控制流程:
areturn
、dreturn
、freturn
、ireturn
、lreturn
—执行时,使方法正常返回。在极少数情况下(错误生成的字节码),也可以抛出IllegalMonitorStateException
athrow
—执行时会引发异常if_icmpne
、if_icmpeq
、if_icmplt
、if_icmpge
、if_icmpgt
、if_icmple
、ifne
、ifeq
、iflt
、ifge
、ifgt
、ifle
、ifnonnull
、ifnull
—条件跳转指令goto
、goto_w
—无条件跳转指令aaload
、aastore
、anewarray
、arraylength
、baload
、bastore
、caload
、castore
、checkcast
、daload
、dastore
、faload
、fastore
、getfield
、getstatic
、iaload
、iastore
、idiv
、instanceof
、putfield #18
0、laload
、lastore
、ldc
、ldc_w
、ldc2_w
、ldiv
、lrem
、monitorenter
、monitorexit
、this.price = price
0、newarray
、putfield
、putstatic
、saload
,sastore
-在某些情况下可以抛出异常invokedynamic
、invokeinterface
、invokespecial
、invokestatic
、invokevirtual
、new
-导致调用其他方法,这可能导致引发异常jsr
、jsr_w
、ret
-允许方法包含从多个位置调用的子例程。幸运的是,现代编译器似乎不会生成这些
正如EJP在注释中所说,您可以反编译一个名为example的类。通过使用javap-c示例Main。Main.class命令。正如我们所看到的,Java字节码比IA32更结构化。实际的字节码指令包含在方法中,就像Java本身一样。
您可以在以下位置找到有关每个字节码指令的信息:
- JVM规范
- 维基百科
JVM指令操作操作数堆栈。例如:
LDC 10 // Push the constant 10 onto the stack.
LDC 20 // Push the constant 20 onto the stack.
IADD // Pop two numbers off the stack, add them, push the result.
ISTORE 5 // Pop an integer (in this case 30) off the stack and put it in variable #5.
正如您可能注意到的,局部变量实际上存储在堆栈帧的编号槽中。Java编译器的工作是将一个局部变量与一个带编号的槽相关联。需要指出的是,类型(boolean、char、byte、short、int或引用类型)的变量将存储在单个槽中。但是,long或double类型的变量需要两个插槽来存储它们。此外,在非静态方法中,时隙#0总是用于保持this
。此外,参数总是与编号最低的插槽相关联。因此,在非静态方法moo(String message, int times)
中,this
将在时隙#0中,变量message
将在时隙#1中,而变量times
将在时隙#2中。
为了确定方法的字节码中局部变量alive的位置,您需要对方法的字节代码使用Live variable Analysis,因为字节码指令是随后执行的(即不是一次全部执行),但不一定是线性执行的。
另一方面,字段不存储在堆栈帧中的上述字段中。我认为您可能指的是字段,因为它们可以"在类中初始化",然后在不同的方法中使用,而局部变量是声明它们的方法的局部变量。
JVM不一定按顺序执行字节码,但它的行为就像按顺序执行一样(有点像处理器上的无序执行,但有更多的深度优化)。
不过,您似乎关心的主要问题是字节码平台的结构问题。
-
不,字节码不能跳转到另一个函数。转移控制的唯一方法是通过异常或特殊调用指令,这些指令通过VM。每个堆栈帧都是完全隔离的。
-
此外,所有字节码都强制执行了类型检查,尽管类型检查比Java语言级别的检查要宽松一些。例如,您不能将浮点值作为int进行插入,更不用说指针了。所有内存访问都被虚拟机抽象掉了,不可能像在本机代码中那样进行原始内存访问。
-
指令可以超过2个字节(事实上,切换指令可以是任意长的)。但是绝大多数指令都是1字节或3字节。它们不一定与Java源代码的元素一一对应,尽管映射通常很简单。一般来说,Java的后续版本添加了更多的语法糖,这在使用这些功能时降低了字节码与原始源代码的相似性(一个值得注意的例子是打开字符串)。