PC 指针
PC 指针指向当前方法中运行的 Code 行号
主要服务于一些非顺序跳转指令:
- 条件语句的分支跳转
- 循环语句的跳转
- 异常分支的跳转
- debug 行号控制
行号表
行号表记录了行号和代码 PC 指针的对应关系
主要服务于:
- 异常抛出代码的定位
- debug 单步调试的定位
//行号 typedef struct _line_number { u16 start_pc; u16 line_number; } LineNumberTable
指令序列
指令序列在一个方法中是一个顺序排列的指令集合
解释器从指令序列中取址执行。
方法执行流程
准备工作
//准备方法栈 Runtime *runtime = runtime_create_inl(pruntime); runtime->method = method; runtime->clazz = clazz; while (clazz->status < CLASS_STATUS_CLINITING) { class_clinit(clazz, runtime); } s32 method_sync = method->access_flags & ACC_SYNCHRONIZED; // if (utf8_equals_c(method->name, "getMethod")) { // s32 debug = 1; // } //操作数栈 RuntimeStack *stack = runtime->stack; if (!(method->access_flags & ACC_NATIVE)) { //拿出 Code CodeAttribute *ca = method->converted_code; if (ca) { //初始化本地变量 localvar_init(runtime, ca->max_locals); LocalVarItem *localvar = runtime->localvar; //方法参数进入本地变量 _stack2localvar(method, localvar, stack); s32 stackSize = stack->size; //如果方法是同步的,加锁 if (method_sync)_synchronized_lock_method(method, runtime); u8 *opCode = ca->code; runtime->ca = ca; JavaThreadInfo *threadInfo = runtime->threadInfo; //调试相关 do { runtime->pc = opCode; u8 cur_inst = *opCode; if (java_debug) { //breakpoint if (method->breakpoint) { jdwp_check_breakpoint(runtime); } //debug step if (threadInfo->jdwp_step.active) {//单步状态 threadInfo->jdwp_step.bytecode_count++; jdwp_check_debug_step(runtime); } } //process thread suspend if (threadInfo->suspend_count) { if (threadInfo->is_interrupt) { ret = RUNTIME_STATUS_INTERRUPT; break; } check_suspend_and_pause(runtime);
取指执行
这个 opCode 就是 pc 指针
这里用 Switch 分发,因为 Switch 直接使用 CPU 指令 跳转效率高,因此被称为 Switch 解释器。
/* ==================================opcode start =============================*/ #ifdef __JVM_DEBUG__ s64 inst_pc = runtime->pc - ca->code; #endif JUMP_TO_IP(cur_inst); switch (cur_inst) { label_nop: case op_nop: { #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); jvm_printf("nop\n"); #endif opCode += 1; break; } label_aconst_null: case op_aconst_null: {} case op_xxxxx:{} ........
Native 方法
如果待执行的是一个 native 方法
具体会在 JNI 篇详细描述
//本地方法 localvar_init(runtime, method->para_slots);//可能有非静态本地方法调用,因此+1 _stack2localvar(method, runtime->localvar, stack); //缓存调用本地方法 if (!method->native_func) { //把本地方法找出来缓存 java_native_method *native = find_native_method(utf8_cstr(clazz->name), utf8_cstr(method->name), utf8_cstr(method->descriptor)); if (!native) { Instance *exception = exception_create_str(JVM_EXCEPTION_NOSUCHMETHOD, runtime, utf8_cstr(method->name)); push_ref(stack, (__refer) exception); ret = RUNTIME_STATUS_EXCEPTION; } else { method->native_func = native->func_pointer; } } if (method->native_func) { if (method_sync)_synchronized_lock_method(method, runtime); ret = method->native_func(runtime, clazz); if (method_sync)_synchronized_unlock_method(method, runtime); } // if (utf8_equals_c(method->name, "nvgTextGlyphPositionsJni")) { // int debug = 1; // } localvar_dispose(runtime)
JVM 指令
JVM 每一个指令基本都有几个类似的指令,比如像iconst、lconst、fconst、dconst 这些主要是针对不同的类型(int、long、float、double),将对应类型的值push到栈顶,其他指令类似。
JVM 指令大约可以分为 9 种:
- 本地变量操作指令
- 栈操作指令
- 常量操作指令
- 算术和逻辑操作指令
- 转换指令
- 对象,字段,方法操作指令
- 数组操作指令
- 跳转指令
- return
基本指令
x 有 i,l,f,d, a 代表(int、long、float、double、引用)
| 指令 | 描述 |
| xconst_n | x 型常量值n进栈 |
| bipush | 将一个byte型常量值推送至栈顶 |
| xstore_n | 将栈顶x型数值存入第n个局部变量 |
| xadd | 栈顶两x型数值相加,并且结果进栈 |
| return | 当前方法返回void |
| getstatic | 获取指定类的静态域,并将其值压入栈顶 |
| putstatic | 为指定的类的静态域赋值 |
| invokevirtual | 调用实例方法 |
| invokespecial | 调用超类构造方法、实例初始化方法、私有方法 |
| invokestatic | 调用静态方法 |
| invokeinterface | 调用接口方法 |
| new | 创建一个对象,并且其引用进栈 |
| newarray | 创建一个基本类型数组,并且其引用进栈 |
本地变量操作指令
该指令负责操作数栈和本地变量表的数据交互工作,主要是
- 从本地变量表中取出某值压入操作数栈(只是复制,不会清空本地变量表中的值)
- 从操作数栈中弹出值到本地变量表中(会清空操作数栈中该值)
- 这里举个常见的例子:
依然是 c = a + b
- 首先 a 和 b 的值在本地变量表中
- 第一步用 load 指令将 a 和 b 从本地变量中压入操作数栈
- 执行 add 指令,add 指令将操作数栈的栈顶两个值相加并清空这两个操作数,产生的结果压入操作数栈顶
- 最后用 store 指令将运算结果存到本地变量表的 c 中
和上面一样,为了区分操作数类型,指令也根据不同类型开头
以 load 为例:
xload_n(n = 0~3)
x 有 i,l,f,d, a 代表(int、long、float、double、引用)
n 代表局部变量表中第 n 槽的值,这里取 0-3 ,这样就可以节省很多操作数所占用的字节码空间。
当 n 超过 3 时,则使用 xload n 这种指令 + 一元操作数的方式。
- VM 代码
static inline u8 *_op_ifload_n(u8 *opCode, RuntimeStack *stack, LocalVarItem *localvar, Runtime *runtime, s32 i) { Int2Float i2f; //从本地变量中 get 到 i2f.i = localvar_getInt(localvar, i); #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); jvm_printf("if_load_%d: push localvar(%d)= [%x]/%d/%f \n", i, i, i2f.i, i2f.i, i2f.f); #endif //push 到操作数栈 push_int(stack, i2f.i); opCode += 1; return opCode;
栈操作指令
该指令主要是对操作数栈内的一些操作
- 弹出某些值
- 复制栈中值到栈内
- 栈内某些值的交换
- 这里以复制指令 dup 为例,引用 new 对象的一个经典案例:
- Java Code
A a = new A();
- Byte Code
// operand stack: // ... new A // ..., ref dup // ..., ref, ref invokespecial A.<init>()V // ..., ref astore_
这里 dup 的必要性就体现出来了
当 new 完 A 后,new 指令将实例引用压入栈顶
紧接着就会调用 A 的无参构造函数,而 invokespecial 会清空栈顶的引用,这样的话接下来将 A 实例存到本地变量 a 的操作将无法完成,所以在调用 invokespecial 之前需要将实例引用复制一份。
- VM 代码
case op_dup: { StackEntry entry; //取得操作数栈栈顶的值 peek_entry(stack, &entry, stack->size - 1); //将该值再压入操作数栈 push_entry(stack, &entry); #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); jvm_printf("dup\n"); #endif opCode += 1; break;
常量操作指令
该指令和简单,就是将我们程序中定义的各种常量入操作数栈已准备接下来的运算而已,和前面一样也需要区分常量的类型以及值的范围
以 int 为例:
当int取值-1~5采用 iconst 指令,取值-128~127采用bipush指令,取值-32768!32767采用sipush指令,取值-2147483648~2147483647采用 ldc 指令。
- VM 代码
case op_bipush: { //此行 code 的第二个元素就是常量操作数 s32 value = (s8) opCode[1]; //常量入栈 #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); jvm_printf("bipush a byte %d onto the stack \n", value); #endif opCode += 2; break;
算术和逻辑操作指令
该指令用于运算符运算和逻辑操作
- 加减乘除
- 与或操作
- 移位操作
- 大小相等比较等
- 与前面类似,不同数据类型也有不同的指令
以加法 IADD 为例:
弹出操作数栈顶两个操作数,相加后压入操作数栈顶
case op_iadd: { s32 value1 = pop_int(stack); s32 value2 = pop_int(stack); s32 result = value1 + value2; #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); jvm_printf("iadd: %d + %d = %d\n", value1, value2, result); #endif push_int(stack, result); opCode += 1; break;
lcmp 比较指令
弹出操作数比较
相等则结果为 0,大于则为 1,小于则为 – 1
case op_lcmp: { s64 value1 = pop_long(stack); s64 value2 = pop_long(stack); s32 result = value2 == value1 ? 0 : (value2 > value1 ? 1 : -1); #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); jvm_printf("lcmp: %llx cmp %llx = %d\n", value2, value1, result); #endif push_int(stack, result); opCode += 1; break;
转换指令
各种类型强转的指令
比如
Int -> Float
Float -> Int
等等
case op_f2i: { f32 value1 = pop_float(stack); s32 result = (s32) value1; #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); jvm_printf("f2i: %d <-- %f\n", result, value1); #endif push_int(stack, result); opCode += 1; break; }
对象,字段,方法操作
这类指令基本是 Java 这类语言特有的
- Field 操作,Get/Set
- 方法调用,各种 Invoke
- InstanceOf 操作符
- New Instance
- 同步块的进和出
Field 操作
有 get/set field 和对应 static field 的 get/set static
Get Filed
- 从 opcode 中获取 Field 引用在类常量池中的 index
- 从操作数栈中弹出 Field 所在的对象实例,为空则抛出空指针
- 尝试直接从缓存中获取 Field
- 失败则先找到 Field 引用常量,再找到 Field
- 根据 Field 和实例加载值
- 如果是 Field 是原子则使用内存屏障
- 最后根据值的不同类型把值压入操作数栈
case op_getfield: { //从 Code 中获取 Field 的 Index Short2Char s2c; s2c.c1 = opCode[1]; s2c.c0 = opCode[2]; //Field 所在的对象 Instance *ins = (Instance *) pop_ref(stack); if (!ins) { //如果对象为空,则抛出空指针异常 Instance *exception = exception_create(JVM_EXCEPTION_NULLPOINTER, runtime); push_ref(stack, (__refer) exception); ret = RUNTIME_STATUS_EXCEPTION; } else { //先从前面加载的缓存中获取目标 Field 的信息 FieldInfo *fi = class_get_constant_fieldref(clazz, s2c.s)->fieldInfo; if (!fi) { //如果是空,那么该段应该没有加载过,先获取引用常量,然后通过引用常量找到真正的 Field ConstantFieldRef *cfr = class_get_constant_fieldref(clazz, s2c.s); fi = find_fieldInfo_by_fieldref(clazz, cfr->item.index, runtime); cfr->fieldInfo = fi; } //从目标对象中获取 Field 值的指针 c8 *ptr = getInstanceFieldPtr(ins, fi); //如果该 Field 是原子的 if (fi->isvolatile) { //那么设置内存屏障,强制从内存中读取 barrier(); } if (fi->isrefer) { //如果是引用类型 push_ref(stack, getFieldRefer(ptr)); } else { // check variable type to determine s64/s32/f64/f32 s32 data_bytes = fi->datatype_bytes; //基本类型,只要关注大小 switch (data_bytes) { case 4: { push_int(stack, getFieldInt(ptr)); break; } case 1: { push_int(stack, getFieldByte(ptr)); break; } case 8: { push_long(stack, getFieldLong(ptr)); break; } case 2: { if (fi->datatype_idx == DATATYPE_JCHAR)push_int(stack, getFieldChar(ptr)); else push_int(stack, getFieldShort(ptr)); break; } } } #if _JVM_DEBUG_BYTECODE_DETAIL > 5 invoke_deepth(runtime); StackEntry entry; peek_entry(stack, &entry, stack->size - 1); s64 v = entry_2_long(&entry); jvm_printf("%s: push %s.%s[%llx]\n", "getfield", utf8_cstr(clazz->name), utf8_cstr(fi->name), (s64)(intptr_t)ptr, v); #endif } opCode += 3; break;
Set Field
基本类似
if (fi->isrefer) {//垃圾回收标识 setFieldRefer(ptr, entry_2_refer(&entry)); } else { s32 data_bytes = fi->datatype_bytes; //非引用类型 switch (data_bytes) { case 4: { setFieldInt(ptr, entry_2_int(&entry)); break; } case 1: { setFieldByte(ptr, entry_2_int(&entry)); break; } case 8: { setFieldLong(ptr, entry_2_long(&entry)); break; } case 2: { setFieldShort(ptr, entry_2_int(&entry)); break; } } }