写自己的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;
    }

运行配置:

运行结果:

相关推荐
「、皓子~5 分钟前
后台管理系统的诞生 - 利用AI 1天完成整个后台管理系统的微服务后端+前端
前端·人工智能·微服务·小程序·go·ai编程·ai写作
就改了8 分钟前
Ajax——在OA系统提升性能的局部刷新
前端·javascript·ajax
凌冰_10 分钟前
Ajax 入门
前端·javascript·ajax
AI大模型23 分钟前
LangGraph官方文档笔记(4)——提示聊天机器人
程序员·langchain·llm
京东零售技术25 分钟前
京东小程序JS API仓颉改造实践
前端
老A技术联盟34 分钟前
从小白入门,基于Cursor开发一个前端小程序之Cursor 编程实践与案例分析
前端·小程序
风铃喵游38 分钟前
构建引擎: 打造小程序编译器
前端·小程序·架构
sunbyte43 分钟前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ThemeClock(主题时钟)
前端·javascript·css·vue.js·前端框架·tailwindcss
小飞悟1 小时前
🎯 什么是模块化?CommonJS 和 ES6 Modules 到底有什么区别?小白也能看懂
前端·javascript·设计
浏览器API调用工程师_Taylor1 小时前
AOP魔法:一招实现登录弹窗的全局拦截与动态处理
前端·javascript·vue.js