Java字节码文件解读

来源:互联网 发布:安广网络宽带怎么样 编辑:程序博客网 时间:2024/05/22 06:52

前言

对于如下的Java代码(Demo.java):

package cn.bjut;public class Demo {    private int x = 10;    public void testMethod() {    }}

经过javac 编译,生成字节码文件Demo.class
Demo.class

它对应10个部分:
Magic Number: 0xCAFEBABE
Version of Class File Format: the minor and major versions of the class file
Constant Pool: Pool of constants for the class
Access Flags: for example whether the class is abstract, static, etc.
This Class: The name of the current class
Super Class: The name of the super class
Interfaces: Any interfaces in the class
Fields: Any fields in the class
Methods: Any methods in the class
Attributes: Any attributes of the class (for example the name of the sourcefile, etc.)

java字节码总览图
这张图是一张java字节码的总览图,我们也就是按照上面的顺序来对字节码进行解读的。


魔数

*.class字节码文件最开始的4个字节表示的是魔数,对应我们Demo.class的是 0xCAFEBABE
什么是魔数?
魔数是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0xCAFEBABE表示的是class文件。


版本号

版本号包含主版本号和次版本号,都是各占2个字节。
在此Demo.class中为0x00000034。其中前面的两个字节0000是次版本号,后面的两个字节0034是主版本号。通过进制转换得到的是次版本号为0,主版本号为52(16进制(0x34)转10进制(52))。
从oracle官方网站我们能够知道,52对应的是jdk1.8,而其次版本为0,所以该文件的版本为1.8.0。如果需要验证,可以在用javac --version命令输出版本号,或者修改编译目标版本-target重新编译,查看编译后的字节码文件版本号是否做了相应的修改。

Java SE 9 = 53 (0x35 hex)Java SE 8 = 52 (0x34 hex)Java SE 7 = 51 (0x33 hex)Java SE 6.0 = 50 (0x32 hex)Java SE 5.0 = 49 (0x31 hex)JDK 1.4 = 48 (0x30 hex)JDK 1.3 = 47 (0x2F hex)JDK 1.2 = 46 (0x2E hex)JDK 1.1 = 45 (0x2D hex)

常量池

紧接着主版本号之后的就是常量池入口。常量池是Class文件中的资源仓库,在接下来的内容中我们会发现很多地方会涉及到常量池,如Class Name,Interfaces等。常量池中主要存储2大类常量:字面常量和符号引用。字面常量如文本字符串,java中声明为final的常量值等等;而符号引用如类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

为什么需要类和接口的全限定名呢?系统引用类或者接口的时候不是通过内存地址进行操作吗?这里大家仔细想想,java虚拟机在没有将类加载到内存的时候根本都没有分配内存地址,也就不存在对内存的操作,所以java虚拟机首先需要将类加载到虚拟机中,那么这个过程设计对类的定位(需要加载A包下的B类,不能加载到别的包下面的别的类中),所以需要通过全局限定名来判别唯一性。这就是为什么叫做全局限定的意思,也就是唯一性。

在进行具体常量池分析之前,我们先来了解一下常量池的项目类型表:
常量类型表
上面的表中描述了11中数据类型,其实在jdk1.7之后又增加了3种(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。这样算起来一共是14种。接下来我们按照Demo的字节码进行逐一翻译。

0x0012:由于常量池的数量不固定(n+2),所以需要在常量池的入口处放置一项u2类型的数据代表常量池数量。因此该16进制是12,表示有18项常量,索引范围为1~17。明明是有18项,为何索引范围是1~17呢?因为Class文件格式规定,设计者就讲第0项保留出来了,以备后患。从这里我们知道接下来我们需要翻译出17项常量。

常量池

#01

0x0A:从常量类型表中我们发现,第一个数据均是u1类型的tag,16进制的0A是十进制的10,对应表中的CONSTANT_Methodref_info。
0x0004:CONSTANT_Class_info索引项 #4
0x000E:CONSTANT_NameAndType索引项 #14

#02

0x09:CONSTANT_Fieldref_info
0x0003:CONSTANT_Class_info索引项 #3
0x000F:CONSTANT_NameAndType索引项 #15

#03

0x07:CONSTANT_Class_info
0x0010:全局限定名常量索引为 #16

#04

0x07:CONSTANT_Class_info
0x0011:全局限定名常量索引为 #17

#05

0x01:CONSTANT_Utf-8_info
0x0001:UTF-8编码的字符串长度为1
0x78:”x”(十六进制转ASCII字符)

#06

0x01:CONSTANT_Utf-8_info
0x0001:UTF-8编码的字符串长度为1
0x49:”I”

#07

0x01:CONSTANT_Utf-8_info
0x0006:UTF-8编码的字符串长度为6
0x3C-69-6E-69-74-3E:”<init>

#08

0x01:CONSTANT_Utf-8_info
0x0003:UTF-8编码的字符串长度为3
0x28-29-56:”()V”

#09

0x01:CONSTANT_Utf-8_info
0x0004:UTF-8编码的字符串长度为4
0x43-6F-64-65:”Code”

#10

0x01:CONSTANT_Utf-8_info
0x000F:UTF-8编码的字符串长度为15
0x4C-69-6E-65-4E-75-6D-62-65-72-54-61-62-6C-65:”LineNumberTable”

#11

0x01:CONSTANT_Utf-8_info
0x000A:UTF-8编码的字符串长度为10
0x74-65-73-74-4d-65-74-68-6f-64:”testMethod”

#12

0x01:CONSTANT_Utf-8_info
0x000A:UTF-8编码的字符串长度为10
0x53-6F-75-72-63-65-46-69-6C-65:”SourceFile”

#13

0x01:CONSTANT_Utf-8_info
0x0009:UTF-8编码的字符串长度为9
0x44-65-6D-6F-2E-6A-61-76-61:”Demo.java”

#14

0x0C:CONSTANT_NameAndType_info
0x0007:字段或者方法名称常量项索引 #7
0x0008:字段或者方法描述符名称常量项索引 #8

#15

0x0C:CONSTANT_NameAndType_info
0x0005:字段或者方法名称常量项索引 #5
0x0006:字段或者方法描述符名称常量项索引 #6

#16

0x01:CONSTANT_Utf-8_info
0x000C:UTF-8编码的字符串长度为12
0x63-6E-2F-62-6A-75-74-2F-44-65-6D-6F:”cn/bjut/Demo”

#17

0x01:CONSTANT_Utf-8_info
0x0010:UTF-8编码的字符串长度为16
0x6A-61-76-61-2F-6C-61-6E-67-2F-4F-62-6A-65-63-74:”java/lang/Object”


Access_Flag 访问标志

访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。通过上面的源代码,我们知道该文件是类并且是public。
Class access and property modifiers
0x0021:是0x0020和0x0001的并集。


类索引

类索引用于确定类的全限定名
0x0003 表示引用第3个常量,同时第3个常量引用第16个常量,查找得”cn/bjut/Demo”。#3 -> #16
类索引


父类索引

0x0004 同理:#4 -> #17(java/lang/Object)
父类索引


接口索引

通过 java字节码总览图 我们知道,这个接口有2+n个字节,前两个字节表示的是接口数量,后面跟着就是接口的列表。我们这个类没有任何接口,所以应该是0000。果不其然,查找字节码文件得到的就是0000。
接口索引


字段表集合

字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
同样,接下来就是2+n个字段属性。我们只有一个属性x,按道理应该是0001。查找文件果不其然是0001。
那么接下来我们要针对这样的字段进行解析。附上字段表结构图

字段表结构

0x0002: 访问标志为private(ACC_PRIVATE)
0x0005: 字段名称索引为#5,对应的是”x”
0x0006: 描述符索引为#6,对应的是”I”
0x0000: 属性表数量为0,因此没有属性表


方法

methods
我们只有一个方法testMethod,按照道理应该前2个字节是0001。通过查找发现是0x0002。这是什么原因,这代表着有2个方法呢?且继续看……
方法表结构
上图是一张方法表结构图,按照这个图我们分析下面的字节码:

第1个方法

0x0001:访问标志 ACC_PUBLIC,表明该方法是public。(可自行搜索方法访问标志表)
0x0007:方法名索引为#7,对应的是”<init>
0x0008:方法描述符索引为#8,对应的是”()V”
0x0001:属性表数量为1(一个属性表)
那么这里涉及到了属性表。什么是属性表呢?可以这么理解,它是为了描述一些专有信息的,上面的方法带有一张属性表。所有属性表的结构如下图:
一个u2的属性名称索引,一个u2的属性长度加上属性长度的info。
虚拟机规范预定义的属性有很多,比如Code,LineNumberTable,LocalVariableTable,SourceFile等等,这个网上可以搜索到。
属性表结构
按照上面的表结构解析得到下面信息:
0x0009:名称索引为#9(“Code”)。
0x00-00-00-27:属性长度为39字节。
那么接下来解析一个Code属性表,按照下图解析
Code属性表
这里写图片描述

第2个方法

0x0001:”public”
0x000B:#11(”testMethod”)
0x0008:”()V”
0x0001:属性数量=1
0x0009:”Code”
0x00-00-00-19:属性长度为25
解析一个Code表
0x0000:max_stack =0
0x0001:max_local =1
0x00-00-00-01:code_length =1
0xB1:return(该方法返回void)
0x0000:异常表长度=0
0x0001:属性表长度为1
//第一个属性表
0x000A:#10,LineNumberTable
0x00-00-00-06:属性长度为6
0x0001:line_number_length = 1
0x0000:start_pc =0
0x0006:line_number =6


属性

Attributes
0x0001: 同样的,表示有1个Attributes了。
0x000C: #12(“SourceFile”)
0x00000002: attribute_length=2
0x0010: sourcefile_index = #13(“Demo.java”)
SourceFile属性用来记录生成该Class文件的源码文件名称。
SourceFile


后记

我们也可以使用java自带的反编译器来解析字节码文件:
javac cn/bjut/Demo.java
javap -verbose cn.bjut.Demo > info.txt

Classfile /D:/N3verL4nd/Desktop/cn/bjut/Demo.class  Last modified 2017-8-10; size 278 bytes  MD5 checksum cdb9f028281cce76494a801949018db5  Compiled from "Demo.java"public class cn.bjut.Demo  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref          #4.#14         // java/lang/Object."<init>":()V   #2 = Fieldref           #3.#15         // cn/bjut/Demo.x:I   #3 = Class              #16            // cn/bjut/Demo   #4 = Class              #17            // java/lang/Object   #5 = Utf8               x   #6 = Utf8               I   #7 = Utf8               <init>   #8 = Utf8               ()V   #9 = Utf8               Code  #10 = Utf8               LineNumberTable  #11 = Utf8               testMethod  #12 = Utf8               SourceFile  #13 = Utf8               Demo.java  #14 = NameAndType        #7:#8          // "<init>":()V  #15 = NameAndType        #5:#6          // x:I  #16 = Utf8               cn/bjut/Demo  #17 = Utf8               java/lang/Object{  public cn.bjut.Demo();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=2, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: aload_0         5: bipush        10         7: putfield      #2                  // Field x:I        10: return      LineNumberTable:        line 2: 0        line 3: 4  public void testMethod();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=0, locals=1, args_size=1         0: return      LineNumberTable:        line 6: 0}SourceFile: "Demo.java"

参考

一文让你明白Java字节码
可视化字节码分析工具classpy:https://github.com/zxh0/classpy
Java class file
The class File Format