class文件格式分析---class文件结构

Java语言是跨平台的,其跨平台的基石是字节码,字节码按照java虚拟机规范的格式组成了class文件,并在虚拟机上运行。因此class文件的结构也是java跨平台很重要的一个基础。下面简单看看class文件的结构:

以上是class文件的基本结构,整个class文件分Magic,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods,Attributes几个部分。
   先看一个class文件,简单分析一下其结构
 Hello.java

点击(此处)折叠或打开

  1. public class Hello{
  2. }
以上是一个没有任何方法和变量的空类,这样,class里的内容就比较少,便于我们分析。先编译生成Hello.class
然后用od -t x1 Hello.class查看其内容

点击(此处)折叠或打开

  1. 0000000 ca fe ba be 00 00 00 32 00 0d 0a 00 03 00 0a 07
  2. 0000020 00 0b 07 00 0c 01 00 06 3c 69 6e 69 74 3e 01 00
  3. 0000040 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69
  4. 0000060 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 0a
  5. 0000100 53 6f 75 72 63 65 46 69 6c 65 01 00 0a 48 65 6c
  6. 0000120 6c 6f 2e 6a 61 76 61 0c 00 04 00 05 01 00 05 48
  7. 0000140 65 6c 6c 6f 01 00 10 6a 61 76 61 2f 6c 61 6e 67
  8. 0000160 2f 4f 62 6a 65 63 74 00 21 00 02 00 03 00 00 00
  9. 0000200 00 00 01 00 01 00 04 00 05 00 01 00 06 00 00 00
  10. 0000220 1d 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00
  11. 0000240 00 01 00 07 00 00 00 06 00 01 00 00 00 01 00 01
  12. 0000260 00 08 00 00 00 02 00 09
1、MagicNumber:cafe babe这个四个字节是用来标识class文件的,虚拟机加载class文件的时候会先检查这四个字节,如果不是cafe babe则虚拟机拒绝加载该文件。
下面我们来尝试修改该MagicNumber看看虚拟机的到底会不会加载我们的class文件。
vim -b Hello.class打开class文件,然后输入命令 “:%!xxd“ ,class文件就会变成二进制文件,将cafebabe修改为aafebabe, 再“:%!xxd -r”转换回文本模式,":wq"存盘退出。
运行java Hello.class,此时会输出如下信息

点击(此处)折叠或打开

  1. Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 2868820670 in class file Hello
输出的错误信息说: 2868820670(0xaafebabe)不是一个正确的mageic value。

ps:    vim -b:是指以二进制方式打开文件
       xxd:是输出文件的十六进制形式,xxd -r是将十六进制转换成二进制
       修改cafe成aafe:文件转成十六进制后,光标移动到cafe的c上,按r进入替换模式,输入a,会将c替换成a,输入:wq保存即        可。

2、Version :0x0000  0032,前两个(0x0000)是次版本号,后两个(0x0032,表示十进制的50)是主版本号,因为java虚拟机规范一直在不断修改和完善,所以class文件会带上版本信息,表示其版本。这里的0x00000032,表示版本号为50.0,主版本号为50,次版本号为0. 如果版本号高于当前虚拟机支持的版本号,虚拟机也会拒绝加载该文件的。
下面我们动手修改一下class文件的版本,让其超出当前虚拟机支持的版本。又上面可知,我本机当前支持的版本为50.0,高出这个版本,虚拟机就会拒绝。这里我们将0x00000032改成0x00010032,再次java Hello。输出如下:

 

点击(此处)折叠或打开

  1. Exception in thread "main" java.lang.UnsupportedClassVersionError: Hello : Unsupported major.minor version 50.1
以上的信息是说:不支持的major.minor 50.1,也就是当前的虚拟机不支持50.1版本,即是版本号为0x00010032的不被当前虚拟机支持。
  

3、constant_pool 常量池
既然是池子,肯定是拿来放东西的,名字叫常量池,那肯定是放常量的,那哪些常量是可以放的,又怎么放的呢。下面就讲解这两个问题
1)放什么
常量池中主要存放两类内容:字面常量和符号引用。
字面常量主要包含文本字符串,被声明为final的常量等。
符号引用:因为java在编译的时候没有进行连接这一步,所有的引用都是在加载到虚拟机里动态连接的,这就要求class文件里存放这些信息。主要有以下三类常量:
 a) 类和接口的全限定名
 b)字段的名称和描述符
 c)方法的名称和描述符

2)怎么放
 现在知道了常量池里要放什么东西,但是具体怎么放呢?
想一想,如果你有个箱子要放很多东西,第一件事情是什么呢?肯定是分类,对吧,把不同的东西按照类型分好,然后再决定怎么放。
 那看看我们有哪些东西,怎么分类,怎么放。
首先,字面常量,想想java里的字面常量也就字符串,整形,长整型,浮点型,双精度浮点型,对于这些常量,我们只要存储它的类型、值的长度和具体的值就可以了。那么,这几个类型可以按照如下的形式来存储:

点击(此处)折叠或打开

  1. CONSTANT{
  2.    type_t type;//常量类型
  3.    int  length;//值的长度
  4.    byte[] value;//常量的值 
  5. }
这样,基本的字面常量的存储就搞定了。
   但是再想想,上面的方案其实是可以优化的,对于整形,长整型,浮点型,双精度浮点型这些常量,它们的长度是固定的,也就是它们的类型决定了它们的长度,因此对于这些类型length这个字段是可以省略的。

然后,是符号引用,符号引用主要是存放了类和接口的引用,字段的信息,方法的信息。
类和接口在java里通过全限定名就可以确定,因此这里相当于存放一个字符串;
那么下面的结构就可以表示一个类了:

点击(此处)折叠或打开

  1. CLASS_INFO{
  2. type_t type;//类型
  3. String fullname;//类或者接口的全限定名
  4. }

字段的信息包含字段所属的类,字段的类型,字段的名称,因此这里需要保存三个信息,但是在实际的class文件里,字段的类型和名称是放在一个结构里的。
方法的信息和字段的信息一样,但是方法会分成两类,一类是普通方法,一类是接口上的方法,可以通过类别来区分。
这样,字段和方法的信息就可以用以下结构来表示了:

点击(此处)折叠或打开

  1. FILE_METHOD{
  2. type_t type;//字段或者方法
  3. CLASS_INFO *class;//指向class的引用
  4. NAME_AND_TYPE nameType;//字段的名称和类型,或者方法的名称和返回值
  5. }
还有一个结构NAME_AND_TYPE,这个结构是怎样的呢?由上面可知,这个结构是存放名称和类型的,也即是包含了两个字符串的一个组合,那么它的结构应该如下:

点击(此处)折叠或打开

  1. NAME_AND_TYPE{
  2.     type_t type;//类型
  3.     String *name;//指向name的引用
  4.     String *type;//指向type的引用
  5. }
还有一个结构是CONSTANT_Utf8_info,这个结构是存放utf-8字符串的,所有的字符串都是用这个结构封装的,String结构,Class结构里的字符串常量都是对CONSTANT_Utf8_info的一个引用,具体的值都会放在CONSTANT_utf8_info里。
它的结构如下:

点击(此处)折叠或打开

  1. CONSTATN_Utf8_info{
  2.    type_t type;//类型
  3.    int length;//字符串占的字节数
  4.    bytes[] value;//UTF-8编码的字符串
  5. }

最后,常量池的结构都弄明白了,下面来看看class文件分析一下:
除去前面的MagicNum和Version,接下来的两个字节0x000d表示常量池包含有多少个常量,这里显示有13个。
实际上是有12个,因为常量池里对常量的索引是从1开始的。当对第0个常量进行索引时,表示不指向任何常量。
接着就是第一个常量,首先是该常量的类型(字节码里用tag标识),0x0a表示tag=10,表示这个常量是类中方法的引用,那么接下来的信息就是方法所属的类和方法的名称和类型;接下来的两个字节0x0003表示指向第三个常量,那么第三个常量肯定是一个类;接着两个自己0x000a表示指向第10个常量,那么第十个常量肯定是一个NAME_AND_FIELD结构。
接着往下分析,其实很简单,常量分3类:
基本常量,包含Integer,Float,Long,Double,String,Class可以表示为:类型(1个字节)和值(长度由具体类型决定)
复合常量,包含Filed,Method,NameAndType可以表示为:类型(1个字节)索引1(2个字节)和索引2(2个字节)
Utf-8,包含类型(1个字节)长度(2个字节)和值(长度个字节)

那么按照这个结构把常量区重新调整一下:

点击(此处)折叠或打开

  1. 0d 常量池包含的常量数目
  2. 0a 00 03 00 0a 方法,名称为第3个常量,返回值和类型为第10个常量
  3. 07 00 0b 类,名称为第11个常量Hello
  4. 07 00 0c 类,名称为第12个常量java/lang/Object
  5. 01 00 06 3c 69 6e 69 74 3e 字符串<init>
  6. 01 00 03 28 29 56 字符串()V
  7. 01 00 04 43 6f 64 65 字符串 Code
  8. 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 字符串LineNumberTable
  9. 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 字符串SourceFile
  10. 01 00 0a 48 65 6c 6c 6f 2e 6a 61 76 61 字符串Hello.java
  11. 0c 00 04 00 05 NAME_AND_FIELD ,name指向第4个常量<init>,field指向第5个()V
  12. 01 00 05 48 65 6c 6c 6f 字符串 Hello
  13. 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 字符串java/lang/Object

整个常量池的结构就非常清晰了
这里可以看到第一个常量是方法常量,它所属的类是java/lang/Object,它的名称和类型指向第十个常量,查看第十个常量,可以看到,其中name指向<init>,类型指向()V,也就是第一个方法为void<init>();这是编译器生成特殊方法,用来初始化用的。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值