简版:https://www.cnblogs.com/index-html/p/6492418.html

前言

前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念。为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学。

于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript,这样就能在浏览器中,直接运行等价的逻辑。

为了简单起见,这里选择古董级 CPU —— MOS 6502

本系列陆续更新了 8 篇,前面几篇只是理论分析:

原本只打算遐想一下,分析下可行性而已。不过,后来发现实现也不难,于是又补了两篇:

6502

MOS 6502 是一款经典的 CPU,在上世纪 80 年代十分流行。

例如 Atari、Apple II,还有国内的文曲星,都配置了这个系列的 CPU。小时候常玩的 FC 红白机,也是相同的指令集。

网上相关的文章也非常多,这里收集了一些:

甚至还有在线模拟器:

事实上,模拟器的原理是很简单的:读取一条指令,做相应的操作;然后再读取下一条指令。。。参照文档实现即可。

do {
opcode = memory[pc++] switch (opcode) {
case 0xA9: // LDA
...
case 0x85: // STA
...
case 0xE6: // INC
...
....
}
} while (...)

模拟虽然简单,但有个很大的缺点:效率低。模拟一个指令,需要很多额外操作 —— 那些原本是硬件的工作,现在要用软件来完成,显然会慢得多。

不过,我们的目标并非模拟,而是翻译 —— 在程序运行前,把「虚拟指令」翻译成相应的本地「原生指令」,这样就能直接运行,无需模拟,效率自然大幅提升。

在浏览器层面,JavaScript 就是原生指令。那么,能否将 6502 翻译成 JavaScript 呢?下面开始探索。。。

硬件实现

6502 CPU 有三个 8 位寄存器 A、X、Y,我们用 JS 变量来表示:

var A = 0, X = 0, Y = 0;

至于「状态寄存器」SR,为了直观起见,分别用单独的 bool 变量表示每一位:

// SR: NV-BDIZC
// bit 76543210 var SR_N = false,
SR_V = false,
...
SR_C = false;

其他诸如「栈寄存器」、「指令计数器」,这里暂时先省略。

6502 的地址总线有 16 位,最多能访问 64K 的空间。数据总线 8 位,因此用一个 Uint8Array 就能实现内存:

var MEM = new Uint8Array(65536);

这里假设把整个地址空间都用做 RAM,事实上屏幕、键盘等 IO 交互,还会占用一些地址空间。

尝试翻译

现在,尝试翻译第一条指令:

STA 100

STA 即 “Store A”,将 A 写入存储 —— 写到第 100 号位置。对应的 JS 即:

MEM[100] = A;

很简单吧。下面翻译第二条指令:

LDA #123

LDA 即 “Load A”,给 A 赋值,# 表示立即数。因此,生成的 JS 的就是:

A = 123;
SR_Z = (123 == 0);
SR_N = (123 > 127);

稍了解汇编的都知道,修改寄存器的同时,还得更新状态标志。SR_Z 表示结果是否为零;SR_N 表示最高位(符号位)是否为 1。

这时「翻译」的优势就体现出来了。因为 123 == 0 和 123 > 127 都是常量计算,所以预先就能得出结果:

A = 123;
SR_N = false;
SR_Z = false;

相比模拟,翻译能减少运行时的计算量。如果有多个指令,效果则更明显,例如:

LDX 10
INX

翻译成如下 JS 代码:

X = MEM[10];              // LDX 10
SR_Z = (X == 0);
SR_N = (X > 127); X = (X + 1) & 0xff; // INX (X 自增)
SR_Z = (X == 0);
SR_N = (X > 127);

这里虽然没有预先计算,但不要忘了,JavaScript 最终还得交给浏览器解析。

如今的浏览器,本身就有很强的优化能力,脚本引擎发现 SR_Z 和 SR_N 重复赋值,并且中间没有使用,于是就将之前的计算优化掉了。因此,最终效率会非常高。

真正困难

通过这几个例子,感觉翻译并不困难。事实上大多数 6502 指令,都可以生成对应的 JS 逻辑。有的很简短,只有一两行;有的较复杂,例如算术加减法。但不管怎样,都是没有障碍的。

但是,有一类指令很难翻译,那就是「跳转指令」。因为不同的层面,流程控制的能力是不一样的。

在 JavaScript 中,流程控制只能以「语块」为单位:

if (...) {
block 1
} else {
block 2
} for (...) {
break;
continue;
}

我们最多只能退出语块(break),或者重新进入语块(continue),无法指定从某一行开始运行。

而在 C 语言中,流程控制可以细致到行:

a:  ...
goto c;
b: ...
goto a;
c: ...
goto b;

机器指令更底层,因此更灵活。流程控制是以「字节」为单位的,可以跳到任意位置。甚至跳到一个指令的中间:

Address  Hexdump   Dissassembly
-------------------------------
$0600 a9 00 LDA #$00
$0602 4c 01 06 JMP $0601

于是将 LDA 的参数 0x00 当成另一个指令(BRK 指令)执行。

更有甚者,还可以跳到栈内存上,将动态数据当成指令执行。如此灵活的特性,又该如何实现?

下一篇,我们探讨如何处理跳转指令。

【探索】机器指令翻译成 JavaScript的更多相关文章

  1. 机器指令翻译成 JavaScript —— No.5 指令变化

    上一篇,我们通过内置解释器的方案,解决任意跳转的问题.同时,也提到另一个问题:如果指令发生变化,又该如何应对. 指令自改 如果指令加载到 RAM 中,那就和普通数据一样,也是可以随意修改的.然而,对应 ...

  2. 机器指令翻译成 JavaScript —— 终极目标

    上一篇,我们顺利将 6502 指令翻译成 C 代码,并演示了一个案例. 现在,我们来完成最后的目标 -- 转换成 JavaScript. 中间码输出 我们之所以选择 C,就是为了使用 LLVM.现在来 ...

  3. 机器指令翻译成 JavaScript —— No.6 深度优化

    第一篇 中我们曾提到,JavaScript 最终还得经过浏览器来解析.因此可以把一些优化工作,交给脚本引擎来完成. 现代浏览器的优化能力确实很强,但是,运行时的优化终归是有限的.如果能在事先实现,则可 ...

  4. 机器指令翻译成 JavaScript —— No.7 过渡语言

    上一篇,我们决定使用 LLVM 来优化程序,并打算用 C 作为输入语言.现在我们来研究一下,将 6502 指令转换成 C 的可行性. 跳转支持 翻译成 C 语言,可比 JS 容易多了.因为 C 支持 ...

  5. 机器指令翻译成 JavaScript —— No.2 跳转处理

    上一篇,我们发现大多数 6502 指令都可以直接 1:1 翻译成 JS 代码,但除了「跳转指令」. 跳转指令,分无条件跳转.条件跳转.从另一个角度,也可分: 静态跳转:目标地址已知 动态跳转:目标地址 ...

  6. 机器指令翻译成 JavaScript —— No.3 流程分割

    上一篇 我们讨论了跳转指令,并实现「正跳转」的翻译,但最终困在「负跳转」上.而且,由于线程模型的差异,我们不能 1:1 的翻译,必须对流程进行一些改造. 当初之所以选择翻译,而不是模拟,就是出于性能考 ...

  7. 机器指令翻译成 JavaScript —— No.4 动态跳转

    上一篇,我们用模拟流程的方式,解决了跳转问题. 不过静态跳转,好歹事先是知道来龙去脉的.而动态跳转,只有运行时才知道要去哪.既然流程都是未知的,翻译从何谈起? 动态跳转,平时出现的多吗?非常多!除了 ...

  8. [书籍翻译] 《JavaScript并发编程》第五章 使用Web Workers

    本文是我翻译<JavaScript Concurrency>书籍的第五章 使用Web Workers,该书主要以Promises.Generator.Web workers等技术来讲解Ja ...

  9. [书籍翻译] 《JavaScript并发编程》第七章 抽取并发逻辑

    本文是我翻译<JavaScript Concurrency>书籍的第七章 抽取并发逻辑,该书主要以Promises.Generator.Web workers等技术来讲解JavaScrip ...

随机推荐

  1. 多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类)

    前言:刚学习了一段机器学习,最近需要重构一个java项目,又赶过来看java.大多是线程代码,没办法,那时候总觉得多线程是个很难的部分很少用到,所以一直没下决定去啃,那些年留下的坑,总是得自己跳进去填 ...

  2. SQL Server常见数据类型介绍

    数据表是由多个列组成,创建表时必须明确每个列的数据类型,以下列举SQL Server常见数据类型的使用规则,方便查阅. 1.整数类型 int 存储范围是-2,147,483,648到2,147,483 ...

  3. Linux碎碎念

    在学习Linux过程中,有许多有用的小技巧.如果放在纸质的笔记本上,平时查阅会相当不方便.现在以一种“碎碎念”的方式,汇集整理在此,目前还不是很多,但随着学习.工作的深入,后续会陆陆续续添加更多的小技 ...

  4. CRL快速开发框架系列教程十二(MongoDB支持)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  5. 奇葩问题-TextView无法获取值

    问题场景 前几天写一个界面的时候,遇到一个非常奇葩的问题.app第一次安装的时候,这里针对用户第一次安装的时候,后来是不会出现这个问题了.我明明是对某个界面的一个textview赋值了,而且服务端也返 ...

  6. 【基于WPF+OneNote+Oracle的中文图片识别系统阶段总结】之篇一:WPF常用知识以及本项目设计总结

    篇一:WPF常用知识以及本项目设计总结:http://www.cnblogs.com/baiboy/p/wpf.html 篇二:基于OneNote难点突破和批量识别:http://www.cnblog ...

  7. jQuery2.x源码解析(缓存篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...

  8. 6.在MVC中使用泛型仓储模式和依赖注入实现增删查改

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/crud-operations-using-the-generic-repository-pat ...

  9. C#创建、安装、卸载、调试Windows Service(Windows 服务)的简单教程

    前言:Microsoft Windows 服务能够创建在它们自己的 Windows 会话中可长时间运行的可执行应用程序.这些服务可以在计算机启动时自动启动,可以暂停和重新启动而且不显示任何用户界面.这 ...

  10. from表单提交数据之后,后台对象接受不到值

    如果SSH框架下,前段页面通过from表单提交数据之后,在后台对象显示空值,也就是接收不到值得情况下.首先保证前段输入框有值,这个可以在提交的时候用jQuery的id或者name选择器alert弹出测 ...