文章出處

簡版: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 指令)執行。

更有甚者,還可以跳到棧內存上,將動態數據當成指令執行。如此靈活的特性,又該如何實現?

下一篇,我們探討如何處理跳轉指令。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()