竹笋

首页 » 问答 » 灌水 » 字节码文件结构详解
TUhjnbcbe - 2024/10/9 10:04:00
北京哪家治白癜风医院最好 https://m.39.net/disease/a_m7kpmsm.html

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

Class类文件的结构

概述

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

Class文件结构

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

无符号数:基本类型数据,一u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以_info结尾,用于描述有层次关系的复合结构的数据。当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时就代表此类型的集合。整个Class文件本质上就是一张表,其数据项如下伪代码所示:

ClassFile{u4magic;u2minor_version;u2major_version;u2constant_pool_count;cp_infoconstant_pool[constant_pool_count-1];u2access_flags;u2this_class;u2super_class;u2interfaces_count;u2interfaces[interfaces_count];u2fields_count;field_infofields[fields_count];u2methods_count;method_infomethods[methods_count];u2attributes_count;attribute_infoattributes[attributes_count];}  

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

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

publicclassByteCode{privateStringusername;publicStringgetUsername(){returnusername;}publicvoidsetUsername(Stringusername){this.username=username;}}

其.class文件内容如下:

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

Classfile/Users/chenjianyuan/IdeaProjects/blog/blog-web/target/test-classes/tech/techstack/blog/ByteCode.classLastmodified-8-8;sizebytesMD5checksum43eb79fd9c5bbecfade0f3cCompiledfromByteCode.javapublicclasstech.techstack.blog.ByteCodeminorversion:0majorversion:52flags:ACC_PUBLIC,ACC_SUPERConstantpool:#1=Methodref#4.#21//java/lang/Object.init:()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=Utf8username#6=Utf8Ljava/lang/String;#7=Utf8init#8=Utf8()V#9=Utf8Code#10=Utf8LineNumberTable#11=Utf8LocalVariableTable#12=Utf8this#13=Utf8Ltech/techstack/blog/ByteCode;#14=Utf8getUsername#15=Utf8()Ljava/lang/String;#16=Utf8setUsername#17=Utf8(Ljava/lang/String;)V#18=Utf8MethodParameters#19=Utf8SourceFile#20=Utf8ByteCode.java#21=NameAndType#7:#8//init:()V#22=NameAndType#5:#6//username:Ljava/lang/String;#23=Utf8tech/techstack/blog/ByteCode#24=Utf8java/lang/Object{publictech.techstack.blog.ByteCode();descriptor:()Vflags:ACC_PUBLICCode:stack=1,locals=1,args_size=10:aload_01:invokespecial#1//Methodjava/lang/Object.init:()V4:returnLineNumberTable:line7:0LocalVariableTable:StartLengthSlotNameSignaturethisLtech/techstack/blog/ByteCode;publicjava.lang.StringgetUsername();descriptor:()Ljava/lang/String;flags:ACC_PUBLICCode:stack=1,locals=1,args_size=10:aload_01:getfield#2//Fieldusername:Ljava/lang/String;4:areturnLineNumberTable:line11:0LocalVariableTable:StartLengthSlotNameSignaturethisLtech/techstack/blog/ByteCode;publicvoidsetUsername(java.lang.String);descriptor:(Ljava/lang/String;)Vflags:ACC_PUBLICCode:stack=2,locals=2,args_size=20:aload_01:aload_12:putfield#2//Fieldusername:Ljava/lang/String;5:returnLineNumberTable:line15:0line16:5LocalVariableTable:StartLengthSlotNameSignaturethisLtech/techstack/blog/ByteCode;usernameLjava/lang/String;MethodParameters:NameFlagsusername}SourceFile:ByteCode.java

magic

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

minor_versionmajor_version

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

constant_pool_countconstant_pool[]

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

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

字面量字符串常量final的常量值其他类文件的引用符号引用类和接口的全限定名字段的名称和描述符方法的名称和描述符常量池中的每一个常量都是一个常量表,常量表开始的第一位是一个u1类型的标志位(tag),来区分常量表的类型。在JDK1.7之前共有11种结构各不相同的表结构数据,在JDK1.7中为了更好地支持动态语言调用,又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info),14中常量类型所代表的具体含义如下:

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

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

Constantpool:#1=Methodref#4.#21//java/lang/Object.init:()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=Utf8username#6=Utf8Ljava/lang/String;#7=Utf8init#8=Utf8()V#9=Utf8Code#10=Utf8LineNumberTable#11=Utf8LocalVariableTable#12=Utf8this#13=Utf8Ltech/techstack/blog/ByteCode;#14=Utf8getUsername#15=Utf8()Ljava/lang/String;#16=Utf8setUsername#17=Utf8(Ljava/lang/String;)V#18=Utf8MethodParameters#19=Utf8SourceFile#20=Utf8ByteCode.java#21=NameAndType#7:#8//init:()V#22=NameAndType#5:#6//username:Ljava/lang/String;#23=Utf8tech/techstack/blog/ByteCode#24=Utf8java/lang/Object

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

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

access_flags

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

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

分析[Class]文件我们得出access_flag为,但是查询上表确没有查询到对应的标志,这是因为ByteCode是一个普通的Java类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而其余6个标志应当为假,因此它的access_flags的值应为:0x

0x=0x。而我们通过ByteCode汇编代码查看得到flags:ACC_PUBLIC,ACC_SUPER也证明了的确为上述所言。

this_classsuper_classinterfaces_countinterfaces[]

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

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

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

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

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

fields_countfields[]

字段表(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。其中-1表示field_count,很显然ByteCode类中的字段只有一个privateStringusername;。参照上表继续取两个字节-2表示access_flag,查询可知修饰符号为ACC_PRIVATE,继续取两个字节-5表示name_index,从汇编代码中查询常量池#5为

#5=Utf8username  

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

#6=Utf8Ljava/lang/String;

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

名词释义:全限定名和简单名称把类名中的.替换成/,连续多个全限定名时,为了不产生混淆,在使用时最后一般都会加入一个;表示全限定名结束。方法、字段索引描述方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。基本数据类型B----byteC----charD----doubleF-----floatI------intJ------longS------shortZ------booleanV-------void对象类型String------Ljava/lang/String;数组类型:每一个唯独都是用一个前置[来表示int[]------[I,String[][]------[[Ljava.lang.String;用描述符来描述方法的,先参数列表,后返回值的格式,参数列表按照严格的顺序放在()中比如源码StringgetUserInfoByIdAndName(intid,Stringname)的方法描述符(I,Ljava/lang/String;)Ljava/lang/String;

methods_countmethods[]

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

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

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

#7=Utf8init

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

#8=Utf8()V

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

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

#9=Utf8Code

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

Code_attribute属性表结构如下:

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

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

Code:stack=1,locals=1,args_size=10:aload_01:invokespecial#1//Methodjava/lang/Object.init:()V4:returnLineNumberTable:line7:0LocalVariableTable:StartLengthSlotNameSignaturethisLtech/techstack/blog/ByteCode;

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

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

//_是本文自行添加方便表示数据项之间的间隔,Class文件中是不存在的___2AB7B1___A_6___6_B_C_CD

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

0:aload_01:invokespecial#1//Methodjava/lang/Object.init:()V4:return

0x2A:对应的字节码注记符是aload_0,作用就是把当前调用方法的栈帧中的局部变量表索引位置为0的局部变量推送到操作数栈的栈顶。0xB7:表示是invokespecial调用父类的方法那么后面需要接入二个字节表示调用哪个方法,所以表示的是指向常量池中第一个位置为为如下结构1:invokespecial#1//Methodjava/lang/Object.init:()V0xB1:对应的字节码指令值retrun`表示retrunvoidfrommethod。

表示异常表个数(exception_table_length)为0,方法没有抛出异常。

-2表示Code_attribute结构中属性表的个数为2个。A-10表示attribute_name_index指向常量池#10LineNumberTable常量。继续后面4位6-10表示attribute_length即LineNumberTable的长度。LineNumberTable是用来描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系,比如我们平时debug某一行代码。其结构如下所示:

LineNumberTable_attribute{u2attribute_name_index;u4attribute_length;u2line_number_table_length;{u2start_pc;u2line_number;  }line_number_table[line_number_table_length];}

-1表示行号表的个数为1,即只存在一个行号表。表示start_pc为字节码行号,6-6表示源码行号为第7(6+1)行。

B-11表示第二个属性表对应常量池#11LocalVariableTable常量。C-12表示LocalVariableTable常量的长度为12。LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。其结构如下:

LocalVariableTable_attribute{u2attribute_name_index;u4attribute_length;u2local_variable_table_length;{u2start_pc;u2length;u2name_index;u2descriptor_index;u2index;}local_variable_table[local_variable_table_length];}

LocalVariableTable也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。

-1表示本地变量表的个数local_variable_table_length为1。表示local_variable_table的start_pc为0,其含义为这个局部变量的生命周期开始的字节码偏移量。-5表示local_variable_table的length为5,其含义为这个局部变量作用范围覆盖的长度。两者结合起来就是这个局部变量在字节码之中的作用域范围。CD分别表示name_index和descriptor_index,分别指向常量池中#12this和#13Ltech/techstack/blog/ByteCode;常量。分别代表了局部变量的名称以及这个局部变量的描述符。表示了这个变量在本地变量表中的index即这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个。

attributes_countattributes[]

属性表(attribute_info)用于描述某些场景专有的信息。在Class文件、字段表、方法表都可以携带自己的属性表集合。所有的属性都具有一下常规格式:

attribute_info{u2attribute_name_index;u4attribute_length;u1info[attribute_length];}

根据TheJavaVirtualMachineSpecification已经增加到了23项。根据其用途可以分为三组:

五个属性对于classJava虚拟机正确解释文件至关重要:ConstantValueCodeStackMapTableExceptionsBootstrapMethods十二个属性对于JavaSE平台的类库正确解释class文件至关重要:InnerClassesEnclosingMethodSyntheticSignatureRuntimeVisibleAnnotationsRuntimeInvisibleAnnotationsRuntimeVisibleParameterAnnotationsRuntimeInvisibleParameterAnnotationsRuntimeVisibleTypeAnnotationsRuntimeInvisibleTypeAnnotationsAnnotationDefaultMethodParameters六个属性对于classJava虚拟机或JavaSE平台的类库对文件的正确解释不是至关重要的,但对于工具来说非常有用:SourceFileSourceDebugExtensionLineNumberTableLocalVariableTableLocalVariableTypeTableDeprecated属性汇总

参考:

[1]周志明.深入理解Java虚拟机:JVM高级特性与最佳实践.北京:机械工业出版社,.

[2]Chapter4.ThclassFileFormat

[3]Chapter6.TheJavaVirtualMachineInstructionSet

文章首发于陈建源的博客,欢迎访问。文章作者:陈建源文章链接:

1
查看完整版本: 字节码文件结构详解