玩转Java字节码(上)

  Java字节码(Byte-code)是指Java源代码编译而成的,供JVM虚拟机执行的代码。用文本编辑器打开将是一团乱码,用十六进制编辑器打开能勉强看懂头部一些规范,例如魔数,主次版本。而用/bin/javap“反编译”之后可以得到一个人类可读的代码段,类似于用Wireshark来分析cap数据包。

  要看懂这个文件必须要知道字节码规范,主要有以下两个表格内容构成。其中表1是Class文件的组成部分,各段依次排列,排列紧密,无多余的分隔符。其中u1~u8表示占用字节数,1表示1个字节,2表示2个字节,4字节,8字节。例如魔数:仅有1个,占用4个字节,位于文件的前4个字节。紧接着4个字节是minor_version,major_version各占用2个字节。接下来2个字节是常量池数量统计,意味着一个Class不能有超过65535个常量池(maybe)。接下来是常量池,常量池的格式由表2来定义,下一段再来分析。依次类推可以将整个Class文件分析出来。

表1Class文件组成

  常量池中的每一个常量项通常有2~3个项目组成。例如:CONSTANT_Utf8_info这一项,第1个字节是表示定义(tag),值为1,紧接着2个字节表示该项将占用的长度(length),意味着接下来的几个字节长度将是字符串的实际内容。

  再来分析下CONSTANT_Integer_info项,从下表可以看出头一个字节tag值为3,接下来4个字节安高位在前编码表示int值,所以在Java中int数据类型的最大值是去掉一个符号位的0x7FFFFFFF。Utf8,Integer,Float,Long,Double这5个都是类似的,直接以值的形式存储。其余常量项都是存储着引用值(index),分别指向这5个值或其它地方,这里不一一介绍了。

  等等java不是八大基本数据类型吗,这里怎么只有提到了4个,没错,小于int的都当作int处理了。也就是boolean,byte,char,short都“拉长”至int级别来对待。例如:boolean a = “true” 对应的字节码指令是:iconst_1,putfield a,将int型常量值1进栈赋值给变量a。

  介于编译器可能会做一些性能优化,例如int值超过2字节(32768)才会加入到常量项,小于2字节由sipush指令在运行时分配。

表2Class 14个常量项

  分析这玩意实在有些无聊,既然本文是以玩转为主题,自然就说点有趣的。首先一个简单的Java代码如下:

package classloader;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class Hello {
    public String sayHello(String name) {
        System.out.println(this.getClass().getName() + " -> " + name);
        return name;
    }
}

  这段代码自然再简单不过了,随便一个文本编辑器敲进去,javac编译,找个main方法调用完事。那能不能不编译直接调用?当然没问题,先不谈这个,先来看看编译后究竟是个什么样子。我们用WinHex来打开编译好的Class文件,截图如下:

Hello.class

  黑色标记的是全部的常量项tag值(一个个标出来,该是有多蛋疼~),自己可以对照表2慢慢分析。当然也可以借助javap来“反编译”下,输出更为直观的表格:

Constant pool:
   #1 = Utf8               classloader.Hello
   #2 = Class              #1             // "classloader.Hello"
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               sayHello
  #10 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #11 = Utf8               java/lang/System
  #12 = Class              #11            // java/lang/System
  #13 = Utf8               out
  #14 = Utf8               Ljava/io/PrintStream;
  #15 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
  #16 = Fieldref           #12.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Utf8               java/lang/StringBuilder
  #18 = Class              #17            // java/lang/StringBuilder
  #19 = Methodref          #18.#7         // java/lang/StringBuilder."<init>":()V
  #20 = Utf8               getClass
  #21 = Utf8               ()Ljava/lang/Class;
  #22 = NameAndType        #20:#21        // getClass:()Ljava/lang/Class;
  #23 = Methodref          #4.#22         // java/lang/Object.getClass:()Ljava/lang/Class;
  #24 = Utf8               java/lang/Class
  #25 = Class              #24            // java/lang/Class
  #26 = Utf8               getName
  #27 = Utf8               ()Ljava/lang/String;
  #28 = NameAndType        #26:#27        // getName:()Ljava/lang/String;
  #29 = Methodref          #25.#28        // java/lang/Class.getName:()Ljava/lang/String;
  #30 = Utf8               append
  #31 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #32 = NameAndType        #30:#31        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #33 = Methodref          #18.#32        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #34 = Utf8                ->
  #35 = String             #34            //  ->
  #36 = Utf8               toString
  #37 = NameAndType        #36:#27        // toString:()Ljava/lang/String;
  #38 = Methodref          #18.#37        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #39 = Utf8               java/io/PrintStream
  #40 = Class              #39            // java/io/PrintStream
  #41 = Utf8               println
  #42 = Utf8               (Ljava/lang/String;)V
  #43 = NameAndType        #41:#42        // println:(Ljava/lang/String;)V
  #44 = Methodref          #40.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #45 = Utf8               Code

  这里我仅放上常量池部分,这下很好对了把。Class文件就是这样一个紧凑结构,不愧是为嵌入式而打造的一门语言。回到之前的问题,有没有可能自己来生成字节码?答案是肯定的,下回分解。

本文链接地址:https://dorole.com/1559/

来自:Dorole's Blog

发布者

Steve

编程/摄影

《玩转Java字节码(上)》上有1条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

:wink: :-| :-x :twisted: :) 8-O :( :roll: :-P :oops: :-o :mrgreen: :lol: :idea: :-D :evil: :cry: 8) :arrow: :-? :?: :!: