上一篇,我们决定使用 LLVM 来优化程序,并打算用 C 作为输入语言。现在我们来研究一下,将 6502 指令转换成 C 的可行性。

跳转支持

翻译成 C 语言,可比 JS 容易多了。因为 C 支持 goto,跳转可轻松实现。例如:

$0600  LDA #$01
$0602 STA $02
$0604 JMP $0600

就能翻译成如下 C 代码:

L_0600: A = 0x01; ...
L_0602: write(A, 0x02);
L_0604: goto L_0600;

我们把指令所在的位置,标记成对应的 label 名。例如 $0600 变成 L_0600:

这样 JMP $0600 即可翻译成 goto L_0600。很简单,完全符合 C 语法。

只要跳转是以指令为粒度的,都可以翻译成 goto。(跳到指令中间、指令区外,另当别论)

转换工具

现在,我们需要一个 6502 指令转 C 的工具。

本着不造轮子的原则,我们直接用现成的反汇编工具,生成上述那种的汇编代码。

但是,这不符合 C 语法啊 —— 没事,可以用脚本简单处理一下。例如:

  • 把每行开头的 $nnnn 替换成 L_nnnn:

  • 把 $ 替换成 0x

  • 补上括号、分号

几个正则一上,于是就变成这样:

L_0600: LDA(0x01);
L_0602: STA(0x02);
L_0604: JMP(0600);

这不就符合 C 语法了!

我们把每个指令定义成宏,让编译器自己去展开。调试时,就像调汇编一样,一步一个指令,不会进入逻辑细节。

当然,指令有很多种寻址模式。比如 LDA 指令,就有立即数、零页、零页偏移、绝对地址等模式。

因此,我们再增加一个后缀,以分区它们。例如 LDA $01 翻译成 LDA_ZP(0x01)

类似还有 _ZP_X_ZP_Y_ABS 等等。所以,最终 C 代码是这样的:

L_0600: LDA_ZP(0x01)
L_0602: STA_ZP(0x02)
L_0604: JMP_ABS(0600)

翻译跳转

现在换成 C 语言,跳转又该如何实现?

静态跳转很简单,只是包了一个语法糖:

#define JMP_ABS(addr)  goto L_##addr;

但是动态跳转,就需要斟酌一番了。

因为现在是用 goto 流程控制,没有将指令切割成 block_xxx 函数块,所以不能用之前那种查表的方式。

但是,可以用类似的方案。我们把已知的跳转点,事先都准备好:

jump_map:

switch (pc) {
case 0x0600: goto L_0600;
case 0x0608: goto L_0608;
case 0x0620: goto L_0620;
...
default: enter_interpret_mode()
}

动态跳转时,先 goto jump_map,然后根据 pc 的值,跳到相应的位置;如果没有符合的,则进入解释器模式。

这样,就实现了 C 版的动态跳转!

跳转点优化

当然,已知的跳转点会有很多,其实可以再精简一下。

动态跳转,绝大多数时候都是 RTS 指令,返回之前 JSR 的位置。所以,我们可以只列出 JSR 所在的位置。

相当于只提供 call/return 的跳转。其他小概率情况,反正可以用解释模式。

我们把这个跳转表(或者称作返回表),用宏包装一下,看起来变成这样:

RET_BEGIN
RET_ADDR(0600)
RET_ADDR(0604)
RET_ADDR(0620)
RET_END

这样,非常方便工具输出。

事实上精简这个跳转表,不只是为了减少代码量,更多是为了优化。如果每一行都有被 goto 的可能,那么 inline 就很难做了。

时钟模拟

现在,讨论时钟相关的问题。我们还是用一个变量,模拟剩余的周期数:

int cycle_remain = 20000;

然后每执行一条指令,扣除相应的数量。我们可以在宏里面定义,例如:

#define CYCLE(V)        cycle_remain -= V;

#define _ST(R, M)       write(M, R);
#define STA_ZP(v) CYCLE(3); _ST(A, v);
#define STA_ABS(v) CYCLE(4); _ST(A, v);
#define STA_ABS_X(v) CYCLE(5); _ST(A, v + X);

之前我们讨论的,是在跳转时统一扣除;现在,每个指令都要减一次,会不会浪费性能?

其实不用担心,这些都是常量计算,编译器会优化合并的。

接着就是频率控制。因为这是 C 语言,我们先尝试编译成本地程序,所以暂时无需考虑线程问题。如果 cycle_remain 用尽了,sleep 一下就可以。

演示

最后,就是将所有 6502 指令实现成宏。好在数量不算太多,而且很多都有规律。

上周末花了大半天时间,把这唯一的体力活完成了,大致是这样的:6502.h

现在,我们尝试一个案例 —— 贪吃蛇。

这是它的原始机器码:

0600: 20 06 06 20 38 06 20 0d 06 20 2a 06 60 a9 02 85
0610: 02 a9 04 85 03 a9 11 85 10 a9 10 85 12 a9 0f 85
0620: 14 a9 04 85 11 85 13 85 15 60 a5 fe 85 00 a5 fe
....
0730: ea ca d0 fb 60

用现有的工具反汇编:

$0600    20 06 06  JSR $0606
$0603 20 38 06 JSR $0638
$0606 20 0d 06 JSR $060d
$0609 20 2a 06 JSR $062a
$060c 60 RTS
$060d a9 02 LDA #$02
....
$0731 ca DEX
$0732 d0 fb BNE $072f
$0734 60 RTS

然后,经过我们的脚本加工,变成 C 代码:

L_0600: JSR(0606, 0600)
L_0603: JSR(0638, 0603)
L_0606: JSR(060d, 0606)
L_0609: JSR(062a, 0609)
L_060c: RTS()
L_060d: LDA_IMM(0x02)
....
L_0731: DEX()
L_0732: BNE(072f)
L_0734: RTS()

以及一个用来支持动态跳转的表:

RET_BEGIN
RET_ADDR(0603)
RET_ADDR(0606)
RET_ADDR(0609)
RET_ADDR(060c)
....
RET_END

此外,还约定了几个特殊地址:

  • [0x0200, 0x0600) 屏幕输出 - 32*32 像素,8 位

  • 0xfe 随机数

  • 0xff 最后按下的键

考虑到屏幕很小,不如直接 print 出来吧。为了简单演示,先不实现颜色。

我们将其编译成本地程序,运行:

(由于字体原因,看起来变成了长方形,所以把图片高度缩小了一半)

虽然效果很差,但至少逻辑上是没问题的!

结尾

将 6502 指令翻译成 C 代码,其实还是非常容易的。不过,我们目标可是 JavaScript。

下一篇,我们将尝试终极挑战。

机器指令翻译成 JavaScript —— No.7 过渡语言的更多相关文章

  1. 【探索】机器指令翻译成 JavaScript

    前言 前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念.为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学. 于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript ...

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

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

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

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

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

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

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

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

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

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

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

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

  8. quartz定时任务cron表达式讲解及翻译成现实语言的插件的使用详解

    cron表达式讲解 参见该网址: https://www.cnblogs.com/GarfieldTom/p/3746290.html cron表达式只有专业技术人员才看得懂,普通人不知道表达式是什么 ...

  9. 利用Google翻译成多国语言的见解

    1.首先注意,英语句子中的 第一个单词的首字母要大写, 2.句子结尾了,要用句号. 3.英语中单词和前面的标点符号要留一个空格,如:  you.Are   应该是 you. Are you..... ...

随机推荐

  1. 如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车

     阅读目录 前言 回顾 梳理 实现 结语 一.前言 之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能.本篇准备把剩下的购物车的基本概念一次处理完. 二.回顾 在动手之前我对之 ...

  2. Javascript生成二维码(QR)

    网络上已经有非常多的二维码编码和解码工具和代码,很多都是服务器端的,也就是说需要一台服务器才能提供二维码的生成.本着对服务器性能的考虑,这种小事情都让服务器去做,感觉对不住服务器,尤其是对于大流量的网 ...

  3. ExtJS 4.2 第一个程序

    本篇介绍如何创建一个ExtJS应用程序.并通过创建目录.导入文件.编写代码及分析代码等步骤来解释第一个ExtJS程序. 目录 1. 创建程序 1.1 创建目录建议 1.2 实际目录 1.3 index ...

  4. KV存储系统

    现在的KV存储系统都是分布式的,首先介绍Zookeeper——针对大型分布式系统的高可靠的协调系统. 开发分布式系统是件很困难的事情,其中的困难主要体现在分布式系统的“部分失败”.“部分失败”是指信息 ...

  5. 【手记】注意BinaryWriter写string的小坑——会在string前加上长度前缀length-prefixed

    之前以为BinaryWriter写string会严格按构造时指定的编码(不指定则是无BOM的UTF8)写入string的二进制,如下面的代码: //将字符串"a"写入流,再拿到流的 ...

  6. H3 BPM产品安装手册(.Net版本)

    1         安装说明 1.1    服务器安装必备软件 在使用该工作流软件之前,有以下一些软件是必须安装: l  IIS7.0以上版本(必须): l  .Net Framework 4.5(必 ...

  7. Android中ListView实现图文并列并且自定义分割线(完善仿微信APP)

    昨天的(今天凌晨)的博文<Android中Fragment和ViewPager那点事儿>中,我们通过使用Fragment和ViewPager模仿实现了微信的布局框架.今天我们来通过使用Li ...

  8. MongoDB学习笔记三—增删改文档上

    插入insert 单条插入 > db.foo.insert({"bar":"baz"}) WriteResult({ }) 批量插入 > db.fo ...

  9. Linux命令

    系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMBIOS ...

  10. 关于javascript中的this关键字

    this是非常强大的一个关键字,但是如果你不了解它,可能很难正确的使用它. 下面我解释一下如果在事件处理中使用this. 首先我们讨论一下下面这个函数中的this关联到什么. function doS ...