Java字节码(.class文件)的代码解析

简介:

Java二进制指令代码以以下格式紧凑排列(opcode占一个字节):

opcode operand*

除了tableswitch和lookupswitch两条指令中间存在填充字节以外,其他指令都没有填充字节,即使在两条指令之间也没有。因而在读取指令的时候,要根据指令的定义读取。 

通过对上面Java指令集的分析可以知道,Java指令集中很大一部分没有操作数,因而对这部分指令,只需要读取一个字节的操作码,将操作码映射成助记符即可。 

而对其他带操作数的指令,则需要根据不同类型分析(由于apache中的bcel(Binary Code Engineering Library)对字节码的支持,操作码和助记符的映射可以用com.sun.org.apache.bcel.internal.Constats中提供的映射表数组来完成)。

1.       处理两条特殊的指令tableswitch和lookupswitch指令。

对这两条指令,首先都要去掉填充字符以使defaultbyte1索引号是字对齐的。

    private static void make4ByteAlignment(ByteSequence codes) {

       int usedBytes = codes.getIndex() % 4;

       int paddingBytes = (usedBytes == 0) ? 0 : 4 - usedBytes;

       for(int i = 0;i < paddingBytes;i++) {

           codes.readByte();

       }

}

对tableswitch指令,读取defaultoffset值,最小项的值,最大项的值以及在最小项和最大项之间每一项的offset值。并且将读取到的offset值和当前指令的基地址相加:

           int defaultOffset1 = baseOffset + codes.readInt();

           builder.append("\tdefault = #" + defaultOffset1);

           int low = codes.readInt();

           int high = codes.readInt();

           int npair1 = high - low + 1;

           builder.append(", npairs = " + npair1 + "\n");

           for(int i = low;i <= high;i++) {

              int match = i;

              offset = baseOffset + codes.readInt();

              builder.append(String.format("\tcase %d : #%d\n", match, offset));

        }

 

对lookupswitch指令,读取defaultoffset值,键值对数值(npairs),以及npairs对的键值对,将得到的offset值和当前指令的基地址相加:

           int defaultOffset2 = baseOffset + codes.readInt();

           builder.append("\tdefault = #" + defaultOffset2);

           int npairs2 = codes.readInt();

           builder.append(", npairs = " + npairs2 + "\n");

           for(int i = 0;i < npairs2;i++) {

              int match = codes.readInt();

              offset = baseOffset + codes.readInt();

              builder.append(String.format("\tcase %d : #%d\n", match, offset));

        }

 

2.       所有条件跳转指令都有两个字节的偏移量操作数(if<cond>, if_icmp<cond>, ifnull, ifnonnull, if_acmp<cond>)。无条件跳转指令goto和子例程跳转指令jsr也都是两个字节的偏移量作为操作数。

offset = baseOffset + codes.readShort();

builder.append(String.format("\t\t#%d\n", offset));

 

3.       对宽偏移量的跳转指令goto_w和子例程跳转指令jsr_w的操作数是四个字节的偏移量。

offset = baseOffset + codes.readInt();

builder.append(String.format("\t\t#%d\n", offset));

 

4.       wide指令,则继续读取下一条指令,并将wide参数设置为true。

byteCodeToString(codes, pool, verbose, true);

 

5.       还有一些指令值以一个字节的局部变量索引号作为操作数的,如果有wide修饰,则用两个字节作为操作数,代表局部变量索引号。这样的指令有:aload, iload, fload, lload, dload, astore, istore, fstore, lstore, dstore, ret。

if(wide) {

    index = codes.readUnsignedShort();

} else {

    index = codes.readUnsignedByte();

}

builder.append(String.format("\t\t%%%d\n", index));

6.       iinc指令,以一个字节的局部变量索引号和一个自己的常量作为参数;如果以wide修饰,则该指令的局部变量索引号和常量都占两个字节。

    if(wide) {

       index = codes.readUnsignedShort();

       constValue = codes.readShort();

    } else {

       index = codes.readUnsignedByte();

       constValue = codes.readByte();

    }

builder.append(String.format("\t\t%d %d\n", index, constValue));

 

7.       对象操作指令,它们的操作数都是常量池中的索引,长度为两个字节。指向CONSTANT_Class_info类型的结构,这些指令有new, checkcast, instanceof, anewarray。

index = codes.readUnsignedShort();

builder.append("\t\t" + pool.getClassInfo(index).toInstructionString(verbose) + "\n");

 

8.       所有字段操作指令,它们的操作数都是常量池中的索引,长度为两个字节。指向CONSTANT_Fieldref_info类型结构,这些指令有getfield, putfield, getstatic, putstatic。

index = codes.readUnsignedShort();

builder.append("\t\t" + pool.getFieldRefInfo(index).toInstructionString(verbose) + "\n");

 

9.       非接口方法调用指令,也都是以两个字节的索引号作为操作数,指向常量池中的CONSTANT_Methodref_info类型结构,这些指令有invokespecial, invokevirtual, invokestatic。

index = codes.readUnsignedShort();

builder.append("\t\t" + pool.getMethodRefInfo(index).toInstructionString(verbose) + "\n");

 

10.   接口方法调用指令invokeinterface,它有四个字节的操作数,前两个字节为常量池的索引号,指向CONSTANT_InterfaceMethodref_info类型,第三个字节为count,表示参数的字节数,最后一个字节为0值。

index = codes.readUnsignedShort();

int nargs = codes.readUnsignedByte(); //Historical, redundant

builder.append("\t\t" + pool.getInterfaceMethodRefInfo(index).toInstructionString(verbose));

builder.append(" : " + nargs + "\n");

codes.readUnsignedByte(); //reserved should be zero

 

11.   基本类型的数组创建指令newarray,它的操作数为一个字节的类型标识。

String type = Constants.TYPE_NAMES[codes.readByte()];

builder.append(String.format("\t\t(%s)\n", type));

 

12.   多维数组的创建指令multianewarray,它有三个字节的操作数,前两个字节为索引号,指向CONSTANT_Class_info类型,表示数组的类型,最后一个字节指定数组的维度。

index = codes.readUnsignedShort();

int dimensions = codes.readUnsignedByte();

builder.append(String.format("\t\t%s (%d)\n", pool.getClassInfo(index).getName(), dimensions));

 

13.   常量入栈指令ldc,以一个字节的索引号作为参数,指向CONSTANT_Integer_info、CONSTANT_Float_info、CONSTANT_String_info、CONSTANT_Class_info类型,表示要入栈的常量值(int类型值、float类型值、String引用类型值或对象引用类型值)。

index = codes.readUnsignedByte();

builder.append("\t\t" + pool.getPoolItem(index).toInstructionString(verbose) + "\n");

 

14.   宽索引的常量入栈指令ldc_w,以两个字节的索引号作为参数,指向CONSTANT_Integer_info、CONSTANT_Float_info、CONSTANT_String_info、CONSTANT_Class_info类型,表示要入栈的常量值(int类型值、float类型值、String引用类型值或对象引用类型值)。

index = codes.readUnsignedShort();

builder.append("\t\t" + pool.getPoolItem(index).toInstructionString(verbose) + "\n");

 

15.   宽索引的常量入栈指令ldc2_w,以两个字节的索引号作为参数,指向CONSTANT_Long_info、CONSTANT_Double_info类型,表示要入栈的常量值(long类型值、double类型值)。

index = codes.readUnsignedShort();

builder.append("\t\t" + pool.getPoolItem(index).toInstructionString(verbose) + "\n");

 

16.   bipush指令,以一个字节的常量作为操作数。

byte constByte = codes.readByte();

builder.append(“\t” + constByte);

 

17.   sipush指令,以两个字节的常量作为操作数。

short constShort = codes.readShort();

builder.append(“\t” + constShort);

 

以上还有一些没有完成的代码,包括字段(方法)的签名和描述符没有解析,有一些解析的格式还需要调整等。不管怎么样,总体的结构就是这样了,其它的都是细节问题,这里不讨论了。 

参见bcel项目的org.apache.bcel.classfile.Utility类.

相关文章
|
12月前
|
Java 开发工具
【Azure Storage Account】Java Code访问Storage Account File Share的上传和下载代码示例
本文介绍如何使用Java通过azure-storage-file-share SDK实现Azure文件共享的上传下载。包含依赖引入、客户端创建及完整示例代码,助你快速集成Azure File Share功能。
731 6
|
12月前
|
Java 数据处理 API
为什么你的Java代码应该多用Stream?从循环到声明式的思维转变
为什么你的Java代码应该多用Stream?从循环到声明式的思维转变
453 115
|
12月前
|
安全 Java 编译器
为什么你的Java代码需要泛型?类型安全的艺术
为什么你的Java代码需要泛型?类型安全的艺术
354 98
|
12月前
|
安全 Java 容器
告别空指针噩梦:Optional让Java代码更优雅
告别空指针噩梦:Optional让Java代码更优雅
595 94
|
12月前
|
Java 编译器 API
java最新版和java8的区别,用代码展示
java最新版和java8的区别,用代码展示
768 43
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
629 2
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
989 140
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
1675 29
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
705 4
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS