首页 > 其他分享 >001_字节码文件结构

001_字节码文件结构

时间:2024-07-17 14:57:06浏览次数:14  
标签:info 文件 java 字节 ... 001 0000 class

字节码有啥用呢?知道了字节码之后你会看到更广阔的天地。作为一个只会敲代码的打工人,你可能只会写java代码,但是知其然而不知其所以然也。大人,时代已经变了,曾经修个电脑就能上班的年代已经一去不复返。如果卷不动别人,就要被人卷。

字节码概述

JVM与字节码

我们都知道java的出现是围绕着一个愿景的:

一个代码写一遍,到处运行,因为那个年代不同的os上的底层的指令集是可能不一样的,这样子可就为难程序员了,同样的逻辑要写好几便。我们it领域解决问题都可以用分层的方式解决,一层不够那就再加一层,TCP就是这么弄出来的。java领域呢,也抽象出来了一层,往下屏蔽os,往上就可以进行统一化了,真正的实现,write one time,run everywhere。

java领域被抽象出来的东西就是JVM,而和jvm本身对接的东西我们称为字节码。

而java代码是更高的一层抽象,这一层屏蔽语言本身。类似于scala,groove,kotlin都可以运行在jvm上,这就是屏蔽了语言的好处。那么我们理一理,java从书写到运行的整个流程。

开发的人根据java语言规范编写 .java代码 -> jdk自带指令javac 进行编译,生成jvm识别的.class文件 -> 运行 java 指令将刚刚写的代码跑起来,之后就是jvm的领域了。
image.png

那么从上面的图中,我们就能看出来,理解字节码本身好处颇多。你可以知道你平时写java的背后都有些什么,有了这个基础之后,每行代码是怎么运行的,也是可以脑补出来的。同时思想上的补充也会弥补以前对jvm本身的认知。

以下是罗列出来的各项知识点:

  1. Class文件结构
  2. 从字节码角度看java代码
    1. 基本数据类型与运算符操作
    2. 流程控制
    3. 对象初始化
    4. 方法调用与多态原理
    5. 异常捕获与处理
    6. 并发关键字
  3. 字节码改写框架
  4. agent寄生插件

既然我们要深究字节码,那自然是需要知道字节码的结构的,在class文件结构设计上,你会学到很多思想

  • 如何最小化表达大量信息
  • 如何制造出一个稳定的,可支持扩展的文件格式
  • 知道如何制造出可以尽可能向下兼容的字节码文件

之后我们就可以和java语言本身做一个对照,看看我们平时的java代码背后都发生了些什么,学的够深了,写起代码来就如有神灵加持。

字节码在一定程度上为开发的人开放了一个口子——在运行态动态修改字节码,完成一个又一个黑科技。skywalking就是这么来的,它相当于是在jvm最上面做了一个代理。背后用的就是agent插件,伴随着jvm运行而运行,因此叫做插件。插件的附着方式可以使用java运行指令携带,也可以使用socket连接,这就是arthas的连接方式。jdk里面的instrument包就是专门干这个事儿的。

因此,如果要玩这么一整套东西,就必须知道字节码指令本身,改写字节码可以借助与框架的力量,毕竟字节码的确复杂。那么这个改写字节码的框架的api就要尽可能熟悉。字节码改写框架又会有很多,挑一个自己熟悉的就可以。

逻辑上,如果要一个闭环,那么就需要知道class是怎么产生的,class文件是怎么在jvm里面运行的。

前者需要知道编译原理,再细节一点就是javac的编译原理,后者就更复杂了,需要知道jvm领域的大量知识进才能把这部分的知识产生一个闭环。

字节码的产生

当我们学习JVM,切入口会从JVM内部构造开始出发,知道JVM的一些行为来指导我们做各种优化。但是当不断深入,字节码的知识就逃不开。当学习完字节码之后,你会好奇字节码是怎么来的。兜兜转转又回到了最本质上的问题——java语言到底是怎么一步一步从低级语言成为当今IT里面的一束红花。

“编译原理”,大家应该都听过,it专业的基本课程之一。我不是it专业的,在不断学习过程中,我一直都想不明白很多事情,包括为什么我的鼠标移动一下,电脑上的鼠标小图标就会移动到我想移动到的位置上,java为什么那么写就能跑,我写成那样,我的机器就能明白我的意思。

编译原理概述

image.png
从上图上可以看到,编译原理可以大体上分为两个大部分:

  • 前端部分
  • 后端部分

前端部分搞定就已经可以制造出类似于python或者js的脚本语言,而后端部分就可以把语义树变成另外一种样子(更低级别的代码形式,例如机器码),然后在此基础上进行不断地优化优化优化,变成执行又快又稳的一种东西。

前端部分的三个概念在这里稍微描述一下,后端部分的步骤暂时就按字面意思理解就可以,问题不大,后面我们详细去说这个东西。

  • 词法分析
    指的是将你写的代码分割成各种有含义的片段,这种小片段称为token,执行完词法分析之后产生的是一连串的token,我们叫做token流。这是一个首尾相连的东西。例如,你的输入是age >= 45,那么产出就会是
(Identifier age) <-> (GE >= ) <-> (IntLiteral 45)

()括号里面的东西我们当成token,<->可以理解为链表的链,关联前后的token

  • 语法分析
    经过词法分析,我们得到token流,接下去语法分析就是将平铺开来的token转化为树状形式,树是按照语法规范走的,这棵树我们称为语法树,即AST树。例如你的输入是 2+3*5,输出就是一颗树一样的东西。这种结构,计算机很容易处理。

image.png

  • 语义分析
    语义分析里面的东西就多了,这是需要理解上下文的。比方说全局变量,引用变量进行计算,变量有效域之类的东西。当然,我们后面会详细描述。

Javac的编译过程

image.png

java编译过程需要经历4个组件

  1. 词法分析器 -> 生成token流
  2. 语法分析器 -> 生成语法树
  3. 语义分析器 -> 注解语法树
  4. 字节码生成器 -> 生成字节码

而对应的步骤事实上会很多:

  • 词法分析: 把源代码中的字符(各个关键字、变量等)转为标记(Token)集合,单个字符的程序编写的最小单元,而token是编译过程的最小单元。
  • 语法分析: 将标记(Token)集合构造为抽象语法树。语法树的每一个节点都代表代码中的一个语法结构(如包、类型、接口、修饰符等等)。
  • 填充符号表:符号表是有一组符号地址和符号信息构成的表格。填充符号表的过程的出口是一个待处理列表,包含了每一个抽象语法树(和package-info.java)的顶级节点。
  • 插入式注解处理器处理注解: 注解处理器可以增删改抽象语法树的任意元素。因此每当注解处理器对语法树进行修改时,都将重新执行1,2,3步,直到注解处理器不再对语法树进行修改为止。每一次的循环过程都称为一次Round。
  • 语义分析:对语法树结构上正确的源程序进行上下文有关的审查。
  • 标注检查:包括是否变量声明、变量和赋值类型是否匹配等、常量折叠。
  • 数据和控制流分析:对程序上下文逻辑更进一步验证。包括变量使用前是否赋值、方法是否有返回值、异常是否被正确处理等。
  • 解语法糖: 把高级语法(如:泛型、可变参数、拆箱装箱等)转为基础语法结构,虚拟机运行时不支持这些高级语法。
  • 生成字节码:把语法树、符号表里的信息转为字节码写到磁盘,同时进行少量的代码添加和转换工作。
  1. 编译与反编译
  2. 什么是编译
    1. 前端编译
    2. 后端编译
  3. 什么是反编译
  4. jit优化
    1. 逃逸分析
    2. 栈上分配
    3. 标量替换
    4. 锁优化
  5. 编译工具
  6. 反编译工具
  7. 类加载机制
  8. classloader
  9. 类加载过程
  10. 双亲委派 如何破坏双亲委派
  11. 模块化 jboosmodule,osgi,jigsaw

编译与反编译相关指令

这部分我们要熟悉javac与javap指令

字节码的编译指令

回忆一波最开始学Java的时候,肯定书里会教你怎么在黑框框里执行你的"hello word",在此我们再详细地回顾一下HelloWorld是怎么玩的

  1. 我们写好的代码为xx.java格式
  2. 使用javac xx.java会在同级目录生成 xx.class文件
  3. 继续使用java xx 指令,就可以执行xx下的main函数。

以下是 javac 的编译指令参数

Usage: javac <options> <source files>
where possible options include:
  -g                         Generate all debugging info
  -g:none                    Generate no debugging info
  -g:{lines,vars,source}     Generate only some debugging info
  -nowarn                    Generate no warnings
  -verbose                   Output messages about what the compiler is doing
  -deprecation               Output source locations where deprecated APIs are used
  -classpath <path>          Specify where to find user class files and annotation processors
  -cp <path>                 Specify where to find user class files and annotation processors
  -sourcepath <path>         Specify where to find input source files
  -bootclasspath <path>      Override location of bootstrap class files
  -extdirs <dirs>            Override location of installed extensions
  -endorseddirs <dirs>       Override location of endorsed standards path
  -proc:{none,only}          Control whether annotation processing and/or compilation is done.
  -processor <class1>[,<class2>,<class3>...] Names of the annotation processors to run; bypasses default discovery process
  -processorpath <path>      Specify where to find annotation processors
  -parameters                Generate metadata for reflection on method parameters
  -d <directory>             Specify where to place generated class files
  -s <directory>             Specify where to place generated source files
  -h <directory>             Specify where to place generated native header files
  -implicit:{none,class}     Specify whether or not to generate class files for implicitly referenced files
  -encoding <encoding>       Specify character encoding used by source files
  -source <release>          Provide source compatibility with specified release
  -target <release>          Generate class files for specific VM version
  -profile <profile>         Check that API used is available in the specified profile
  -version                   Version information
  -help                      Print a synopsis of standard options
  -Akey[=value]              Options to pass to annotation processors
  -X                         Print a synopsis of nonstandard options
  -J<flag>                   Pass <flag> directly to the runtime system
  -Werror                    Terminate compilation if warnings occur
  @<filename>                Read options and filenames from file

翻译过来就是:

-g 生成所有调试信息

-g:none 不生成任何调试信息

-g:{lines,vars,source} 只生成某些调试信息

-nowarn 不生成任何警告

-verbose 输出有关编译器正在执行的操作的消息

-deprecation 输出使用已过时的 API 的源位置

-classpath <路径> 指定查找用户类文件和注释处理程序的位置

-cp <路径> 指定查找用户类文件和注释处理程序的位置

-sourcepath <路径> 指定查找输入源文件的位置

-bootclasspath <路径> 覆盖引导类文件的位置

-extdirs <目录> 覆盖所安装扩展的位置

-endorseddirs <目录> 覆盖签名的标准路径的位置

-proc:{none,only} 控制是否执行注释处理和/或编译。

-processor[,,...] 要运行的注释处理程序的名称; 绕过默认的搜索进程

-processorpath <路径> 指定查找注释处理程序的位置

-d <目录> 指定放置生成的类文件的位置

-s <目录> 指定放置生成的源文件的位置

-implicit:{none,class} 指定是否为隐式引用文件生成类文件

-encoding <编码> 指定源文件使用的字符编码

-source <发行版> 提供与指定发行版的源兼容性

-target <发行版> 生成特定 VM 版本的类文件

-version 版本信息

-help 输出标准选项的提要

-A关键字[=值] 传递给注释处理程序的选项

-X 输出非标准选项的提要

-J<标记> 直接将 <标记> 传递给运行时系统

-Werror 出现警告时终止编译

@<文件名> 从文件读取选项和文件名

-g 需要注意,当在后面使用javap反编译的时候,使用javap -l 可以输出方法的变量表table,如果不使用-g指令编译,将得不到方法的变量列表

接下来仔细描述一下javac的一些重要参数的使用方法

-implicit选项

-implicit选项用来指定是否为隐式引用的文件生成字节码文件,默认生成;选项支持:

1、none:不为隐式引用的文件生成字节码文件;

2、class:为隐式引用的文件生成字节码文件,默认选项;

public class Main {
    public static void main(String[] args) {
        A a = new A();
        a.func();
    }
}
public class A {
    public void func() {
        System.out.println("Test Implicit.");
    }
}

执行情况如下:

> javac Main.java                # 默认生成A的字节码文件
> javac -implicit:none Main.java # 不会生成A的字节码文件

-g 选项

用于生成调试信息,调试信息有-bootclasspathlines、vars和source;

lines:字节码文件中对应源码的行号;字节码调试打断点时,无行号信息,无法打断点。

vars:字节码文件中对应源码的变量信息;字节码调试时,无该信息,无法查看变量信息。

source:字节码文件对应的源文件名,针对类似非public修饰类场景,举例如下:Main.java编译后生成两个字节码文件Main.class && Test.class,Test.class隶属于Main.java,而不是Test.java

不指定-g选项生成lines和source调试信息
-g生成lines、vars、source调试信息
-g:none不生成任何调试信息
-g:{lines,vars,source}指定生成哪些调试信息,可以指定多个用逗号隔开;

-source/-target选项

-source:

用于指定编译源码时使用的JDK版本,例如:javac -source 1.7 TestSource.java 指定使用JDK1.4编译TestSource.java,

但是TestSource.java中使用了lamba表达式,因此编译报错,需要指定JDK版本为1.8;

-target:

用于指定生成的字节码文件要运行在哪个JDK版本,如指定target版本为1.8,则运行字节码文件的JDK版本必须大于等于1.8

编译时同时使用:

运行使用的JDK版本必须大于等于编译使用的JDK版本,即-target指定的版本必须大于等于-source,否则编译会有如下错误:

javac: 源发行版 1.8 需要目标发行版 1.8

public class TestSource {
    public static void main(String[] args) {
        List<String> stringList = Arrays.stream(new String[]{"hello", "hi", "how are you", "what?", "hi"})
            .distinct()
            .filter(word -> word.startsWith("h"))
            .sorted(Comparator.reverseOrder())
            .collect(Collectors.toList());
        System.out.println(stringList);
    }
}

-d/-sourcepath/-classpath选项

-d选项:         指定编译生成字节码文件路径

-classpath选项:  指定依赖类路径,这里的依赖类为字节码文件;用于指导编译器在编译时按照指定路径查找依赖类;可以指定多个classpath,linux用:分隔,windows用;分隔

-sourcepath选项: 指定依赖类源文件路径,并且要求源文件除CLASSPATH依赖外,无其他依赖;如果同时找到了源文件和字节码文件,两者一致,以字节码文件为准;不一致,以源文件为准,并编译源文件

-bootclasspath/-extdirs选项

这两选项是几乎不需要使用的选项,两个选项的作用如下:

-bootclasspath: 用来覆盖引导类文件路径,引导类文件路径为: jdk1.8.0_212\jre\lib\rt.jar

-extdirs:       用来覆盖扩展类文件路径,扩展文件路径为: jdk1.8.0_212\jre\lib\ext\

-Xlint选项

Java编译选项有标准选项和非标准选项之分,标准选项指的是当前版本支持的选项,后续版本也一定支持;非标准选项指的是当前版本支持的选项,后续版本不一定支持。

-Xlint选项用来启用建议的告警,有如下选项:

Java编译选项有标准选项和非标准选项之分,标准选项指的是当前版本支持的选项,后续版本也一定支持;非标准选项指的是当前版本支持的选项,后续版本不一定支持。

非标准选项是以-X开头的选项,但是-X选项则是一个标准选项,用来显示-X选项的帮助信息;有特例:-J选项

1、-Xlint

启用所有编译建议的警告;该选项等同于-Xlint:all,相反禁用所有警告的选项为:-Xlint:none;-Xlint:none并非不显示任何警告,而是会给出存在哪些类型的警告并建议使用-Xlint对应的选项

2、-Xlint:unchecked

启用未经检查的转换警告,JDK1.5泛型引入的,源码中的编译警告即属于该种类型

3、-Xlint:finally

finally语句无法正常结束的警告

4、-Xlint:serial

需要序列化的类,未指定序列化ID的警告

5、-Xlint:fallthrouth

switch case语句中,第一个case语句无break

-encoding指令

指定编译时编码格式,中文windows默认GBK编码,java文件一般使用UTF-8格式,因此常用命令为javac -encoding UTF-8 XXX.java

-verbose

输出编译时的详细信息,包括:classpath、加载的类文件信息。

@<文件名>

用来通经文件指定编译多个java源文件;比如,有如下的几个java源文件,我们希望编译这些源文件,一个一个编译?当然不,

我们可以通过将编译选项和源文件名以行为单位写入文件,然后通过-@标签编译。

javac @compile.cfg

-j <标记>

传递一些信息给 Java Launcher,例如

javac -J -Xms48m Xxx.java

字节码分析工具

javap

JDK 提供了专门用来分析类文件的工具:javap

其中-c -v -l -p -s是最常用的

Usage: javap <options> <classes>
where possible options include:
  -help  --help  -?        Print this usage message
  -version                 Version information
  -v  -verbose             Print additional information
  -l                       Print line number and local variable tables
  -public                  Show only public classes and members
  -protected               Show protected/public classes and members
  -package                 Show package/protected/public classes
                           and members (default)
  -p  -private             Show all classes and members
  -c                       Disassemble the code
  -s                       Print internal type signatures
  -sysinfo                 Show system info (path, size, date, MD5 hash)
                           of class being processed
  -constants               Show final constants
  -classpath <path>        Specify where to find user class files
  -cp <path>               Specify where to find user class files
  -bootclasspath <path>    Override location of bootstrap class files

一般常用的是-v -l -c三个选项。

javap -v classxx,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。

javap -l 会输出行号和本地变量表信息。

javap -c 会对当前class字节码进行反编译生成汇编代码。

javap -p 默认情况下,javap 会显示访问权限为 public、protected 和默认(包级 protected)级别的方法,加上 -p 选项以后可以显示 private 方法和字段

javap -v 参数的输出更多详细的信息,比如栈大小、方法参数的个数。

javap -s 可以输出签名的类型描述符。我们可以看下 xx.java 所有的方法签名

AsmTools

我们知道直接修改.class文件是很麻烦的,虽然有一些图形界面的工具,但还是很麻烦。在OpenJDK里有一个AsmTools项目,用来生成正确的或者不正确的java .class文件,主要用来测试和验证。AsmTools引入了两种表示.class文件的语法:

  • JASM
    用类似java本身的语法来定义类和函数,字节码指令则很像传统的汇编。
  • JCOD
    整个.class用容器的方式来表示,可以很清楚表示类文件的结构。

查看JASM语法结果

java -jar asmtools.jar jdis Test.class

查看JCOD语法结果

java -jar asmtools.jar jdec Test.class

从JASM/JCOD语法文件生成类文件

因为是等价表达,可以从JASM生成.class文件:

java -jar asmtools.jar jasm Test.jasm

同样可以从JCOD生成.class文件:

java -jar asmtools.jar jcoder Test.jasm

更多使用方法参考:

idea插件

在插件商场里面找到jclassLib并安装show Bytecode With JclassLib,找到任意一个.class,然后点开show 的菜单。
image.png
你就可以看到这个class文件的数据结构了,基本上你能想到的,这个插件都能帮你捞出来
image.png

jar指令

打包.class文件

jar -cvf Test.jar -C classes/ .

这个命令将会把classes下的所有文件(包括.class和文件夹等)打包为Test.jar文件。

上篇博客中,介绍了参数-C的意义:-C  更改为指定的目录并包含其中的文件,如果有任何目录文件, 则对其进行递归处理。它相当于使用 cd 命令转指定目录下。

注意后面的".“的用法,jar可以递归的打包文件夹,”."表示是当前文件夹。如果执行命令“jar -cvf Test.jar .”,表示将当前目录下的所有文件及文件夹打包。所以上面的命令的意思就是“将classes下的所有文件打包为Test.jar”。

生成可以运行的jar包

java -cp Test.jar Main

通过上面的命令就可以执行Test.jar中的Main.class。其中,cp指定了jar文件的位置。

需要指定jar包的应用程序入口点,用-e选项

Class 文件结构

先整体看下class的文件结构

ClassFile {
    u4             magic;                                // 魔数(Magic Number)
    u2             minor_version;                        // 版本号(Minor&Major Version)
    u2             major_version; 
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1]; // 常量池(Constant Pool)
    u2             access_flags;                         // 类访问标记(Access Flags)
    u2             this_class;                           // 类索引(This Class)
    u2             super_class;                          // 超类索引(Super Class)
    u2             interfaces_count;
    u2             interfaces[interfaces_count];         // 接口表索引(Interfaces)
    u2             fields_count;
    field_info     fields[fields_count];                 // 字段表(Fields)
    u2             methods_count;
    method_info    methods[methods_count];               // 方法表(Methods)
    u2             attributes_count;
    attribute_info attributes[attributes_count];         // 属性表(Attributes)
}

Java虚拟机规定用u1、u2、u4三种数据结构来表示1、2、4字节无符号整数。上面的结构是采用类似c结构体的形式表达。
image.png

魔数

先造一个最简单的代码:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

生成的class文件用工具打开是这样的(本身是一个二进制,里面都是01,用工具打开才会变成这个样子)

00000000: cafe babe 0000 0034 001d 0a00 0600 0f09  .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507  ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169  umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67  n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75  /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 4865 6c6c 6f2e  rceFile...Hello.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800  java............
00000090: 1901 000c 4865 6c6c 6f2c 2057 6f72 6c64  ....Hello, World
000000a0: 0700 1a0c 001b 001c 0100 0548 656c 6c6f  ...........Hello
000000b0: 0100 106a 6176 612f 6c61 6e67 2f4f 626a  ...java/lang/Obj
000000c0: 6563 7401 0010 6a61 7661 2f6c 616e 672f  ect...java/lang/
000000d0: 5379 7374 656d 0100 036f 7574 0100 154c  System...out...L
000000e0: 6a61 7661 2f69 6f2f 5072 696e 7453 7472  java/io/PrintStr
000000f0: 6561 6d3b 0100 136a 6176 612f 696f 2f50  eam;...java/io/P
00000100: 7269 6e74 5374 7265 616d 0100 0770 7269  rintStream...pri
00000110: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61  ntln...(Ljava/la
00000120: 6e67 2f53 7472 696e 673b 2956 0021 0005  ng/String;)V.!..
00000130: 0006 0000 0000 0002 0001 0007 0008 0001  ................
00000140: 0009 0000 001d 0001 0001 0000 0005 2ab7  ..............*.
00000150: 0001 b100 0000 0100 0a00 0000 0600 0100  ................
00000160: 0000 0200 0900 0b00 0c00 0100 0900 0000  ................
00000170: 2500 0200 0100 0000 09b2 0002 1203 b600  %...............
00000180: 04b1 0000 0001 000a 0000 000a 0002 0000  ................
00000190: 0005 0008 0006 0001 000d 0000 0002 000e  ................

可以看到,二进制的最开始就是 ca fe ba be,这四个字节相当于被认为是java代码编译之后的标记。不同种的文件都会有不同的标记,例如png文件,JPEG文件等等。

意味着文件如果不是以ca fe ba be开头的,将不被jvm承认。

主版本号与副版本号

依旧参考Hello.java反编译出来的二进制:

00000000: cafe babe 0000 0034 001d 0a00 0600 0f09  .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507  ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829  .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e  V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169  umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67  n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75  /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 4865 6c6c 6f2e  rceFile...Hello.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800  java............
00000090: 1901 000c 4865 6c6c 6f2c 2057 6f72 6c64  ....Hello, World
000000a0: 0700 1a0c 001b 001c 0100 0548 656c 6c6f  ...........Hello
000000b0: 0100 106a 6176 612f 6c61 6e67 2f4f 626a  ...java/lang/Obj
000000c0: 6563 7401 0010 6a61 7661 2f6c 616e 672f  ect...java/lang/
000000d0: 5379 7374 656d 0100 036f 7574 0100 154c  System...out...L
000000e0: 6a61 7661 2f69 6f2f 5072 696e 7453 7472  java/io/PrintStr
000000f0: 6561 6d3b 0100 136a 6176 612f 696f 2f50  eam;...java/io/P
00000100: 7269 6e74 5374 7265 616d 0100 0770 7269  rintStream...pri
00000110: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61  ntln...(Ljava/la
00000120: 6e67 2f53 7472 696e 673b 2956 0021 0005  ng/String;)V.!..
00000130: 0006 0000 0000 0002 0001 0007 0008 0001  ................
00000140: 0009 0000 001d 0001 0001 0000 0005 2ab7  ..............*.
00000150: 0001 b100 0000 0100 0a00 0000 0600 0100  ................
00000160: 0000 0200 0900 0b00 0c00 0100 0900 0000  ................
00000170: 2500 0200 0100 0000 09b2 0002 1203 b600  %...............
00000180: 04b1 0000 0001 000a 0000 000a 0002 0000  ................
00000190: 0005 0008 0006 0001 000d 0000 0002 000e  ................

紧跟着ca fe ba be的四个字节是,00 00 00 34

00 00表示副版本号(Minor Version),00 34(值是52)表示主版本号(Major Version)

Java版本Major version
Java1.448
Java559
Java650
Java751
Java852
Java953

JVM运行时向下兼容的,意味着比方1.8的jdk,一看到主版本是高于52的,就直接报出异常来

常量池

常量池是一个最复杂最复杂的数据结构了,还是一样的套路,我们制造一个代码出来

public class Hello1 {

    public final boolean bool = true; //  1(0x01)
    public final char c = 'A';        // 65(0x41)
    public final byte b = 66; // 66(0x42)
    public final short s = 67;        // 67(0x43)
    public final int i = 68;  // 68(0x44)
    public final long l = Long.MAX_VALUE;
    public final double d = Double.MAX_VALUE;

    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

编译出来之后的二进制文件是

00000000: cafe babe 0000 0034 004e 0a00 1600 3509  .......4.N....5.
00000010: 0015 0036 0900 1500 3709 0015 0038 0900  ...6....7....8..
00000020: 1500 3909 0015 003a 0700 3b04 7f7f ffff  ..9....:..;.....
00000030: 0900 1500 3c07 003d 057f ffff ffff ffff  ....<..=........
00000040: ff09 0015 003e 0700 3f06 7fef ffff ffff  .....>..?.......
00000050: ffff 0900 1500 4009 0041 0042 0800 430a  [email protected].
00000060: 0044 0045 0700 4607 0047 0100 0462 6f6f  .D.E..F..G...boo
00000070: 6c01 0001 5a01 000d 436f 6e73 7461 6e74  l...Z...Constant
00000080: 5661 6c75 6503 0000 0001 0100 0163 0100  Value........c..
00000090: 0143 0300 0000 4101 0001 6201 0001 4203  .C....A...b...B.
000000a0: 0000 0042 0100 0173 0100 0153 0300 0000  ...B...s...S....
000000b0: 4301 0001 6901 0001 4903 0000 0044 0100  C...i...I....D..
000000c0: 0166 0100 0146 0100 016c 0100 014a 0100  .f...F...l...J..
000000d0: 0164 0100 0144 0100 063c 696e 6974 3e01  .d...D...<init>.
000000e0: 0003 2829 5601 0004 436f 6465 0100 0f4c  ..()V...Code...L
000000f0: 696e 654e 756d 6265 7254 6162 6c65 0100  ineNumberTable..
00000100: 046d 6169 6e01 0016 285b 4c6a 6176 612f  .main...([Ljava/
00000110: 6c61 6e67 2f53 7472 696e 673b 2956 0100  lang/String;)V..
00000120: 0a53 6f75 7263 6546 696c 6501 000b 4865  .SourceFile...He
00000130: 6c6c 6f31 2e6a 6176 610c 002d 002e 0c00  llo1.java..-....
00000140: 1700 180c 001b 001c 0c00 1e00 1f0c 0021  ...............!
00000150: 0022 0c00 2400 2501 000f 6a61 7661 2f6c  ."..$.%...java/l
00000160: 616e 672f 466c 6f61 740c 0027 0028 0100  ang/Float..'.(..
00000170: 0e6a 6176 612f 6c61 6e67 2f4c 6f6e 670c  .java/lang/Long.
00000180: 0029 002a 0100 106a 6176 612f 6c61 6e67  .).*...java/lang
00000190: 2f44 6f75 626c 650c 002b 002c 0700 480c  /Double..+.,..H.
000001a0: 0049 004a 0100 0c48 656c 6c6f 2c20 576f  .I.J...Hello, Wo
000001b0: 726c 6407 004b 0c00 4c00 4d01 0006 4865  rld..K..L.M...He
000001c0: 6c6c 6f31 0100 106a 6176 612f 6c61 6e67  llo1...java/lang
000001d0: 2f4f 626a 6563 7401 0010 6a61 7661 2f6c  /Object...java/l
000001e0: 616e 672f 5379 7374 656d 0100 036f 7574  ang/System...out
000001f0: 0100 154c 6a61 7661 2f69 6f2f 5072 696e  ...Ljava/io/Prin
00000200: 7453 7472 6561 6d3b 0100 136a 6176 612f  tStream;...java/
00000210: 696f 2f50 7269 6e74 5374 7265 616d 0100  io/PrintStream..
00000220: 0770 7269 6e74 6c6e 0100 1528 4c6a 6176  .println...(Ljav
00000230: 612f 6c61 6e67 2f53 7472 696e 673b 2956  a/lang/String;)V
00000240: 0021 0015 0016 0000 0008 0011 0017 0018  .!..............
00000250: 0001 0019 0000 0002 001a 0011 001b 001c  ................
00000260: 0001 0019 0000 0002 001d 0011 001e 001f  ................
00000270: 0001 0019 0000 0002 0020 0011 0021 0022  ......... ...!."
00000280: 0001 0019 0000 0002 0023 0011 0024 0025  .........#...$.%
00000290: 0001 0019 0000 0002 0026 0011 0027 0028  .........&...'.(
000002a0: 0001 0019 0000 0002 0008 0011 0029 002a  .............).*
000002b0: 0001 0019 0000 0002 000b 0011 002b 002c  .............+.,
000002c0: 0001 0019 0000 0002 000f 0002 0001 002d  ...............-
000002d0: 002e 0001 002f 0000 006e 0003 0001 0000  ...../...n......
000002e0: 0036 2ab7 0001 2a04 b500 022a 1041 b500  .6*...*....*.A..
000002f0: 032a 1042 b500 042a 1043 b500 052a 1044  .*.B...*.C...*.D
00000300: b500 062a 1208 b500 092a 1400 0bb5 000d  ...*.....*......
00000310: 2a14 000f b500 11b1 0000 0001 0030 0000  *............0..
00000320: 0026 0009 0000 0001 0004 0003 0009 0004  .&..............
00000330: 000f 0005 0015 0006 001b 0007 0021 0008  .............!..
00000340: 0027 0009 002e 000a 0009 0031 0032 0001  .'.........1.2..
00000350: 002f 0000 0025 0002 0001 0000 0009 b200  ./...%..........
00000360: 1212 13b6 0014 b100 0000 0100 3000 0000  ............0...
00000370: 0a00 0200 0000 0d00 0800 0e00 0100 3300  ..............3.
00000380: 0000 0200 34                             ....4

二进制看上去就复杂很多。

常量池的基本结构是这个样子的

struct {
    u2 constant_pool_count;    
    cp_info constant_pool[constant_pool_count-1];
}

意味着紧跟着主版本号的两个字节表达了常量池长度是多少,查看二进制,可以看到,两个字节是00 4e,意味着我们当前的class文件的constant_pool_count=78,但是真正的常量池条目录数最大是77。

这里有两个含义:

  1. 为什么是77
  2. 为什么最大是77

因为常量池是1开始的,0位有特殊含义,表达什么都没有的状态。因此总数是77,那为什么最大是77呢?因为类似long,double事实上是占用两个常量池位置的,因此真正的常量池条目数是<= 77的

而cp_info的结构可以用下面的伪代码表达

cp_info {
    u1 tag;    
    u1 info[];
}

tag指的是常量条目的类型,我在这里列一下:
image.png

不同的类型会有不同的tag值,u1 info[] 相当于和这个tag值绑定,有些tag值固定是2个字节,那么在解析class文件的时候就自动往后扣两个字节。接下来,我们分析一下所有的数据类型tag。

数值类型的常量

我们java领域内,所拥有的数值类型有 bool,byte, short, char, int, long, float, long

而在上图中可以看到,bool,byte, short, char类型是没有对应的tag的,那是因为在常量池里面统一使用int操作了。

2.3.1.1 CONSTANT_Integer_info类型

CONSTANT_Integer_info 结构可以使用下面的结构表示

CONSTANT_Integer_info {
    u1 tag;    
    u4 bytes;
}

因为这里已经确定了是CONSTANT_Integer_info,所以这里的tag是03,后面四个字节表达数值本身。

那么人肉翻译一下代码里面定义的常量:

    public final boolean bool = true; //  1(0x01)
    public final char c = 'A';        // 65(0x41)
    public final byte b = 66; // 66(0x42)
    public final short s = 67;        // 67(0x43)
    public final int i = 68;  // 68(0x44)
bool = 03 00 00 00 01
c = 03 00 00 00 41
b = 03 00 00 00 42
s = 03 00 00 00 43
i = 03 00 00 00 44

使用hex Fiend软件圈选出来

image.png

2.3.1.2 CONSTANT_Float_info类型

CONSTANT_Float_info类型的数据结构和CONSTANT_Integer_info数据是差不多的

CONSTANT_Float_info {
    u1 tag;    
    u4 bytes;
}

意味着,tag值是4,然后后面跟着4个字节表达数据,即 04 7F 7F FF FF
image.png

2.3.1.3 CONSTANT_Long_info类型

以下是 CONSTANT_Long_info数据类型

CONSTANT_Long_info {
    u1 tag;    
    u4 high_bytes;   
    u4 low_bytes;
}

可以看到数据本身会被切分为两个四字节存储,分别叫高位值与低位值

2.3.1.4 CONSTANT_Double_info类型

以下是 CONSTANT_Double_info 类型

CONSTANT_Double_info {
    u1 tag;    
    u4 high_bytes;   
    u4 low_bytes;
}

可以看到和CONSTANT_Long_info的存储结构一毛一样,只不过存储的数据本身格式会有很大区别。

字符串类型常量

这里将会涉及到两种类型的数据CONSTANT_String_info与个CONSTANT_Utf8_info

CONSTANT_Utf8_info存储了字符串真正的内容,而CONSTANT_String_info仅仅包含一个指向常量池中CONSTANT_Utf8_info常量类型的索引。

还是一样的套路,我们制造出用于说明的java代码

public class Hello2{

    public final String hello = "hello";
    public final String x = "\0";
    public final String y = "\uD83D\uDE02";// emoji 

标签:info,文件,java,字节,...,001,0000,class
From: https://blog.csdn.net/sinat_35426920/article/details/140491785

相关文章

  • 2024-07-17 前端项目中assets文件夹和public文件夹的区别(来源:GPT)
    在前端项目中,assets文件夹和public文件夹都扮演着存储静态资源的重要角色,但它们之间存在一些关键的区别。以下是对这两个文件夹区别的详细分析:assets文件夹内容与用途:assets文件夹通常用于存放项目中可能会变动的静态资源,如图片、样式表(CSS文件)、JavaScript脚本、字体文件等......
  • git如何使用分支b的某个文件夹替换main分支的相同路径
    在PyCharm中,如果你没有找到“Checkoutwith...”选项,可以使用以下方法从另一个分支提取特定文件夹或文件:方法1:使用“Git”工具窗口切换到main分支点击右下角的分支名称,选择main分支并切换。获取最新的更改在菜单中,选择VCS>UpdateProject...来确保你的main......
  • Java中的分布式文件系统设计与实现
    Java中的分布式文件系统设计与实现大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!一、引言分布式文件系统是支持大规模数据存储和访问的关键基础设施之一。本文将探讨在Java语言环境中设计和实现分布式文件系统的关键技术和策略。二、分布式文件系统的......
  • 什么是硬盘逻辑损坏和文件系统错误
    硬盘逻辑损坏和文件系统错误是硬盘使用过程中可能遇到的两种常见问题,它们各自具有不同的特征和影响。一、硬盘逻辑损坏硬盘逻辑损坏是指硬盘上的文件系统或数据结构出现问题,导致系统无法正常读取或处理硬盘上的数据。这种损坏通常不涉及硬盘的物理部件(如磁头、盘片等),而是由于软......
  • Office文件打不开如何处理
        在快节奏的现代办公环境中,MicrosoftOffice无疑是我们的得力助手,无论是起草报告、整理数据还是制作演示文稿,它都在背后默默支撑着我们的工作与学习。然而,当有一天,你突然发现重要的Office文档无法打开,屏幕上的错误信息仿佛一道无形的墙,阻隔了你与辛勤工作成果之间的......
  • Windows 10如何使用文件检查器工具修复受损文件
        在Windows10的日常使用中,系统文件的意外损坏或丢失可能会导致一系列问题,从程序崩溃到系统不稳定,严重时甚至影响计算机的正常启动。幸运的是,Windows10内置了一个强大的工具——系统文件检查器(SFC),专门用于扫描和修复系统文件的完整性。本篇文章将带领你深入了解如......
  • 将DBF文件(dBase, FoxPro等)中的数据转换到SQLite
    将DBF文件(dBase,FoxPro等)中的数据转换到SQLite,可遍历指定目录下所有的dbf文件。可参考以下程序,本程序参考了dbf-to-sqlite: #_*_coding:utf-8_*_'''@File:main.py@Time:2024/07/17@Author:LionGIS@Contact:[email protected]@Description:......
  • python--实验12 文件
    目录知识点第一部分:文件概述第二部分:文件的基本操作第三部分:目录管理第四部分:CSV文件读写第五部分:openpyxl等模块小结实验知识点第一部分:文件概述文件标识:找到计算机中唯一确定的文件。组成包括文件路径、文件名主干和文件扩展名。文件类型:区分了文本文件和二进......
  • 使用Tomcat当做一个简单的文件服务器
    背景:简介图片没地方存储,开始想直接存数据库,试了下,直接存效率也太低了,尝试转base64再存,还是不行.最后有大佬说之前有单独搭建过一个tomcat来存图片,尝试了一把,暂时作为解决方案了.(目前看来,这应该是最适合我目前的场景的方式了,方案太多了,要是条件允许,......
  • java导入excel数据,要求数据精度与文件一致
    最近应客户需求,导入excel表格,且要求数据精度和日期格式与文件一致。之前虽然做过导入导出的功能,但要求没有这么细致,因此在网上查找了大量的文件,找到了表格的cell.getCellStyle().getDataFormatString()这个属性,可以根据属性在程序里转换成自己需要的格式。publicStringgetC......