ZJU PPL HW
class文件分析 by ZJU_Felix
Task
给定如下Java代码文件:
class A {
int f(int k) {
return k*2;
}
}
public class B {
public static void main(String[] args) {
A a = new A();
System.out.println(a.f(10));
}
}
编译并运用各种可能的工具,对B.class的文件内容做分析,要求分析到B类的常量池为止,不需要对函数内的可执行代码做分析。
附件为B.java源代码文件。
Compile B.java
在A2.c所在文件夹使用如下命令行
javac B.java
得到B.class
00 50 1e 05 a8 ee 1e 05 10 33 63 0c 00 ee 88 00 fc 55 00 00 a0 38 63 0c e8 eb 88 00 ec eb 88 00 10 33 63 0c 00 ed 62 0c 78 f2 62 0c c8 eb 88 00 cc eb 88 00 34 ec 88 00 70 10 03 10 ff ff ff ff 1c ec 88 00 49 12 02 10 00 ee 88 00 f4 65 89 00 12 00 00 00 08 32 63 0c 40 ec 88 00 f4 65 89 00 13 00 00 00 00 00 00 00 08 32 63 0c 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 a4 ec 88 00 58 ec 88 00 1f 7c 3b 75 00 ed 88 00 f0 06 3f 01 06 00 00 00 94 ec 88 00 02 76 3b 75 f0 06 3f 01 06 00 00 00 f4 65 89 00 c8 8e af 00 fc 55 00 00 64 ec 88 00 00 00 00 00 90 03 3f 01 fc 55 00 00 f4 65 89 00 00 00 00 00 fc 55 00 00 00 00 00 00 13 00 00 00 e0 ec 88 00 1b 7d 3b 75 90 03 3f 01 00 00 00 00 74 fb fe b9 f4 65 89 00 c8 8e af 00 fc 55 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 18 a8 00 00 00 00 00 00 05 6e 64 96 40 62 09 67 3c 68 0f 5f 00 00 63 0c 10 33 63 0c 00 ee 88 00 fc 55 00 00 a0 38 63 0c e8 eb 88 00 ec eb 88 00 10 33 63 0c 00 ed 62 0c 78 f2 62 0c c8 eb 88 00 cc eb 88 00 34 ec 88 00 70 10 03 10 ff ff ff ff 1c ec 88 00 49 12 02 10 00 ee 88 00 f4 65 89 00 12 00 00 00 08 32 63 0c 40 ec 88 00 f4 65 89 00 13 00 00 00 00 00 00 00 08 32 63 0c 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 a4 ec 88 00 58 ec 88 00 1f 7c 3b 75 00 ed 88 00 f0 06
.class文件二进制分析
.class二进制文件最开始的是magic number, 用于表明这是一个.class文件,固定值:0xCAFEBABE。之后的Ox0000003C是编译器版本号。从0022开始为常量池。
首先的两个字节为常量池中所含常量的数量,本例中即为0x0022,说明共有34个常量。之后是这34个常量的内容。
每一个常量,第一位均为该常量类型,即上述表中14项之一。
#1 常量
分析应该是public static void main(String[] args)函数的Methodref info.
#2常量
#3常量
#4常量
#5常量
#6常量
#7常量
#8常量
#9常量
#10常量
#11常量
以此类推,但这样手工解码二进制文件过于复杂,其中的函数名与函数体的连接方式较为复杂,javap可以帮助我们理清常量池内元素的相互关联。接下来我们还是使用javap命令可以更加方便得分析常量池。
Javap命令分析
输入命令行javap -c -l B.class
得到以下输出:
Compiled from "B.java"
public class B {
public B();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public static void main(java.lang.String[]);
Code:
0: new #7 // class A
3: dup
4: invokespecial #9 // Method A."<init>":()V
7: astore_1
8: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: bipush 10
14: invokevirtual #16 // Method A.f:(I)I
17: invokevirtual #20 // Method java/io/PrintStream.println:(I)V
20: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 20
}
以上为原始的终端输出,我分析后加上注释解释的版本如下:
Compiled from "B.java"
public class B {
//B的默认构造函数,在执行时主要完成一些初始化操作,包括一些成员变量的初始化赋值等操作
public B();
Code:
//0 加载索引为0的变量的值,也即this的引用,压入栈
0: aload_0
//1 出栈,调用java/lang/Object."<init>":()V 初始化对象
//就是this指定的对象的init()方法完成初始化
1: invokespecial #1
//return过程
4: return
//指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数
//第二个数字对应前面code中指令前面的数字
LineNumberTable:
line 7: 0
//main函数区域
public static void main(java.lang.String[]);
Code:
//0 new指令,创建一个A对象,new指令并不能完全创建一个对象,
// 只有在调用初始化方法完成后,对象才创建成功
0: new #7 // class A
//3 将操作数栈顶部的数据复制一份,并push进栈
3: dup
//4 pop出栈引用值,调用Method A."<init>":()V,完成对象的初始化
4: invokespecial #9
//7 pop出栈引用值,将其(引用)赋值给局部变量表中的变量a
7: astore_1
//8 准备java/lang/System.out:Ljava/io/PrintStream
8: getstatic #10
//11 a的引用值压入栈 准备作printstream
11: aload_1
//采用bipush指令将常量10压入栈中 以便A.f()调用
12: bipush 10
//14 引用出栈,调用A.f:(I)I
14: invokevirtual #16
//17 引用出栈。调用java/io/PrintStream.println:(I)V
17: invokevirtual #20
20: return
//指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数
//第二个数字对应前面code中指令前面的数字
LineNumberTable:
line 9: 0
line 10: 8
line 11: 20
}
分析所得与java源码中的操作吻合。
B的构造函数只需要调用A的构造即可,没有其他操作。
main函数中需要先创建变量a,这是语句A a = new A();
。
之后压入print函数,压入A.(),最后是常量10,这是执行语句System.out.println(a.f(10));