上一篇,我们决定使用 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. bcp 命令实例

    set sql_flow="select Id,',',ApplierName,',',FlowStatus,',',IsApproved,',',CreateTime from *** w ...

  2. bzoj3207--Hash+主席树

    题目大意: 给定一个n个数的序列和m个询问(n,m<=100000)和k,每个询问包含k+2个数字:l,r,b[1],b[2]...b[k],要求输出b[1]~b[k]在[l,r]中是否出现. ...

  3. Java中用得比较顺手的事件监听

    第一次听说监听是三年前,做一个webGIS的项目,当时对Listener的印象就是个"监视器",监视着界面的一举一动,一有动静就触发对应的响应. 一.概述 通过对界面的某一或某些操 ...

  4. Boost信号/槽signals2

    信号槽是Qt框架中一个重要的部分,主要用来解耦一组互相协作的类,使用起来非常方便.项目中有同事引入了第三方的信号槽机制,其实Boost本身就有信号/槽,而且Boost的模块相对来说更稳定. signa ...

  5. 装饰者模式 Decoration

    1.什么是装饰者模式 动态给对象增加功能,从一个对象的外部来给对象添加功能,相当于改变了对象的外观,比用继承的方式更加的灵活.当使用装饰后,从外部系统的角度看,就不再是原来的那个对象了,而是使用一系列 ...

  6. Struts2入门(六)——国际化

    一.前言 1.1.国际化简介 国际化是指应用程序在运行的时候,根据客户端请求来自的国家地区.语言的不同而显示不同的界面(简单说就是根据你的地区显示相关地区的语言,如果你现在在英国,那么显示的语言就是英 ...

  7. Android Retrofit 2.0 使用-补充篇

    推荐阅读,猛戳: 1.Android MVP 实例 2.Android Retrofit 2.0使用 3.RxJava 4.RxBus 5.Android MVP+Retrofit+RxJava实践小 ...

  8. Form 表单提交参数

    今天因为要额外提交参数数组性的参数给form传到后台而苦恼了半天,结果发现,只需要在form表单对应的字段html空间中定义name = 后台参数名 的属性就ok了. 后台本来是只有模型参数的,但是后 ...

  9. ubuntu系统(华硕笔记本)屏幕亮度用Fn控制的调节设置

    亲测配置: 系统:Linux lite 3.2 x86_64(Ubuntu其他版本可参考修改) 笔记本:华硕(asus)1201N 达到的效果: 可以正常使用Fn+F5调暗,Fn+F6调亮. 设置步骤 ...

  10. 作为前端应当了解的Web缓存知识

    缓存优点 通常所说的Web缓存指的是可以自动保存常见http请求副本的http设备.对于前端开发者来说,浏览器充当了重要角色.除此外常见的还有各种各样的代理服务器也可以做缓存.当Web请求到达缓存时, ...