写自己的JVM(0x9)

项目地址:

github.com/aprz512/wri...

Jvm在启动一个程序的时候,会涉及到非常多的内容,其中就有一些JNI方法。拿 System.out 来说,这个变量是 final 修饰的,按照一般情况来说,我们是无法更改这个变量的值的:

java 复制代码
public final static PrintStream out = null;

但是 System 类提供了一个 setOut 方法:

scss 复制代码
  public static void setOut(PrintStream out) {
        checkIO();
        setOut0(out);
    }

可以看出是使用了 native 方法来实现变量的赋值。

我们之前一直是使用 hack 的方式来打印输出:

scss 复制代码
if ("println".equals(methodRef.getName())) {
    print(methodRef, operandStack);
    operandStack.popRef();
    return;
}

如果想真正的实现 System.out.println 方法,我们就需要实现这些JNI方法。

本地方法实现

本地方法在class文件中没有Code属性,所以需要主动给maxStack和maxLocals字段赋值。

本地方法帧的操作数栈至少要能容纳返回值,为了简化代码,暂时给maxStack字段赋值为4。

因为本地方法帧的局部变量表只用来存放参数值,所以把argSlotCount赋给maxLocals字段刚好。

至于code字段,也就是本地方法的字节码,它本是没有字节的,但是我们需要模拟本地方法的调用,所以就自己设定一个操作符 0xFE,它表示 invokenative,返回指令则根据函数的返回值选择相应的返回指令。

我们在创建Method的时候,判断:

less 复制代码
if (method.isNative()) {
    method.injectCodeAttribute(methodDescriptor.getReturnType());
}

injectCodeAttribute 里面就加入两条指令:

arduino 复制代码
  private void injectCodeAttribute(String returnType) {
        this.maxStack = 4;
        this.maxLocals = argsSlotCount;
        switch (returnType.charAt(0)) {
            case 'V':
                this.code = new byte[]{(byte) 0xfe, (byte) 0xb1}; // return
                return;
            case 'D':
                this.code = new byte[]{(byte) 0xfe, (byte) 0xaf}; // dreturn
                return;
            case 'F':
                this.code = new byte[]{(byte) 0xfe, (byte) 0xae}; // freturn
                return;
            case 'J':
                this.code = new byte[]{(byte) 0xfe, (byte) 0xad}; // lreturn
                return;
            case 'L':
            case '[':
                this.code = new byte[]{(byte) 0xfe, (byte) 0xb0}; // areturn
                return;
            default:
                this.code = new byte[]{(byte) 0xfe, (byte) 0xac}; // ireturn
        }
    }

这样我们的 NativeMethod 就创建好了,但是还有一个问题,就是这个 code 里面没有逻辑,我们该如何实现该方法的具体逻辑呢?

本地方法的实现只能靠自己去翻译JNI代码。拿 System 类来说,它的 static 代码段里面就有一个 native 方法:

csharp 复制代码
  private static native void registerNatives();
    static {
        registerNatives();
    }

我们实现一个本地方法注册表:

csharp 复制代码
  public static void init() {
        NativeRegistry.getInstance().registerNativeMethod(
                "java/lang/System",
                "arraycopy",
                "(Ljava/lang/Object;ILjava/lang/Object;II)V",
                new ArrayCopy());
        NativeRegistry.getInstance().registerNativeMethod(
                "java/lang/System",
                "initProperties",
                "(Ljava/util/Properties;)Ljava/util/Properties;",
                new InitProperties());
        NativeRegistry.getInstance().registerNativeMethod(
                "java/lang/System",
                "registerNatives",
                "()V",
                new EmptyNativeMethod());
    }

注册的时候需要传递一个实现了 NativeMethod 接口的类,这个类里面就是具体的实现逻辑:

ini 复制代码
  public static class ArrayCopy implements NativeMethod {

        @Override
        public void invoke(StackFrame frame) {
            LocalVariableTable localVariableTable = frame.getLocalVariableTable();
            ArrayObject src = (ArrayObject) localVariableTable.getRef(0);
            int srcPos = localVariableTable.getInt(1);
            ArrayObject dst = (ArrayObject) localVariableTable.getRef(2);
            int dstPos = localVariableTable.getInt(3);
            int length = localVariableTable.getInt(4);

            if (src == null || dst == null) {
                throw new MyJvmException("java.lang.NullPointerException");
            }

            if (!checkArrayCopy(src, dst)) {
                throw new MyJvmException("java.lang.ArrayStoreException");
            }

            if (srcPos < 0 || dstPos < 0 || srcPos + length > src.getArrayLength()
                    || dstPos + length > dst.getArrayLength()) {
                throw new MyJvmException("java.lang.IndexOutOfBoundsException");
            }

            for (int i = 0; i < length; i++) {
                dst.setArrayElement(dstPos + i, src.getArrayElement(srcPos + i));
            }
        }

当执行到 InvokeNative 指令的时候,我们根据方法签名去匹配本地方法:

ini 复制代码
  @Override
    public void execute(StackFrame frame) {
        MyMethod myMethod = frame.getMyMethod();
        String thisClassName = myMethod.getMyClass().getThisClassName();
        String methodName = myMethod.getName();
        String methodDescriptor = myMethod.getDescriptor();
        NativeMethod nativeMethod = NativeRegistry.getInstance().findNativeMethod(thisClassName, methodName, methodDescriptor);
        if (nativeMethod == null) {
            String methodInfo = thisClassName + "." + methodName + methodDescriptor;
            throw new MyJvmException("java.lang.UnsatisfiedLinkError: " + methodInfo);
        }
        nativeMethod.invoke(frame);
    }

这样,本地方法就搞定了。

但是很蛋疼的是,JVM在启动的时候会调用到很多的本地方法。所以实现起来特别的累,到现在项目里面还有很多本地方法没有实现,后面才会慢慢补上。不过话说回来,实现这些本地方法对理解项目也没多少帮助是,属于吃力不讨好。

反射实现

前面在实现运行时数据区的时候,创建了两个类:MyClass 和 MyObject。

对于 String 的字节码文件来说,MyClass 表示的是String.class。MyObject 表示的是一个 String 对象。

但是对于 Class 的字节码来说,MyClass 表是的是 Class.class 。MyObject 表示的是 String.class 或者其他的 clas。

嗯,可能比较难理解。没关系,多看几遍就自然懂了。

为了记录 MyObject 与 MyClass 的关系,我们使用 extra 字段来建立联系。

每次MyClassLoader加载类的时候,我们都建立如下联系:

ini 复制代码
    MyClass classValue = loadedClasses.get("java/lang/Class");
        if (classValue != null) {
            MyObject classObject = classValue.newObject();
            classObject.setExtra(loadedClass);
            loadedClass.setJClass(classObject);
        }

这样,就可以正确的运行 String.class 这样的代码了。

代码测试

csharp 复制代码
  public static void main(String[] args) {
        System.out.println(void.class.getName()); // void
        System.out.println(boolean.class.getName()); // boolean
        System.out.println(byte.class.getName()); // byte
        System.out.println(char.class.getName()); // char
        System.out.println(short.class.getName()); // short
        System.out.println(int.class.getName()); // int
        System.out.println(long.class.getName()); // long
        System.out.println(float.class.getName()); // float
        System.out.println(double.class.getName()); // double
        System.out.println(Object.class.getName()); // java.lang.Object
        System.out.println(int[].class.getName()); // [I
        System.out.println(int[][].class.getName()); // [[I
        System.out.println(Object[].class.getName()); // [Ljava.lang.Object;
        System.out.println(Object[][].class.getName()); // [[Ljava.lang.Object;
        System.out.println(Runnable.class.getName()); // java.lang.Runnable
        System.out.println("abc".getClass().getName()); // java.lang.String
        System.out.println(double[].class.getName()); // [D
        System.out.println(String[].class.getName()); //[Ljava.lang.String;
    }

运行配置:

运行结果:

相关推荐
吕彬-前端18 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白39 分钟前
react hooks--useCallback
前端·react.js·前端框架
恩婧1 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog1 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川1 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试
森叶1 小时前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
drebander1 小时前
ubuntu 安装 chrome 及 版本匹配的 chromedriver
前端·chrome
软件技术NINI1 小时前
html知识点框架
前端·html
深情废杨杨1 小时前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS1 小时前
【vue3】vue3.3新特性真香
前端·javascript·vue.js