点击上方“ Java资料站 ”,选择“标星公众号”

优质文章,第一时间送达

陈建源  |  作者

urlify.cn/INFrUr  |  来源

“一次编写,到处运行(Write Once,Run Anywhere)“,这是 Java 诞生之时一个非常著名的口号。在学习 Java 之初,就了解到了我们所写的.java会被编译期编译成.class文件之后被 JVM 加载运行。JVM 全称为 Java Virtual Machine,一直以为 JVM 执行 Java 程序是一件理所当然的事情,但随着工作过程中接触到了越来越多的基于 JVM 实现的语言如Groovy Kotlin Scala等,就深刻的理解到了 JVM 和 Java 的无关性,JVM 运行的不是 Java 程序,而是符合 JVM 规范的.class字节码文件。字节码是各种不同平台的虚拟机与所有平台都统一使用的程序储存格式。是构成Run Anywhere 的基石。因此了解 Class 字节码文件对于我们开发、逆向都是十分有帮助的。

Class 类文件的结构

 概述

Class文件是一组以 8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用 8 位字节以上空间的数据项时,则会按照Big-Endian的方式分割成若干个 8 字节进行存储。Big-Endian具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据。SPARCPowerPC等处理器默认使用Big-Endian字节存储顺序,而x86等处理器则是使用了相反的Little-Endian顺序来存储数据。因此为了Class文件的保证平台无关性,JVM必须对其规范统一。

Class 文件结构

在讲解Class类文件结构之前需要先介绍两个概念:无符号数和表。一种类似 C 语言结构体的伪结构。

  • 无符号数:基本类型数据,一 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

  • 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以_info结尾,用于描述有层次关系的复合结构的数据。

当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时就代表此类型的集合。整个 Class文件本质上就是一张表,其数据项如下伪代码所示:

ClassFile {  u4              magic;  u2              minor_version;  u2              major_version;  u2              constant_pool_count;  cp_info         constant_pool[constant_pool_count-1];  u2              access_flags;  u2              this_class;  u2              super_class;  u2              interfaces_count;  u2              interfaces[interfaces_count];  u2              fields_count;  field_info      fields[fields_count];  u2              methods_count;  method_info     methods[methods_count];  u2              attributes_count;  attribute_info  attributes[attributes_count];}

每项数据项的含义我们可以对照下图参照表:

同时我们将根据一个具体的 Java 类来分析 Class 文件结构

public class ByteCode {    private String username;

    public String getUsername() {        return username;    }

    public void setUsername(String username) {        this.username = username;    }}

其.class 文件内容如下:

使用 javap 命令可以得到反汇编代码:

Classfile /Users/chenjianyuan/IdeaProjects/blog/blog-web/target/test-classes/tech/techstack/blog/ByteCode.class  Last modified 2020-8-8; size 581 bytes  MD5 checksum 43eb79f48927d9c5bbecfa5507de0f3c  Compiled from "ByteCode.java"public class tech.techstack.blog.ByteCode  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref #4.#21 // java/lang/Object."":()V   #2 = Fieldref #3.#22 // tech/techstack/blog/ByteCode.username:Ljava/lang/String;   #3 = Class #23 // tech/techstack/blog/ByteCode   #4 = Class #24 // java/lang/Object   #5 = Utf8 username   #6 = Utf8 Ljava/lang/String;   #7 = Utf8    #8 = Utf8 ()V   #9 = Utf8 Code  #10 = Utf8 LineNumberTable  #11 = Utf8 LocalVariableTable  #12 = Utf8 this  #13 = Utf8 Ltech/techstack/blog/ByteCode;  #14 = Utf8 getUsername  #15 = Utf8 ()Ljava/lang/String;  #16 = Utf8 setUsername  #17 = Utf8 (Ljava/lang/String;)V  #18 = Utf8 MethodParameters  #19 = Utf8 SourceFile  #20 = Utf8 ByteCode.java  #21 = NameAndType #7:#8 // "":()V  #22 = NameAndType #5:#6 // username:Ljava/lang/String;  #23 = Utf8 tech/techstack/blog/ByteCode  #24 = Utf8 java/lang/Object{  public tech.techstack.blog.ByteCode();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1 // Method java/lang/Object."":()V         4: return      LineNumberTable:        line 7: 0      LocalVariableTable:        Start Length Slot Name Signature            0 5 0 this Ltech/techstack/blog/ByteCode;  public java.lang.String getUsername();    descriptor: ()Ljava/lang/String;    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: getfield #2 // Field username:Ljava/lang/String;         4: areturn      LineNumberTable:        line 11: 0      LocalVariableTable:        Start Length Slot Name Signature            0 5 0 this Ltech/techstack/blog/ByteCode;  public void setUsername(java.lang.String);    descriptor: (Ljava/lang/String;)V    flags: ACC_PUBLIC    Code:      stack=2, locals=2, args_size=2         0: aload_0         1: aload_1         2: putfield #2 // Field username:Ljava/lang/String;         5: return      LineNumberTable:        line 15: 0        line 16: 5      LocalVariableTable:        Start Length Slot Name Signature            0 6 0 this Ltech/techstack/blog/ByteCode;            0 6 1 username Ljava/lang/String;    MethodParameters:      Name Flags      username}SourceFile: "ByteCode.java"

magic

每个 Class 文件的头 4 个字节0xCAFEBABE称为魔数(Magic Number),用来确定这个文件是否为能被虚拟机接受的 Class 文件格式。

minor_version & major_version

第 5、6 个字节为次版本号(minor_version),第 6、7 个字节是主版本号(major version)上图次版本号 00 00转换为 10 进制为 0,主版本号 00 34 转换为十进制为 52,代表 JDK 1.8。观察反汇编代码也能得到次版本和主版本信息。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能运行高版本的 Class 文件,即使文件格式没有发生任何变化,虚拟机也拒绝执行高于其版本号的 Class 文件。

constant_pool_count & constant_pool[]

后面紧跟着的 2 个字节为常量池个数(constant_pool_count),然后后面紧跟 constant_pool_count 个数的常量。constant_pool_count 是从 1 开始而不是从 0 开始,是为了将 0 项空出来标识后面某些指向常量池的索引值的数据在特定情况下不引用常量池,这种情况下就可以把索引值置为 0 来表示。(除常量池计数外,对于其他类型集合包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的)

常量池(constant_pool)主要存放两大类常量:

  • 字面量

    • 字符串常量

    • final 的常量值

    • 其他类文件的引用

  • 符号引用

    • 类和接口的全限定名

    • 字段的名称和描述符

    • 方法的名称和描述符

常量池中的每一个常量都是一个常量表,常量表开始的第一位是一个u1类型的标志位(tag),来区分常量表的类型。在JDK 1.7之前共有11种结构各不相同的表结构数据,在JDK 1.7中为了更好地支持动态语言调用,又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info),14 中常量类型所代表的具体含义如下:

我们对其按照字面量和符号引用类型分类的话可以入下图所示

Class文件中的常量池结构通过上例汇编代码可看出:

Constant pool:   #1 = Methodref #4.#21 // java/lang/Object."":()V   #2 = Fieldref #3.#22 // tech/techstack/blog/ByteCode.username:Ljava/lang/String;   #3 = Class #23 // tech/techstack/blog/ByteCode   #4 = Class #24 // java/lang/Object   #5 = Utf8 username   #6 = Utf8 Ljava/lang/String;   #7 = Utf8    #8 = Utf8 ()V   #9 = Utf8 Code  #10 = Utf8 LineNumberTable  #11 = Utf8 LocalVariableTable  #12 = Utf8 this  #13 = Utf8 Ltech/techstack/blog/ByteCode;  #14 = Utf8 getUsername  #15 = Utf8 ()Ljava/lang/String;  #16 = Utf8 setUsername  #17 = Utf8 (Ljava/lang/String;)V  #18 = Utf8 MethodParameters  #19 = Utf8 SourceFile  #20 = Utf8 ByteCode.java  #21 = NameAndType #7:#8 // "":()V  #22 = NameAndType #5:#6 // username:Ljava/lang/String;  #23 = Utf8 tech/techstack/blog/ByteCode  #24 = Utf8 java/lang/Object

观察上面Class文件00 19表示有 25 个常量,依次往后数 24(25-1)个常量则为常量池中的常量。紧随其后的一个字节为第一个常量表的 tag 位 0A -> 10,通过常量表类型查询可知 10 为 CONSTANT_Methodref_info,表内数据项为u1: tag u2: class_info u2: name_and_type_index,结合Class文件分析,这表示从第一个常量CONSTANT_Methodref_info占用 5 个字节,其中第一个字节0A为标志位,其后两个字节00 04 -> 4 之后两个字节为 class_info,紧随 2 个字节00 15 -> 21为 name_and_type_index。我们通过查询汇编代码常量池中的一个常量表为#1 = Methodref #4.#21得出一个常量表正是方法引用,其数据项索引也是#4#21。剩下的 24 种常量分析也是如此。也是因为这 14 中常量类型各自均有自己的结构,所以说常量池是最繁琐的数据。

小知识:

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

access_flags

在常量池结束之后,紧接着两个字节代表访问标志(access_flag)这个标志用于识别一些类或接口层次的访问信息。具体标志位以及标志的含义见下表:

invokeSpecial 指令语义在 JDK1.0.2发生过改变,为了区别这条指令使用哪种语意,在 JDK1.0.2之后编译出来的类的这个标志都必须为真。

分析[Class]文件我们得出 access_flag 为 00 21,但是查询上表确没有查询到对应的标志,这是因为 ByteCode是一个普通的 Java 类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而其余 6 个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。而我们通过 ByteCode 汇编代码查看得到 flags: ACC_PUBLIC, ACC_SUPER 也证明了的确为上述所言。

this_class & super_class &interfaces_count & interfaces[]

类索引(this_class)、父类索引(super_class)和 接口数量(interface_count)是一个 u2类型的数据,而接口索引集合 interfaces[] 是一组 u2 类型的数据的集合。这四项数据直接确定了这个类的继承关系。Java 不允许多继承但是允许实现多个接口,这就为什么super_class是一个而 interfaces 是一个集合。我们通过分析[Class]文件可以看出 this_class 对应00 03 -> 3 从常量池中查询 #3 对应的常量

#3 = Class #23 // tech/techstack/blog/ByteCode#23 = Utf8 tech/techstack/blog/ByteCode

可以看出 #3 对应的就是当前类 tech/techstack/blog/ByteCode。后面同样为占两个字节的 super_class 对应的``00 04 -> 4`从常量池中查询出来对应的常量为

#4 = Class #24 // java/lang/Object #24 = Utf8 java/lang/Object

所以 super_class 表示的为:java/lang/Object。随后便是 interface_count 对应的 00 00 -> 0 说明 ByteCode 没有实现接口,因此就不存在后面的 interfaces[]。

fields_count & fields[]

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。fields_count 类中 field_info 的数量。fields[] 则是 field_info 的集合。field_info 的结构如下图所示:

字段修饰符 access_flag 和类中的 access_flag十分相似:

在实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

继续分析Class文件,00 01 00 02 00 05 00 06 00 00。其中 00 01 -> 1表示 field_count,很显然 ByteCode 类中的字段只有一个 private String username;。参照上表继续取两个字节00 02 -> 2表示access_flag,查询可知修饰符号为ACC_PRIVATE,继续取两个字节00 05 -> 5表示 name_index,从汇编代码中查询常量池#5为

#5 = Utf8 username

继续取两个字节00 006 -> 6表示descriptor_index,指向的是常量池 #6 的常量

#6 = Utf8 Ljava/lang/String;

后续的 00 00 -> 0表示attribute_count的个数,此处为 0。

名词释义:

  1. 全限定名和简单名称
    把类名中的.替换成/,连续多个全限定名时,为了不产生混淆,在使用时最后一般都会加入一个;表示全限定名结束。

  2. 方法、字段索引描述

    方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。

  • 基本数据类型

    B---->byte
    C---->char
    D---->double
    F----->float
    I------>int
    J------>long
    S------>short
    Z------>boolean
    V------->void

  • 对象类型

    String------>Ljava/lang/String;

  • 数组类型:每一个唯独都是用一个前置 [ 来表示

    int[] ------>[ I,

    String [][]------>[[Ljava.lang.String;

用描述符来描述方法的,先参数列表,后返回值的格式,参数列表按照严格的顺序放在()中
比如源码 String getUserInfoByIdAndName(int id,String name) 的方法描述符(I,Ljava/lang/String;)Ljava/lang/String;

methods_count & methods[]

Class文件储存格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如下图所示:

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志:

同样根据Class文件进行分析。00 03表示 method_count 说明ByteCode类的方法有三个,根据Method_info继续取出第一个方法的 8 个字节00 01 00 07 00 08 00 0100 01 -> 0表示的是方法的修饰符 表示的是access_flag 为 acc_public,00 07 -> 7表示的是方法的名称(name_index) 指向常量池中#7常量

#7 = Utf8 

表示方法为的构造方法。00 08 ->8代表方法的描述符号(descriptor_index),指向常量池 #8 常量

#8 = Utf8 ()V

表示的是无参无返回值。00 01 -> 1表示有一个方法属性的个数为 1。

根据 attribute_info 结构继续从Class文件中取出00 09 00 00 00 2F 。00 09 -> 9表示方法属性名称(attribute_name_index)指向常量池 #9 常量

#9 = Utf8 Code

00 00 00 2F ->表示Code属性的长度为 47 个字节。(特别特别需要注意这47个字节从Code属性表中第三个开始也就是max_stack开始,因为此 attribute_info为 Code_attribute 本身,attribute_name_index 和 attribute_length 为 Code 的属性)。

Code_attribute属性表结构如下:

Code_attribute {    u2 attribute_name_index; // 属性名索引,常量值固定为"Code"    u4 attribute_length; //属性值长度,值为整个表的长度减去6个字节(attribute_name_index + attribute_length)    u2 max_stack; //操作数栈深度最大值    u2 max_locals; //局部变量表所需的存储空间,单位为"Slot",Slot是虚拟机为局部变量分配内存所使用的最小的单位。    u4 code_length; // 存储Java源程序编译后生成的字节码指令,每个指令为u1类型的单字节。虚拟机规范中明确限制了一个方法不允许超过65535条字节指令,实际上只用了u2长度。    u1 code[code_length]; // 方法指向的具体指令码    u2 exception_table_length; // 异常表的个数    { u2 start_pc; // start_pc 和 end_pc 表示在 Code 数组中的[start_pc, end_pc)处指令所抛出的异常由这个表处理。        u2 end_pc;        u2 handler_pc; // 异常代码的开始处        u2 catch_type; // 表示被处理流程的异常类型,指向常量池中具体的某一个异常类,catchType为 0 处理所有的异常    } exception_table[exception_table_length]; // 异常表结构,用于存放异常信息    u2 attributes_count; // 属性的个数    attribute_info attributes[attributes_count]; // 属性的集合}

第一个 Code 的汇编代码如下:

Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."":()V         4: return      LineNumberTable:        line 7: 0      LocalVariableTable:        Start Length Slot Name Signature            0       5     0  this   Ltech/techstack/blog/ByteCode;

Tips: args_size=1是因为在任何实例方法里面,都可以通过"this"关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。

回到示例代码,取出 47 位 Code 值:

// _ 是本文自行添加方便表示数据项之间的间隔,Class 文件中是不存在的00 01 _00 01 _00 00 00 05 _2A B7 00 01 B1 _00 00 _00 02 _00 0A _00 00 00 06 _00 01 _00 00 _00 06 _00 0B _00 00 00 0C _00 01 00 00 00 05 00 0C 00 0D 00 00

00 01 -> 1 表示 操作数栈(max_stack)的最大深度为 1。后面的00 01 -> 1表示局部变量表的长度(max_locals)为 1,正好与 Code 的汇编代码stack=1 locals=1对应。紧接着后面 4 位00 00 00 05 -> 5表示字节码指令长度(code_length)为 5。继续往后数 5 位2A B7 00 01 B1表示 JVM具体的字节码指令。

0: aload_0 1: invokespecial #1                  // Method java/lang/Object."":()V 4: return
  1. 0x2A:对应的字节码注记符是aload_0,作用就是把当前调用方法的栈帧中的局部变量表索引位置为0的局部变量推送到操作数栈的栈顶。

  2. 0xB7:表示是 invokespecial 调用父类的方法 那么后面需要接入二个字节表示调用哪个方法,所以00 01表示的是指向常量池中第一个位置为为如下结构

    1: invokespecial #1 // Method java/lang/Object."":()V

1 字节的 utf-8 序列的字节 1 无效_字节码文件结构详解相关推荐

  1. Java指令全集_Java的JVM字节码指令集详解

    本文详细介绍了如何使用javap查看java方法中的字节码.以及各种字节码的含义,并且配以完善的案例,一步步,从头到尾带领大家翻译javap的输出.在文末还附有JVM字节码指令集表. 本文不适合没有J ...

  2. JVM笔记:Java虚拟机的字节码指令详解

    1.字节码 Java能发展到现在,其"一次编译,多处运行"的功能功不可没,这里最主要的功劳就是JVM和字节码了,在不同平台和操作系统上根据JVM规范的定制JVM可以运行相同字节码( ...

  3. 客户和服务器之间响应的序列,Redis的请求/响应协议和往返时间详解

    Redis是一种采用客户端-服务器(C/S)模型的TCP服务器,这种模型也被称作请求/响应协议. 这就意味着,一个请求通常需要经过以下步骤才能完成: 客户端会向服务器发送一个查询请求,然后从套接字(s ...

  4. pythonbyte连接_Python3之字节串bytes与字节数组bytearray的使用详解

    字节串bytes 字节串也叫字节序列,是不可变的序列,存储以字节为单位的数据 字节串表示方法: b"ABCD" b"\x41\x42" ... 字节串的构造函数 ...

  5. mysql宽字节注入_转宽字节注入详解

    在mysql中,用于转义的函数有addslashes,mysql_real_escape_string,mysql_escape_string等, 还有一种情况是magic_quote_gpc,不过高 ...

  6. Buffer(缓冲/字节容器)详解

    本文来说下Buffer(缓冲/字节容器)详解 文章目录 概述 Buffer API ByteBuf - Netty 字节数据的容器 ByteBuf如何工作 ByteBuf 使用模式 HEAP BUFF ...

  7. bit、byte、KB、B、字节、位、字符之间关系详解

    bit就是位,也叫比特位,是计算机表示数据最小的单位 byte就是字节 3.1byte=8bit 4.1byte就是1B 5.一个字符=2字节 6.1KB=1024B 1.字节就是Byte,也是B 2 ...

  8. 字节、字符串,以及16进制字符串的字节长度

    前提 公司有个项目使用到第三方厂商的硬件sdk,其中硬件的设置中有个配置项的单位是字节长度,一开始以为是字符串长度,后来发现不对,仔细排查才发现是字节长度. 知识点 字节(Byte)是一种计量单位,表 ...

  9. python bytearray转为byte_Python3 bytearray() 函数详解 将参数转为可变的字节数组

    Python3 bytearray() 函数详解 将参数转为可变的字节数组 bytearray()函数的主要用途是将参数转换为一个新的字节数组,它是一个可变的整数序列,它的取值范围是0 <= x ...

最新文章

  1. 异常检测概览——孤立森林 效果是最好的
  2. 一个关于linux文件预读机制问题
  3. sql 一个字段在另外一个表没出现_都9012年啦,不懂得这些SQL语句优化,你是要吃大亏的...
  4. AFNetworking 对数据进行https ssl加密
  5. 【牛客 - 272B】Xor Path(树上操作,路径异或值)
  6. Oracle分组排序查询
  7. 外媒报道电子垃圾被运入中国
  8. 我34岁,曾月入10万,给前员工修汽车
  9. DataList控件嵌套,激发内部控件事件
  10. Matlab使用心得
  11. python中oxf2是多少_Python学习笔记[2]
  12. 用caffe训练测试自己的图片
  13. JNI中调用Java函数
  14. JavaWeb开发——JSP技术
  15. 3分钟快速了解,如何一次通过CISSP考试?
  16. WIN2003序列号
  17. NKOJ 2522 Sandy的卡片(差分数组+DP)
  18. SDN网络下有哪些SDN交换机选择?
  19. 2009年最新搞笑语录
  20. cad移动时捕捉不到基点_CAD很难学吗?其实不然,掌握这10个知识你就能走天下!...

热门文章

  1. RecyclerView 的findFirstVisibleItemPosition()与findLastVisibleItemPosition()
  2. python 生成验证码
  3. Jenkins 插件 地址证书报错问题解决思路
  4. short s1 = 1; s1 = s1 + 1;有错而short s1 = 1; s1 += 1正确
  5. JavaScript高级程序设计(第3版)第六章读书笔记
  6. 更换AppleWWDRCA.cer证书
  7. clientcontainerThrift Types
  8. android系统短信库的一些用法
  9. A Rectangular Barn
  10. 如何建立图像数据矩阵和图像显示灰度之间的关系!_放射技术考试第四章第一节 数字图像的特征...