上一篇,我们决定使用 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. 移动端之Android开发的几种方式的初步体验

    目前越来越多的移动端混合开发方式,下面列举的大多数我都略微的尝试过,就初步的认识写个简单的心得: 开发方式 开发环境 是否需要AndroidSDK 支持跨平台 开发语言&技能 MUI Win+ ...

  2. 札记:Fragment基础

    Fragment概述 在Fragment出现之前,Activity是app中界面的基本组成单位,值得一提的是,作为四大组件之一,它是需要"注册"的.组件的特性使得一个Activit ...

  3. Socket聊天程序——客户端

    写在前面: 上周末抽点时间把自己写的一个简单Socket聊天程序的初始设计和服务端细化设计记录了一下,周二终于等来毕业前考的软考证书,然后接下来就是在加班的日子度过了,今天正好周五,打算把客户端的详细 ...

  4. TODO:Laravel 内置简单登录

    TODO:Laravel 内置简单登录 1. 激活Laravel的Auth系统Laravel 利用 PHP 的新特性 trait 内置了非常完善好用的简单用户登录注册功能,适合一些不需要复杂用户权限管 ...

  5. ExtJS 4.2 组件介绍

    目录 1. 介绍 1.1 说明 1.2 组件分类 1.3 组件名称 1.4 组件结构 2. 组件的创建方式 2.1 Ext.create()创建 2.2 xtype创建 1. 介绍 1.1 说明 Ex ...

  6. CENTOS 6.5 平台离线编译安装 Mysql5.6.22

    一.下载源码包 http://cdn.mysql.com/archives/mysql-5.6/mysql-5.6.22.tar.gz 二.准备工作 卸载之前本机自带的MYSQL 安装 cmake,编 ...

  7. WPF 微信 MVVM 【续】发送部分QQ表情

    今天主要记录的就是发送QQ表情, WPF 微信 MVVM里写了,后期为了发送QQ表情,需要把TextBox替换为RichTextBox,接下来就说说替换的过程. 一.支持Binding的RichTex ...

  8. Webpack 配置摘要

    open-browser-webpack-plugin 自动打开浏览器 html-webpack-plugin 通过 JS 生成 HTML webpack.optimize.UglifyJsPlugi ...

  9. docker for mac 学习记录

    docker基本命令 docker run -d -p 80:80 --name webserver nginx 运行容器并起别名 docker ps 展示目前启动的容器 docker ps -a 展 ...

  10. NOIP2016纪录[那些我所追求的]

    人生第一场正式OI [序] 2016-12-04 见底部 [Day -1] 2016-11-17 期中考试无心插柳柳成荫,考了全市第2班里第1(还不是因为只复习了不到两天考试),马上请了一个周的假准备 ...