深入理解Class文件-2
在深入理解Class文件-1这篇文章中,大致了解了Class文件的结构,在平时的使用中,我一般都倾向于使用工具来解析并查看class文件,如jdk自带 javap 程序,使用:
javap -c class文件所在路径 。
平时我个人使用IDEA开发,一般使用 <jclasslib Bytecode Viewer> 这个插件查看。
作为一名程序员,为了更深入的了解class文件,靠看是不够的,如果能够自己手写解析class文件的代码,并输出其中的内容,才是学会了class文件的结构。
在开始之前,放一个官方文档的链接
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.7
遇到不懂,先看官方文档。
待解析的类
类
package temp;
import temp.*;
public class TestClassFile extends TestSuperClass implements TestInterface{
public static String str = "felix.zhu";
@Override
public void debug() {
System.out.println("debug");
}
public static void main(String[] args) {
TestClassFile tf = new TestClassFile();
Runtime.getRuntime().totalMemory();
}
}
超类
package temp;
public class TestSuperClass{
private int value;
public int getValue() {
return this.value;
}
}
接口
package temp;
interface TestInterface {
public void debug();
}
将类编译成class文件,放到一个指定的目录
main() 解析方法
目前只解析到接口名称,其他的留到后续的文章
File file = new File("D:\\temp\\TestClassFile.class");
DataInput in = getInputStream(file);
ClassFileReader classFileReader = new ClassFileReader();
//读取魔数
classFileReader.readMagic(in);
//读取版本号
classFileReader.readVersion(in);
//读取常量池
classFileReader.readConstansPool(in);
//读取访问标识
classFileReader.readAccessFlag(in);
//读取class名称
classFileReader.readClass(in);
//读取superclass
classFileReader.readSuperClass(in);
//读取接口名称
classFileReader.readInterfaces(in);
getInputString()
public static DataInput getInputStream(File file) throws FileNotFoundException {
return new DataInputStream(
new BufferedInputStream(
new FileInputStream(file)));
}
由于考虑到字节操作,这里选择DataInputStream,它提供了readInt, readUnsignedShort 等等方法正好适合,尤其喜欢它有一个readUTF函数,由于java的字符串是以Unicode的形式存储的,字符串会经过UTF-8 encode写入到class文件中,读取出来后需要转成可读的字符串就需要写decodeUTF-8函数,这里懒得写,就用现成的了。
读取魔数readMagic()
class文件开头四个字节是魔数,我们用readInt读取出来,在java中Int占4个字节,最后转换成16进制就可以看到啦。
public void readMagic(DataInput input) throws IOException {
//魔数占4个字节
int magicNumb = input.readInt();
print("magic : " + Integer.toHexString(magicNumb));
}
读取版本号readVersion()
public void readVersion(DataInput input) throws IOException {
int minorVersion = input.readUnsignedShort();
int majorVersion = input.readUnsignedShort();
//根据文档描述 实际是 如果两个值都有,最终的版本是 majorVersion.minorVersion
print("minorVersion: " + minorVersion + " majorVersion: " + majorVersion);
String classVersion = getMajorVersionName(majorVersion);
print("当前class文件版本号:" + classVersion);
}
根据文档描述读取出来的版本号最小从45开始,为了方便阅读,写一个版本号转换的函数
public String getMajorVersionName(int majorVersion) {
switch (majorVersion) {
case 45:
return "1.1";
case 46:
return "1.2";
case 47:
return "1.3";
case 48:
return "1.4";
case 49:
return "1.5";
case 50:
return "1.6";
case 51:
return "1.7";
case 52:
return "1.8";
case 53:
return "1.9";
default:
return "unknown value " + majorVersion;
}
}
读取常量池readConstansPool()
public void readConstansPool(DataInput input) throws IOException {
//开头两个字节是常量池的大小
int poolCount = input.readUnsignedShort();
indexItems = new int[poolCount]; //常量池index索引数组
strings = new String[poolCount];
//根据文档描述 The constant_pool table is indexed from 1 to constant_pool_count-1.
for (int i = 1; i < poolCount; ++i) {
byte tag = input.readByte();
switch (tag) {
case FIELD:
case METH:
case IMETH:
case INT:
case FLOAT:
case NAME_TYPE:
input.skipBytes(4);
break;
case CLASS:
indexItems[i] = input.readUnsignedShort();
break;
case LONG:
case DOUBLE:
input.skipBytes(8);
break;
case UTF8:
strings[i] = input.readUTF();
break;
default:
input.skipBytes(2);
break;
}
}
}
1 根据文档的描述常量池下标从1开始,i初始化为1
2 indexItems 数组理解成存储对应常量池数据的下标,后面读取的数据都是根据下标到常量池里找数据。
3 strings 用来存储对应下标的String值
4 文档描述常量池里的类型如下:
image.png
常量池里的每个元素都有可能是这些类型当中的其中一个。
这里用CONSTANT_Class 举例
image.png
可以看到开头是一个字节(u1) 的tag,对应上图。
程序里的switch语句是根据读取到的tag做相应的处理,我定义了全局变量
static final int CLASS = 7;
接着u2 name_index表示该class全限定名在常量池的下标。我存到strings这个数组里了。
其他的类型可以自行参考官方文档,我这里用不到这些类型,所以用skipBytes方法跳过。
class文件里的字符串是CONSTANT_Utf8_info结构的,如下图
image.png
readUTF方法会先读取length,在根据length读取字节数组转换成String。
readAccessFlag() 读取访问标识
public void readAccessFlag(DataInput input) throws IOException {
int flag = input.readUnsignedShort();
List<String> flagNames = getFlagsByName(flag);
print("flags: " + String.join(",", flagNames));
}
将flag转成Name
public List<String> getFlagsByName(int flags) {
int cpFlags = flags;
List<String> flagsNames = new ArrayList<>();
for (AccessFlag classAccessFlag : CLASS_ACCESS_FLAGS) {
//通过 & 操作,如果存在,这添加
if((cpFlags & classAccessFlag.getFlag()) != 0) {
flagsNames.add(flagToName(classAccessFlag));
flags = ~classAccessFlag.getFlag() & flags;
}
}
return flagsNames;
}
public static String flagToName(AccessFlag flag) {
switch (flag) {
case ACC_PUBLIC:
return "ACC_PUBLIC";
case ACC_FINAL:
return "ACC_FINAL";
case ACC_SUPER:
return "ACC_SUPER";
case ACC_INTERFACE:
return "ACC_INTERFACE";
case ACC_ABSTRACT:
return "ACC_ABSTRACT";
case ACC_SYNTHETIC:
return "ACC_SYNTHETIC";
case ACC_ANNOTATION:
return "ACC_ANNOTATION";
case ACC_ENUM:
return "ACC_ENUM";
default:
return null;
}
}
class类的修饰,如果是public final class, 读出来的值就是 0x0011
image.png定义一个枚举表示它
enum AccessFlag{
ACC_PUBLIC(0x0001, "public"),
ACC_FINAL(0x0010, "final"),
ACC_SUPER(0x0020, "Treat superclass methods"),
ACC_INTERFACE(0x0200, "interface"),
ACC_ABSTRACT(0x0400, "abstract"),
ACC_SYNTHETIC(0x1000, "synthetic"),
ACC_ANNOTATION(0x2000, "annotation"),
ACC_ENUM(0x4000, "enum");
AccessFlag(int flag, String name) {
this.flag = flag;
this.name = name;
}
private int flag;
private String name;
public int getFlag() {
return flag;
}
public String getName() {
return name;
}
}
readClass() 读取class名称
public void readClass(DataInput input) throws IOException {
// classInfo结构在常量池的位置
int classIndex = input.readUnsignedShort();
//class全名称字符串在常量池的位置
int stringIndex = indexItems[classIndex];
String className = strings[stringIndex];
print("class Name: " + className);
}
这里就用到根据常量池解析出来的indexItems数组和strings数组了
readSuperClass() 超类
public void readSuperClass(DataInput input) throws IOException {
int superClassIndex = input.readUnsignedShort();
int stringIndex = indexItems[superClassIndex];
String superClassName = strings[stringIndex];
print("superClass Name: " + superClassName);
}
和读取class名称一样
readInterfaces() 接口
public void readInterfaces(DataInput input) throws IOException {
//有效索引范围为0~interfaces_count
int interfaceCount = input.readUnsignedShort();
String[] interfaces = new String[interfaceCount];
for(int i = 0; i < interfaceCount; i++) {
int interfaceIndex = input.readUnsignedShort();
int stringIndex = indexItems[interfaceIndex];
String interfaceName = strings[stringIndex];
interfaces[i] = interfaceName;
}
print("interfaces Name: " + String.join(",",interfaces));
}
java接口是多继承的,可能会有多个,class文件里是数组
最后的结果
image.png结尾
可以看到,虽然看class文件格式觉得自己理解了,但是解析class文件还是比较复杂的,通过自己动手解析class文件,能加深对class文件的理解,如果你有其他更好的解析class文件的方法,欢迎加入一起探讨。
我是左手落花,一个喜欢通过coding来探究原理的人。