zoukankan      html  css  js  c++  java
  • 【探索】机器指令翻译成 JavaScript

    简版: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 指令)执行。

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

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

  • 相关阅读:
    使用Twitter异常检测框架遇到的坑
    Python从入门到精通
    Windows中几个内存相当的指标
    Windows应用程序进程级别统一监控实践
    基于时序数据的微内核预警引擎架构设计
    Flink1.4.0连接Kafka0.10.2时遇到的问题
    wait和sleep的区别
    JVM几种垃圾回收器介绍
    二叉树的非递归遍历
    段页式内存管理
  • 原文地址:https://www.cnblogs.com/index-html/p/translate-machine-code-to-javascript.html
Copyright © 2011-2022 走看看