最近发生太多不愉快的事, 还是看看源码压压惊(.这篇大概有很多错误.

1. 说在前面

Javac 大家(只是习惯用大家)应该用的很熟悉了.想我第一次接触 java 就是从 Javac 编译第一个程序的.到了今天,我时不时也会想这到底是怎么实现的?这个想法一直挥之不去.咦,跑题了(.

“代码编译的结果是从本地机器码转变成字节码,是存储格式发展的一小步,却是编程语言发展的一大步.”

这句话反复出现在《深入理解Java虚拟机》里.我倒是没有体会到什么发展,也可以理解为这是为了跨平台而做出的牺牲,虚拟机也为此出现.当然也有直接把 Java 源代码编译成机器码的编译器( AOT 编译器).先不管这个,从 Java 源代码到字节码到底经历了什么呢?肯定要分析处理源码的语句啊,这便是词法分析和语法分析,从而把源码变成了抽象语法树(AST).接着只要遍历语法树就得到了字节码指令流.emmm,以目前的垃圾我不可能实现的.那,先从简单的开始,这篇重点就是从源码验证一下Javac的编译结果.

2. 编写测试程序

1
2
3
4
5
6
7
8
9
public class Test {

    public int test() {
        int a = 100;
        int b = 200;
        int c = 400;
        return  (a + b) * c;
    }
}

这是个简单的测试程序,没什么好说的.接着用 Javac 编译他.然后打开这个字节码文件.

3. 简单的Javac源码

经过在Javac源码里的一番随便乱翻(划掉),找到了这个文件: com.sun.tools.javac.jvm.ByteCodes .这是一个接口,里面只有一些常量.像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public interface ByteCodes {

    /** Byte code instruction codes.
     */
    int illegal         = -1,
        nop             = 0,
        aconst_null     = 1,
        iconst_m1       = 2,
        iconst_0        = 3,
        iconst_1        = 4,
        ......
}

一打开这个文件,我就觉得很熟悉.这些都是基于栈的指令集.以后(在 JVM 里执行时)都是对 Java VM stack 进行操作的指令.

3.1 Javap 查看字节码指令

省略前面一些常量池里内容.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100
         2: istore_1
         3: sipush        200
         6: istore_2
         7: sipush        400
        10: istore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: iload_3
        15: imul
        16: ireturn
      LineNumberTable:
        line 16: 0
        line 17: 3
        line 18: 7
        line 19: 11
}
  • bipushsipush 都是将整型常量值推入栈顶的指令.

  • istore_1 是将栈顶的数出栈,并存入局部变量表的第一个位置.其他类似.

  • iload_1 则是将局部变量表的第一个位置的数推入栈顶.

  • iadd 将位于栈的前两位的数岀栈,相加,返回的结果推入栈顶. imul 类似,是乘法.

4. 验证

我们看到的字节码文件,里面是16进制数, 可以对照 Javac 的 Bytecodes 文件得到这些指令的十进制数.为了方便查找我做了如下的注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    public int test() {
        //         dec hex
        //bioush   16, 10
        //sipush   17, 11
        //istore_1 60, 3c
        //istore_2 61, 3d
        //iload_1  27, 1b
        int a = 100; //0x0064
        int b = 200; //0x00c8
        int c = 400; //0x0190
        return  (a + b) * c;
    }

字节码文件:

cafe babe 0000 0034 000f 0a00 0300 0c07

……

0011 1064 3c11 00c8 3d11 0190 3e1b 1c60

1d68 ac00 0000 0100 0700 0000 1200 0400

……

可以很明显的看出字节码文件里的16进制数和 Bytecodes.java 里的指令常量是一一对应的,跟用 Javap 查看的反编译结果也对应.

4.1 直接修改字节码

这样一来的话,是不是可以直接写字节码,而不是 Java 源代码了呢?当然可以,只有你熟悉各种指令(笑).不过简单的修改一下还是没问题的.我将原来的那行改成了:

1
1065 3c11 00c8 3d11 0190 3e1b 1c60

只是改了个变量,接下来反射调用一下.代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public static void main(String[] args) {
        HotSwapClassLoader loader = new HotSwapClassLoader();
        byte[] bytes = null;
        try {
            InputStream in = new
                FileInputStream("/home/lee/IdeaProjects/playGround/src/xyz/leezoom/playground/underjvm/classenegine8/Test.class");
            bytes = new byte[in.available()];
            in.read(bytes);
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        //load class from bytecode
        Class clazz = loader.loadClassFromByte(bytes);
        //invoke main method.
        Method method;
        try {
            method = clazz.getMethod("main", String[].class);
            method.invoke(null, new String[]{null});
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            System.out.println("Failed to invoke method: " + e.getLocalizedMessage());
        }

    }

修改之前:

修改之后: