自制Java 虚拟机(一)解析class文件
来源:互联网 发布:全球发生交通事故数据 编辑:程序博客网 时间:2024/04/29 23:46
自制Java 虚拟机(一)解析class文件
一、认识class文件结构
一个.java
后缀的java源文件,经过javac
编译之后的字节码文件,结构如下:(摘自jvm虚拟机规范 version8)
ClassFile { u4 magic; // 魔数,值为 0xCAFEBABE,表示这是一个java class文件 u2 minor_version; // 次版本号 u2 major_version; // 主版本号 u2 constant_pool_count; // 等于constant_pool表中条目的数量+1 cp_info constant_pool[constant_pool_count-1]; // 常量池表,下标从1开始 u2 access_flags; // 该类或接口的访问限制标志 u2 this_class; // 表示该class文件定义的类或者接口,其值是常量池表中的索引,对应一个CONSTANT_Class_info结构 u2 super_class; // 表示该class文件定义的类/接口的父类/父接口,其值是常量池表中的索引,是一个CONSTANT_Class_info结构。特殊情况下,其值是0,表示该类没有父类(java.lang.Object) u2 interfaces_count; // 该类/接口的父类/接口的数量 u2 interfaces[interfaces_count]; // 该数组中的每一个值都是常量池中的索引, 对应一个CONSTANT_Class_info结构 u2 fields_count; // 该数值表示fields表中field_info的总数(由该类/接口声明的字段),包括类变量和实例变量 field_info fields[fields_count]; // 每个fields表中的一项是一个field_info结构,含有该字段完整的描述信息,不包括从父类或者父接口中继承而来的字段 u2 methods_count; // 该数值表示methods表中method_info的总数 method_info methods[methods_count]; // 每个methods中的一项是一个method_info结构,含有该阿方法的完整描述信息,如果该方法不是ACC_NATIVE或者ACC_ABSTRACT,还包含JVM指令(就是方法的代码) u2 attributes_count; // 该class文件表示的类/接口的attributes表中包含多少个attribute_info attribute_info attributes[attributes_count];}
其中u1、u2、u4分别表示1个字节、2个字节、4个字节。看了以上结构,很自然地想到可以用一个结构体来表示一个class文件,u1可以用unsigned char
类型,u2用unsigned short
类型,u4用unsigned int
类型。
不过由于cp_info、field_info、method_info、attribute_info是复合类型,光以上信息还不能够确定如何用C语言中的结构表示一个class文件,所以我们还得继续往下看:
1. 常量池中的主要结构
常量池是个关键,很多java指令都以索引的形式引用常量池中的符号信息。
一个常量池中的项目有如下通用结构:
cp_info { u1 tag; // 一个字节,表示常量的类型。 u1 info[]; // 该内容因tag的不同而不同}
表1:不同tag对应的常量池类型 摘自jvm虚拟机规范 version8
很自然,我们可以用define来定义这些常量
#define CONSTANT_Class 7#define CONSTANT_Fieldref 9...#define CONSTANT_InvokeDynamic 18
1.1 CONSTANT_Class_info类型
CONSTANT_Class_info表示一个类或接口(interface):
CONSTANT_Class_info { u1 tag; // 固定为7,即 CONSTANT_Class u2 name_index; // 其值是常量池中的一个索引,对应一个CONSTANT_Utf8_info结构}
自然,我们可以用C语言定义一个结构体来表示:
typedef struct _CONSTANT_Class_info { uchar tag; // 为了方便,已经 typedef unsigned char uchar; ushort name_index; // 为了方便,已经 typedef unsigned short ushort;}
1.2 CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info 类型
这三种类型有相似的结构:
.... { u1 tag; // u2 class_index; // 其值是常量池中的一个索引,对应一个 CONSTANT_Class_info结构。 u2 name_and_type_index; // 其值是常量池中的一个索引,对应一个CONSTANT_NameAndType_info结构}
于是我们可以用C语言定义如下结构:
typedef struct _CONSTANT_Fieldref_info { uchar tag; ushort class_index; ushort name_and_type_index; ushort findex; // 该field在对象中的索引,以后备用 uchar ftype; // 该field的类型,以后备用} CONSTANT_Fieldref_info;typedef struct _CONSTANT_Methodref_info { uchar tag; ushort class_index; ushort name_and_type_index; void* ref_addr; // method的地址,以后备用 ushort args_len; // 该方法的参数数码,以后备用} CONSTANT_Methodref_info;typedef CONSTANT_Methodref_info CONSTANT_InterfaceMethodref_info; // CONSTANT_InterfaceMethodref_info 暂时不涉及,故与CONSTANT_Methodref_info一样
1.3 CONSTANT_String_info
该类型表示java.lang.String类型的常量对象,结构如下:
CONSTANT_String_info { u1 tag; // 固定为8,表示一个CONSTANT_String u2 string_index; // 其值是常量池中的一个索引,对应一个CONSTANT_Utf8_info结构。实例化该String对象时的Unicode代码点序列。}
对应C的结构体如下:
typedef struct _CONSTANT_String_info { uchar tag; ushort string_index;} CONSTANT_String_info;
1.4 CONSTANT_Integer_info和CONSTANT_Float_info
这两个类型表示4个字节的数字常量,CONSTANT_Integer_info表示的是int型,CONSTANT_Float_info表示的是float型:结构如下:
... { u1 tag; // 类型标志,3 => CONSTANT_Integer,4 => CONSTANT_Float u4 bytes; // 以大端字节序存储的int或float的4个字节}
相应,我们可以定义如下C结构体:
typedef struct _CONSTANT_Integer_info { uchar tag; int value; // 该int型常量的值} CONSTANT_Integer_info;typedef struct _CONSTANT_Float_info { uchar tag; float value; // 该float型常量的值} CONSTANT_Float_info;
1.5 CONSTANT_Long_info和CONSTANT_Double_info
这两个类型表示8个字节的数字常量,CONSTANT_Long_info表示的是long型,CONSTANT_Double_info表示的是double型:结构如下:
... { u1 tag; // 类型标志: 5 => CONSTANT_Long, 6 => CONSTANT_Double u4 high_bytes; // 高四字节 u4 low_bytes; // 低四字节}
这里需要注意:如果常量池索引n是一个CONSTANT_Long_info或CONSTANT_Double_info类型的结构,那么常量池中下一个可用的索引是n+2,索引n+1必须有效但是不可用。 (这个有点奇怪,jvm规范中也说道让8字节常数占据两个常量池的位置是个糟糕的选择)
与CONSTANT_Integer_info和CONSTANT_Float_ifno类似,我们定义如下C结构体来存储这连个类型:
typedef struct _CONSTANT_Long_info { uchar tag; long value; // 该 long类型的值} CONSTANT_Long_info;typedef struct _CONSTANT_Double_info { uchar tag; double value; // 该dobule类型的值} CONSTANT_Double_info;
1.6 CONSTANT_NameAndType_info
该结构描述一个字段/方法的名称和类型信息:
CONSTANT_NameAndType_info { u1 tag; // 类型标志,固定为12,表示 CONSTANT_NameAndType u2 name_index; // 其值是常量池中的一个索引,对应一个 CONSTANT_Utf8_info结构,表示该字段或方法的名字。 u2 descriptor_index; // 其值是常量池中的一个索引,对应一个 CONSTANT_Utf8_info结构,表示该字段或方法的类型。}
对应定义如下C结构体:
typedef struct _CONSTANT_NameAndType_info { uchar tag; ushort name_index; ushort descriptor_index;} CONSTANT_NameAndType_info;
1.7 CONSTANT_Utf8_info
该结构估计是被提到次数最多的一个结构了,很多常量池中的结构都有个*_index的字段来指向这个结构。该结构表示一个常量字符串值,如下:
CONSTANT_Utf8_info { u1 tag; // 类型标志,固定为1,表示CONSTANT_Utf8 u2 length; // 字符串字节数 u1 bytes[length]; // 实际的字符串字节(不打算深入研究,详细请见jvm规范)}
我们可用定义如下C结构来存储:
typedef struct _CONSTANT_Utf8_info { uchar tag; ushort length; char *bytes; // C风格的字符串,最后一个以0结尾,注意要分配 length+1 个字节} CONSTANT_Utf8_info;
1.8 其它结构
常量池中剩下的其它几个几个:CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info。暂不打算研究,仅把它们的内容存起来就行,后面深入研究时再讨论。
以下是各自对应的C结构体:
typedef struct _CONSTANT_MethodHandle_info { uchar tag; uchar reference_kind; ushort reference_index;} CONSTANT_MethodHandle_info;typedef struct _CONSTANT_MethodType_info { uchar tag; ushort descriptor_index;} CONSTANT_MethodType_info;typedef struct _CONSTANT_InvokeDynamic_info { uchar tag; ushort bootstrap_method_attr_index; ushort name_and_type_index;} CONSTANT_InvokeDynamic_info;
到这里,常量池的各个结构都已经有了对应的C语言结构体来表示和存储,由于各个结构体大同小异,ClassFile中的constant_pool对应的cp_info类型用哪个结构来表示都不合适,我们就用泛型指针 void** 来表示。
typedef void** cp_info;
2. field_info 结构
每个字段由一个field_info结构来描述:
field_info { u2 access_flags; // 访问标志,ACC_PUBLIC(0x0001)、ACC_PRIVATE(0x0002)..... u2 name_index; // 其值是常量池中的一个索引,对应一个 CONSTANT_Utf8_info结构,表示该字段的名字。 u2 descriptor_index; // 其值是常量池中的一个索引,对应一个 CONSTANT_Utf8_info结构,表示该字段的描述。 u2 attributes_count; // 该字段的额外属性个数 attribute_info attributes[attributes_count]; // 属性表,是一个attribute_info结构}
又冒出个attribute_info结构!jvm规范中定义了20多个attribute_info结构类型,它们都有类似的结构:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length];}
当然目前我们不会每个attribute都取分析,不过既然知道了attribute的长度,我们可以一次性读取完,然后只分析我们关心的attribute。
我们定义如下结构来表示和存储attribute_info:
typedef struct _attribute_info{ ushort attribute_name_index; uint attribute_length; uchar *info;} attribute_info;
所以,field_info可定义如下:
typedef struct _field_info{ ushort access_flags; ushort name_index; ushort descriptor_index; ushort attributes_count; attribute_info **attributes; ushort findex; // 字段所以(留作以后用) uchar ftype; // 字段类型(留作以后用)} field_info;
3. method_info 结构
用来描述一个类/接口的方法:
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attribute_count; attribute_info attributes[attributes_count];}
与field_info的结构相似,不多说。定义如下C结构:
typedef struct _method_info{ ushort access_flags; ushort name_index; ushort descriptor_index; ushort attributes_count; attribute_info **attributes; void* code_attribute_addr; // address of code attribute,喜欢用泛型指针 ushort args_len; // 该方法的形式参数个数} method_info;
目前,对于method_info,我们关注以下属性:
- Code_attribute,包含该方法的代码以及一些辅助信息(如局部变量个数,最大操作数栈深度)
- LineNumberTable_attribute,虚拟机指令索引与源代码行号的对应表,该属性是可选的,用于调试,包含在Code_attribute中
相关结构可定义如下:
typedef struct _exception_table { ushort start_pc; ushort end_pc; ushort handler_pc; ushort catch_type;} exception_table;typedef struct _Code_attribute { ushort attribute_type; ushort max_stack; ushort max_locals; uint code_length; uchar *code; ushort exception_table_length; exception_table *exceptions; ushort attributes_count; attribute_info **attributes;} Code_attribute;typedef struct _line_number_table { ushort start_pc; ushort line_number;} line_number_table;typedef struct _LineNumberTable_attribute { ushort attribute_type; uint attribute_length; ushort table_length; line_number_table *tables;} LineNumberTable_attribute;
class文件中的主要结构已经描述完毕,我们可以定义class文件的表示了:
typedef struct _ClassFile{ uint magic; ushort minor_version; ushort major_version; ushort constant_pool_count; cp_info constant_pool; ushort access_flags; ushort this_class; ushort super_class; ushort interface_count; ushort *interfaces; ushort fields_count; field_info **fields; ushort methods_count; method_info **methods; ushort attributes_count; attribute_info **attributes; struct _ClassFile *parent_class; // 父类的ClassFile结构 int parent_fields_size; int fields_size; ushort static_field_size; char *static_fields;} ClassFile;typedef ClassFile Class;
二、辅助代码
从以上class文件的各个结构描述中,我们经常会需要读取一个字节、两个字节、4个字节、8个字节,以及指定长度的字节。对于ushort 、int、float、long、double类型的数字,我们还需要把大端字节序转换成小端字节序(因为java的class文件是用大端字节序表示,而笔者的cpu是小端字节序的)。这里的大小端转换很简单,仍然是顺序读取,只不过放置顺序是反过来放的。
大端顺序转成小端顺序读取的宏定义(有些递归的味道在里面):
#define READ_U1(fp, dest) fread(dest, 1, 1, fp)#define READ_U2(fp, dest) \READ_U1(fp, dest+1);\READ_U1(fp, dest)#define READ_U4(fp, dest) \READ_U2(fp, dest+2);\READ_U2(fp, dest)#define READ_U8(fp, dest) \READ_U4(fp, dest+4); \READ_U4(fp, dest)
然后,读取ushort、int、float等的函数可以定义如下:
ushort readUShort(FILE *fp){ uchar uc2[2]; READ_U2(fp, uc2); return *(ushort*)&uc2[0];}float readFloat(FILE *fp){ char uc4[4]; READ_U4(fp, uc4); return *(float*)&uc4;}uint readUInt(FILE *fp){ uchar uc4[4]; READ_U4(fp, uc4); return *(uint*)&uc4;}int readInt(FILE *fp){ char uc4[4]; READ_U4(fp, uc4); return *(int*)&uc4;}... // long和double省略
三、解析class文件
准备工作已经就绪,可以开始解析class文件 了。
代码大致如下:
Class* loadClass(const char *filename){ FILE *fp = fopen(filename, "rb"); if (!fp) { printf("Cannot open: %s\n", filename); return NULL; } Class *pclass = (Class*)malloc(sizeof(Class)); pclass->parent_class = NULL; // step 1: read magic number pclass->magic = readUInt(fp); printf("Magic: 0x%X\n", pclass->magic); if (pclass->magic != 0xCAFEBABE) { printf("Invalid class file!\n"); exit(1); } // step2: read version pclass->minor_version = readUShort(fp); pclass->major_version = readUShort(fp); printf("minor_version: %d\n", pclass->minor_version); printf("major_version: %d\n", pclass->major_version); printf("--------------------------------------------\n"); // step3: read constant pool parseConstantPool(fp, pclass); printf("constant_pool_count: %d\n", pclass->constant_pool_count); showConstantPool(pclass); printf("--------------------------------------------\n"); // step4: read access_flags pclass->access_flags = readUShort(fp); printf("access_flag: %04X\t%s\n", pclass->access_flags, formatAccessFlag(pclass->access_flags)); // step5: this class pclass->this_class = readUShort(fp); printf("this_class: #%d\t%s\n", pclass->this_class, get_class_name(pclass->constant_pool, pclass->this_class)); // step6: super class pclass->super_class = readUShort(fp); if (pclass->super_class > 0) { printf("super_class: #%d\t%s\n", pclass->super_class, get_class_name(pclass->constant_pool, pclass->super_class)); } printf("--------------------------------------------\n"); // step7: read inerfaces parseInterface(fp, pclass); printf("interface_count: %d\n", pclass->interface_count); showInterface(pclass); printf("--------------------------------------------\n"); // step8: read fields parseFields(fp, pclass); printf("fields_count: %d\n", pclass->fields_count); showFields(pclass); printf("--------------------------------------------\n"); // step9: read methods parseMethods(fp, pclass); printf("methods_count: %d\n", pclass->methods_count); showMethods(pclass); printf("--------------------------------------------\n"); // step10: read attributes parseAttributes(fp, pclass); printf("attributes_count: %d\n", pclass->attributes_count); showAttributes(pclass, pclass->attributes, pclass->attributes_count); //setThisClassFieldIndex(pclass); ((CONSTANT_Class_info*)(pclass->constant_pool[pclass->this_class]))->pclass = pclass; return pclass;}
由于代码太多,涉及的函数就不一一列出了。反正就是“逢山开路,遇水搭桥”,按照jvm规范中的描述来,该怎么办怎么办。
四、测试
Hello.java
package test;public class Hello implements IMath{ static double C_DOUBLE = 12.45; public int xi = 789; protected long xl = 35; public float xf = -235.125f; public double db = 32.5; private int priv_i = 2; public int sum(int x, int y) { int s = 0; for(int i=x;i<=y;i++) { s+=i; } s += sub(x,y); return s+xi + this.priv_i; } private int sub(int x, int y) { return x-y; }}interface IMath { public int sum(int x, int y);}
用javac工具编译成字节码:
javac Hello.java
然后解析Hello.class文件,输出如下(与javap的输出对比):
常量池的输出对比
代码属性的输出对比
可见解析OK。
- 自制Java 虚拟机(一)解析class文件
- 深入java虚拟机--Class文件实例解析
- 深入Java虚拟机:Class文件实例解析
- Java虚拟机(一)-Java的class文件详解
- java class 文件解析 之 class 常量池 (一)
- Java虚拟机--Class文件结构(十七)
- Java Class 文件解析
- java class文件解析
- JAVA CLASS 文件解析
- Java虚拟机之Class文件
- Java虚拟机和Class文件
- 自制Java虚拟机-总结
- Class文件解析一:概述
- Java字节码(class文件)解析
- 深入java虚拟机(二)--Class类文件结构
- 理解Java虚拟机(2)之.class文件加载过程
- Java class文件读书摘要(一)
- 解析java的*.class文件
- OPENCV入门教程三:cvtColor彩色图转灰度图
- 凯撒密码
- List-ArrayList
- Java 多线程
- 面向对象-继承中的面试题:代码块
- 自制Java 虚拟机(一)解析class文件
- SecureCRT使用VBS脚本自动备份网络设备配置,目前支持部分H3C及dptech
- 6月1日,每日20行。
- 《UNIX网络编程 卷1》 笔记: 互斥锁与条件变量
- Css之双列自适应布局
- Spring Cloud Bus 教程
- 关卡流
- 2017年第0届浙江工业大学之江学院程序设计竞赛决赛—K
- maven仓库更新太慢