深入class文件结构
JVM指令集简介
首先将下面这段java代码对应编译成的class文件转化为Oolong语言后进行分析:
1 2 3 4 5
| public class Message { public static void main(String[] args) { System.out.println("Hello World!"); } }
|
得到的.j后缀文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| .source Message.java .class public super org/example/test/Message .super java/lang/Object
.method public <init> ()V .limit stack 1 .limit locals 1 .var 0 is this Lorg/example/test/Message; from l0 to l5 .line 3 l0: aload_0 l1: invokespecial java/lang/Object/<init> ()V l4: return
.end method
.method public static main ([Ljava/lang/String;)V .limit stack 2 .limit locals 1 .var 0 is args [Ljava/lang/String; from l0 to l9 .line 5 l0: getstatic java/lang/System/out Ljava/io/PrintStream; l3: ldc "Hello World!" l5: invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V .line 6 l8: return
.end method
|
与类相关的指令
.source Message.java:表示这个代码的源文件是Message.java
.class public super org/example/test/Message:表示这个是公共类Message
.super java/lang/Object:表示这个类的父类是Object
方法的定义
.method public ()V:表示这是一个公共方法,没有参数,返回值类型是V(Void),表示是构造函数
.method public static main ([Ljava/lang/String;)V:表示定义一个main方法,参数是String类型的数组,L表示的是一个类形式,凡是L表示的类后面都会以;结尾,表示这个类的结束
其余指令解释
.limit stack 1
:设置该方法的操作数栈的最大深度为1。
.limit locals 1
:设置该方法的局部变量表的最大槽位数为1。
.var 0 is this Lorg/example/test/Message; from l0 to l5
:定义一个局部变量,槽位为0,名称为this
,类型为Lorg/example/test/Message;
,作用范围从标签l0
到标签l5
。
.line 3
:指定源代码行号为3。
l0: aload_0
:将局部变量0(即this
)加载到操作数栈中。
l1: invokespecial java/lang/Object/<init> ()V
:调用父类java/lang/Object
的构造函数。
l4: return
:从构造函数中返回。
.end method
:结束方法定义。
.limit stack 2
:设置该方法的操作数栈的最大深度为2。
.limit locals 1
:设置该方法的局部变量表的最大槽位数为1。
.var 0 is args [Ljava/lang/String; from l0 to l9
:定义一个局部变量,槽位为0,名称为args
,类型为[Ljava/lang/String;
,作用范围从标签l0
到标签l9
。
.line 5
:指定源代码行号为5。
l0: getstatic java/lang/System/out Ljava/io/PrintStream;
:获取System.out
静态字段的值(即标准输出流)。
l3: ldc "Hello World!"
:将字符串常量”Hello World!”加载到操作数栈中。
l5: invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V
:调用PrintStream
类的println
方法,将操作数栈顶的字符串打印到标准输出。
.line 6
:指定源代码行号为6。
l8: return
:从main
方法返回。
.end method
:结束方法定义。
class文件头的表示形式
由于篇幅过长,这个二进制字节码将在后续的分析中拆分展示
使用java COM.sootNsmoke.oolong.Dumpclass Message.class
命令,可以生成有字节码解释形式的内容,其中实际上只有第二列是原始的字节码内容,第一列和第三列都是生成的解释性内容
首先看一下文件的头部信息,有三行:
1 2 3
| 000000 cafebabe magic = ca fe ba be 000004 0000 minor version = 0 000006 0034 major version = 52
|
第一行是一个标识符,表示这个文件是一个标准的class文件,它是一个32位的无符号整数,cafebabe是这个整数的16进制表示形式,如果一个文件的前4个字节是这个数字,则表示这个文件是一个class文件,否则JVM将不会加载
后面两个字节分别表示最大版本和最小的版本范围
常量池
文件头信息后面是常量池的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| 000008 0022 34 constants 00000a 0a00060014 1. Methodref class #6 name-and-type #20 00000f 0900150016 2. Fieldref class #21 name-and-type #22 000014 080017 3. String #23 000017 0a00180019 4. Methodref class #24 name-and-type #25 00001c 07001a 5. Class name #26 00001f 07001b 6. Class name #27 000022 010006 7. UTF length=6 000025 3c696e69743e <init> 00002b 010003 8. UTF length=3 00002e 282956 ()V 000031 010004 9. UTF length=4 000034 436f6465 Code 000038 01000f 10. UTF length=15 00003b 4c696e654e756d6265725461626c65 LineNumberTable 00004a 010012 11. UTF length=18 00004d 4c6f63616c5661726961626c65546162 LocalVariableTab 00005d 6c65 le 00005f 010004 12. UTF length=4 000062 74686973 this 000066 01001a 13. UTF length=26 000069 4c6f72672f6578616d706c652f746573 Lorg/example/tes 000079 742f4d6573736167653b t/Message; 000083 010004 14. UTF length=4 000086 6d61696e main 00008a 010016 15. UTF length=22 00008d 285b4c6a6176612f6c616e672f537472 ([Ljava/lang/Str 00009d 696e673b2956 ing;)V 0000a3 010004 16. UTF length=4 0000a6 61726773 args 0000aa 010013 17. UTF length=19 0000ad 5b4c6a6176612f6c616e672f53747269 [Ljava/lang/Stri 0000bd 6e673b ng; 0000c0 01000a 18. UTF length=10 0000c3 536f7572636546696c65 SourceFile 0000cd 01000c 19. UTF length=12 0000d0 4d6573736167652e6a617661 Message.java 0000dc 0c00070008 20. NameAndType name #7 descriptor #8 0000e1 07001c 21. Class name #28 0000e4 0c001d001e 22. NameAndType name #29 descriptor #30 0000e9 01000c 23. UTF length=12 0000ec 48656c6c6f20576f726c6421 Hello World! 0000f8 07001f 24. Class name #31 0000fb 0c00200021 25. NameAndType name #32 descriptor #33 000100 010018 26. UTF length=24 000103 6f72672f6578616d706c652f74657374 org/example/test 000113 2f4d657373616765 /Message 00011b 010010 27. UTF length=16 00011e 6a6176612f6c616e672f4f626a656374 java/lang/Object 00012e 010010 28. UTF length=16 000131 6a6176612f6c616e672f53797374656d java/lang/System 000141 010003 29. UTF length=3 000144 6f7574 out 000147 010015 30. UTF length=21 00014a 4c6a6176612f696f2f5072696e745374 Ljava/io/PrintSt 00015a 7265616d3b ream; 00015f 010013 31. UTF length=19 000162 6a6176612f696f2f5072696e74537472 java/io/PrintStr 000172 65616d eam 000175 010007 32. UTF length=7 000178 7072696e746c6e println 00017f 010015 33. UTF length=21 000182 284c6a6176612f6c616e672f53747269 (Ljava/lang/Stri 000192 6e673b2956 ng;)V
|
第一行有两个字节表示的是在该类中含有的常量总数总共有34个,含有一个0是保留常量
这个34个常量都是由三个字节来描述的,分别是一个字节的常量类型和两个字节的常量池索引地址
UTF8常量类型
UTF8是一种字符编码的格式,可以存储多个字节长度的字符串值,如可以存储类名或者方法名等很长的一个字符串
从左向右看,UTF8类型的常量最后两个字节来表示后面所存储的字符串的总字节数,如第32个常量:010007(十六进制),其中0007表示后面所跟的常量实际内容的字节长度为7,即总共由7个字节:7072696e746c6e(十六进制字符串),表示的实际内容是:println
第一个字节01表示UTF8常量类型对应的类型码
因此,UTF8类型的常量格式如下:
一个字节表示的UTF8常量类型 + 两个字节表示的该常量字节长度 + 该常量的具体内容
Fieldref、Methodref常量类型
这两个常量类型是为了描述Class中的属性项和方法
如常量2:0900150016,就是一个Fieldref类型的常量,总共五个字节
- 第一个字节 09 表示Fieldref常量的类型
- 第二个和第三个字节表示该Fieldref是哪个类中的Field,存储的值是第几个常量的位置,0015表示第21个常量(Class类型常量)
- 最后两个字节表示这个Fieldref常量的Name和Type,指向NameAndType类型常量的索引,即0016表示的第22个常量
Methodref类常量类似,如常量1:0a00060014
- 第一个字节0a表示常量类型10
- 第二个和第三个字节表示该方法属于哪个类,指向特定的常量
- 最后两个字节表示该方法的NameAndType
Class常量类型
该常量表示该类的名称,会指向另一个UTF8类型的常量,这个UTF8类型常量存储了这个类具体的名称
如常量5:07001a:
- 第一个字节表示Class常量的类型
- 后两个字节表示指向第26个常量,该常量是UTF8类型,常量为010018,存储的是org/example/test/Message,即该类的名称
NameAndType常量类型
如常量25:0c00200021:
- 第一个字节表示NameAndType类型的常量
- 后面两个字节表示指向Name的UTF8类型常量索引,0020表示第32个常量println,表示属性项名称(对于Fieldref)或者方法名称(对于Methodref)
- 最后两个字节指向Type的UTF8类型常量索引,0021表示第33个常量(Ljava/lang/String;)V,表示参数类型(对于Fieldref)或者返回类型(对于Methodref)
类信息
常量列表后面是关于这个类本身信息的描述,例如这个类的访问控制、名称和类型,以及是否由父类或者是否实现了某些接口等信息
如:
1 2 3 4
| 000197 0021 access_flags = 33 000199 0005 this = #5 00019b 0006 super = #6 00019d 0000 0 interfaces
|
0021两个字节表示这个类的访问控制描述
实际上这两个字节只用了5个bit,其它的都是0(没有定义),将0021翻译成二进制:0000 0000 0010 0001,加粗的为真正用到的bit位(从右到左)
- 第1个bit表示该类是否是public的,为1是public类型,否则为private,因此类的访问修饰只有两种
- 第5个bit表示该类是否是final,1为是,0为否
- 第6个bit表示该类是否含有invokespecial,也就是是否继承其他类,所有类都默认继承了Object
- 第10个bit表示该类是否是抽象类,0为不是,1为是
0005两个字节是该类的名称,指向第5个常量
0006两个字节表示该类的父类名称,指向第6个常量
0000两个字节表示该类没有实现接口
Fields和Methods定义
类信息描述后面是每个Fields和Methods的具体定义
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| 00019f 0000 0 fields 0001a1 0002 2 methods Method 0: 0001a3 0001 access flags = 1 0001a5 0007 name = #7<<init>> 0001a7 0008 descriptor = #8<()V> 0001a9 0001 1 field/method attributes: field/method attribute 0 0001ab 0009 name = #9<Code> 0001ad 0000002f length = 47 0001b1 0001 max stack: 1 0001b3 0001 max locals: 1 0001b5 00000005 code length: 5 0001b9 2a 0 aload_0 0001ba b70001 1 invokespecial #1 0001bd b1 4 return 0001be 0000 0 exception table entries: 0001c0 0002 2 code attributes: code attribute 0: 0001c2 000a name = #10<LineNumberTable> 0001c4 00000006 length = 6 Line number table: 0001c8 0001 length = 1 0001ca 00000003 start pc: 0 line number: 3 code attribute 1: 0001ce 000b name = #11<LocalVariableTable> 0001d0 0000000a length = 10 0001d4 0001 local_variable_table length: 1 0001d6 0000 start pc: 0 0001d8 0005 length: 5 0001da 000c name_index: 12 0001dc 000d descriptor_index: 13 0001de 0000 index: 0 Method 1: 0001e0 0009 access flags = 9 0001e2 000e name = #14<main> 0001e4 000f descriptor = #15<([Ljava/lang/String;)V> 0001e6 0001 1 field/method attributes: field/method attribute 0 0001e8 0009 name = #9<Code> 0001ea 00000037 length = 55 0001ee 0002 max stack: 2 0001f0 0001 max locals: 1 0001f2 00000009 code length: 9 0001f6 b20002 0 getstatic #2 0001f9 1203 3 ldc #3 0001fb b60004 5 invokevirtual #4 0001fe b1 8 return 0001ff 0000 0 exception table entries: 000201 0002 2 code attributes: code attribute 0: 000203 000a name = #10<LineNumberTable> 000205 0000000a length = 10 Line number table: 000209 0002 length = 2 00020b 00000005 start pc: 0 line number: 5 00020f 00080006 start pc: 8 line number: 6 code attribute 1: 000213 000b name = #11<LocalVariableTable> 000215 0000000a length = 10 000219 0001 local_variable_table length: 1 00021b 0000 start pc: 0 00021d 0009 length: 9 00021f 0010 name_index: 16 000221 0011 descriptor_index: 17 000223 0000 index: 0
|
前四个字节分别表示在该类中定义了多少个属性和方法,后面是这些属性和方法的具体定义
方法和属性与类应用也有访问控制,具体定义的前两个字节即定义了访问控制:0000 x00x xxxx xxxx
- 第1个bit表示是否public
- 第2个bit表示是否private
- 第3个bit表示是否protected
- 第4个bit表示是否static
- 第5个bit表示有没有被final定义
- 第6个bit表示是否被synchronized定义
- 第7个bit表示是否被volatile定义
- 第8个bit表示是否被transient定义
- 第9个bit表示是否native方法
- 第12个bit表示是否abstract方法
后面四个字节0007 0008定义了这个方法的NameAndType,分别指向两个UTF8常量类型
0000002f表示这个方法的代码长度,编译后的字节码长度是47个字节,这里表示长度用了四个字节,按照这个长度的定义来说是最长可以2^32个字节,也就是4GB的长度,但是实际上整个Jvaa源码长度只有64K的字节长度可以表示。这里的64K长度不是源码的方法长度,而是编译后的字节码长度
后面还描述了这个方法的最大栈深度以及本地常量的最大个数、方法定义抛出的异常等信息
最后是方法的附加信息描述,描述了代码本身之外的额外信息,如调试信息等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 0001c0 0002 2 code attributes: code attribute 0: 0001c2 000a name = #10<LineNumberTable> 0001c4 00000006 length = 6 Line number table: 0001c8 0001 length = 1 0001ca 00000003 start pc: 0 line number: 3 code attribute 1: 0001ce 000b name = #11<LocalVariableTable> 0001d0 0000000a length = 10 0001d4 0001 local_variable_table length: 1 0001d6 0000 start pc: 0 0001d8 0005 length: 5 0001da 000c name_index: 12 0001dc 000d descriptor_index: 13 0001de 0000 index: 0
|
类属性描述
字节码的最后是Class的附加属性描述:
1 2 3 4 5
| 000225 0001 1 classfile attributes Attribute 0: 000227 0012 name = #18<SourceFile> 000229 00000002 length = 2 00022d 0013 sourcefile index = #19
|