var constPool = [
"admin",
"123456",
"_",
"authResult"
];//首先,我们定义了一个常量池,这通常是用来集中存放程序运行时会用到的固定值。但并不是所有jsvmp都这么处理固定值部分的。处理这些固定值的方法有许多,固定值可能被加密、拆分、内联到字节码中,或者在运行时动态生成。这里只是为了举例,所以很简单。
var stack = [];//这里我们定义了一个数组 stack,用来模拟虚拟机的操作数栈,也可以叫工作栈。在 VM 执行字节码的过程中,很多指令都会把数据压入栈中,或者从栈中弹出数据进行计算。后续运行过程中,我们会重点观察每条指令是如何对这个工作栈进行入栈、出栈和取值操作的。
var regs = [];//寄存器数组,用于保存虚拟机运行过程中的中间变量或局部变量。
//注意!以上两个变量,在真实jsvmp中,大多数以单个字母,或者混淆变量名出现。
var bytecode = [0x01,0,0x02,0,0x01,1,0x02,1,0x03,0x02,2,0x04,0,0x01,2,0x05,0x04, 1,0x05,0x02,3,0x04,3,0x04,2,0x05,0x06,0x02,4,0x04,4,0x07,3,0xff];//自定义字节码数组
var ip = 0;//起始坐标,我们也可以叫指针,因为它起的就是指针的作用,那么通俗的来讲,它会告诉程序,现在我们走到了字节码数组中的哪一个位置了。当然在这里还是要说,各个jsvmp中的表现方式不同,要注意观察
opcode
紧接着,我们按照代码顺序向下分析,一个while循环,在ip没走完的时候,一直执行。
var op = bytecode[ip++];这样读出来的 op 叫 opcode。operation code,操作码,也就是“这条指令要做什么操作”的编号。
如果您刚接触jsvmp,那就会隐隐有这种疑惑。
VM 是怎么知道当前读出来的这个数字是“操作码”,而不是“参数值”的?
为什么 0x01 就一定对应 push const,而不是别的意思?
这都是因为,我们已经提前约定好的“指令格式”————>字节码数组
字节码数组通过ip走到了哪里,自然就会知道现在该干什么。冥冥之中,自有安排~
好,那我认为,我已经说明白了var op = bytecode[ip++];这行代码,简单来说,就是告诉程序,现在该执行哪个操作了。
堆栈操作
在学习核心知识之前,先在这里,为大家提前补习一些基础知识。
入栈与出栈
[] 纯文本查看复制代码
var stack = [];
stack.push(10)//stack = [10]
stack.push(20)//stack = [10,20]
stack.pop()//stack = [10]
var value = stack.pop();//value = 10 stack = []
查看栈顶
[] 纯文本查看复制代码
stack = [10,20,30]
var top = stack[stack.length - 1];//top = 30
//这种操作是只查看,不删除
case 0x01: {
//在第一次操作的时候,我们的ip已经在var op = bytecode[ip++];这里自增过了,此时id=1
var index = bytecode[ip++];//index = bytecode[1] = 0
//而后,ip++ ->ip = 2
stack.push(constPool[index]);
//将常量池中的第0号元素admin,入栈
//此时stack = ['admin']
break;
}
我们继续示例,以便于大家彻底理解stack工作栈的用途
紧接着,0x02, 0,意思是去case 0x02的地方
[] 纯文本查看复制代码
case 0x02: {
//ip已经在var op = bytecode[ip++];这里自增过了,此时id=3
var regIndex = bytecode[ip++];//regIndex = bytecode[3] = 0
//而后,ip++ ->ip = 4
regs[regIndex] = stack.pop();
//regs[0] = 'admin'
//此时stack = []
break;
}
接下来,我就不一一举例了,感兴趣的话可以继续模拟下去,在这里我们直接给出stack的变化
步骤
指令
操作
stack 变化
1
0x01, 0
push "admin"
["admin"]
2
0x02, 0
pop,存入 regs[0]
[]
3
0x01, 1
push "123456"
["123456"]
4
0x02, 1
pop,存入 regs[1]
[]
5
0x03
push Date.now()
[T]
6
0x02, 2
pop,存入 regs[2]
[]
7
0x04, 0
push regs[0]
["admin"]
8
0x01, 2
push "_"
["admin", "_"]
9
0x05
pop 两个值,相加
["admin_"]
10
0x04, 1
push regs[1]
["admin_", "123456"]
11
0x05
pop 两个值,相加
["admin_123456"]
12
0x02, 3
pop,存入 regs[3]
[]
13
0x04, 3
push regs[3]
["admin_123456"]
14
0x04, 2
push regs[2]
["admin_123456", T]
15
0x05
pop 两个值,相加
["admin_123456" + T]
16
0x06
pop,执行 btoa(),再 push
[H]
17
0x02, 4
pop,存入 regs[4]
[]
18
0x04, 4
push regs[4]
[H]
19
0x07, 3
pop,赋值给 window.authResult
[]
20
0xff
结束
[]
[] 纯文本查看复制代码
while (ip < bytecode.length) {
var op = bytecode[ip++];
switch (op) {
case 0x01: {
var index = bytecode[ip++];
stack.push(constPool[index]);
break;
}
case 0x02: {
var regIndex = bytecode[ip++];
regs[regIndex] = stack.pop();
break;
}
case 0x03: {
stack.push(Date.now());
break;
}
case 0x04: {
var regIndex = bytecode[ip++];
stack.push(regs[regIndex]);
break;
}
case 0x05: {
var b = stack.pop();
var a = stack.pop();
stack.push(String(a) + String(b));
break;
}
case 0x06: {
var value = stack.pop();
stack.push(btoa(value));
break;
}
case 0x07: {
var propIndex = bytecode[ip++];
var value = stack.pop();
window[constPool[propIndex]] = value;
break;
}
case 0xff:
return;
}
}
所以我们可以看到,这段 VM 的核心并不是浏览器直接执行原始业务逻辑,而是通过 bytecode + opcode handler + stack/regs 的方式,把原始逻辑重新解释执行了一遍。