如何用尽可能少的字节编写一个C64可执行文件?本文的作者通过一个竞赛,详细解读了最佳实践技巧,快来Get吧!

作者 | Janne Hellsten

译者 | 弯月,责编 | 郭芮

出品 | CSDN(ID:CSDNnews)

以下为译文:

这篇文章回顾了我主持Commodore 64编程竞赛时,人们使用的C64编程技巧。竞赛的规则很简单:编写一个C64可执行文件(PRG文件),画出两条线,组成下面的图形——目标是用尽可能少的字节。

参赛作品通过Twitter的回复和私信发表,里面只包括PRG文件的字节长度,和PRG文件的MD5哈希值。

下面是一些参赛者,以及他们作品的链接:

●Philip Heron(https://twitter.com/fsphil) (代码:https://github.com/fsphil/tinyx - 34 字节 - 优胜者)

●Geir Straume (https://twitter.com/GeirSigmund)(代码:https://c64prg.appspot.com/downloads/lines34b.zip - 34 字节)

●Petri Häkkinen (https://twitter.com/petrih3)(代码:https://github.com/petrihakkinen/c64-lines - 37 字节)

●Mathlev Raxenblatz (https://twitter.com/laubzega)(代码:https://gist.github.com/laubzega/fb59ee6a3d482feb509dae7b77e925cf - 38 字节)

●Jan Achrenius (https://twitter.com/achrenico)(代码:https://twitter.com/achrenico/status/1161383381835362305 - 48 字节)

●Jamie Fuller (https://twitter.com/jamie30dbs)(代码:- 50 字节)

●David A. Gershman (https://twitter.com/dagershman)(代码:http://c64.dagertech.net/cgi-bin/cgiwrap/c64/index.cgi?p=xchallenge/.git;a=tree - 53 字节)

●Janne Hellsten(https://twitter.com/nurpax) (代码:https://gist.github.com/nurpax/d429be441c7a9f4a6ceffbddc35a0003 - 56 字节)

(如果有遗漏,请联系我更正。)

本文其余部分将重点介绍比赛作品中用到的一些汇编技巧。

基本知识

C64默认的图形模式为40x25字符模式。RAM中的帧缓冲区分为两个阵列:

  • $0400(屏幕RAM,40x25字节)

  • $d800(颜色RAM,40x25字节)

要想设置字符,需要在$0400处的屏幕RAM中(例如:$0400+y*40+x)存储一个字节。颜色RAM默认初始化为浅蓝色(颜色14),正好是线的颜色,意味着我们可以不用管颜色RAM。

边框色和背景色可以通过I/O寄存器控制,它被映射到内存中的$d020(边框色)和$d021(背景色)。

画两条线非常简单,因为斜率是固定的,可以直接硬编码。下面是C语言的实现,可以画两条线并在stdout上显示(去掉了寄存器写入,并把屏幕RAM的写入替换成了malloc(),以便在PC上运行):

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>void dump(const uint8_t* screen) {const uint8_t* s = screen;for (int y = 0; y < 25; y++) {for (int x = 0; x < 40; x++, s++) {printf("%c", *s == 0xa0 ? '#' : '.');}printf("\n");}
}void setreg(uintptr_t dst, uint8_t v) {
//  *((uint8_t *)dst) = v;
}int main() {
//  uint8_t* screenRAM = (uint_8*)0x0400;uint8_t* screenRAM = (uint8_t *)calloc(40*25, 0x20);setreg(0xd020, 0); // Set border colorsetreg(0xd021, 0); // Set background colorint yslope = (25<<8)/40;int yf = yslope/2;for (int x = 0; x < 40; x++) {int yi = yf >> 8;// First linescreenRAM[x + yi*40] = 0xa0;// Second line (X-mirrored)screenRAM[(39-x) + yi*40] = 0xa0;yf += yslope;}dump(screenRAM);
}

上述画图时使用的字符分别为$20(空白)和$a0(8x8填充方块)。运行后应该能看到下面由两条线组成的ASCII图形:

##....................................##
..#..................................#..
...##..............................##...
.....#............................#.....
......##........................##......
........##....................##........
..........#..................#..........
...........##..............##...........
.............#............#.............
..............##........##..............
................##....##................
..................#..#..................
...................##...................
..................#..#..................
................##....##................
..............##........##..............
.............#............#.............
...........##..............##...........
..........#..................#..........
........##....................##........
......##........................##......
.....#............................#.....
...##..............................##...
..#..................................#..
##....................................##

使用6502汇编和汇编伪代码,可以非常容易地将其转换为汇编语言:

!include "c64.asm"+c64::basic_start(entry)entry: {lda #0      ; black colorsta $d020   ; set border to 0sta $d021   ; set background to 0; clear the screenldx #0lda #$20
clrscr:
!for i in [0, $100, $200, $300] {sta $0400 + i, x
}inxbne clrscr; line drawing, completely unrolled; with assembly pseudoslda #$a0!for i in range(40) {!let y0 = Math.floor(25/40*(i+0.5))sta $0400 + y0*40 + ista $0400 + (24-y0)*40 + i}
inf: jmp inf  ; halt
}

这段划线的代码得到的PRG文件有286字节之大。

在讨论优化之前,我们先来观察几点:

首先,在C64上运行代码,而C64带有ROM例程。ROM中有许多例程,也许对我们的小程序有帮助。例如,清除屏幕只需要写JSR $E544。

其次,在6502这种8位CPU上计算地址可能非常麻烦,会消耗许多字节。CPU也没有乘法器,所以计算y*40+i之类的算式通常需要一系列逻辑位移操作,或者使用查找表,同样需要占用许多字节。为了避免乘以40的运算,我们可以逐步递增屏幕指针:

    int yslope = (25<<8)/40;int yf = yslope/2;uint8_t* dst = screenRAM;for (int x = 0; x < 40; x++) {dst[x] = 0xa0;dst[(39-x)] = 0xa0;yf += yslope;if (yf & 256) { // Carry set?dst += 40;yf &= 255;}}

这里我们每次将直线斜率累加到定点计数器yf上,每当8比特累加器设置进位标志时,就再加40。

下面是用汇编实现的累加方法:

!include "c64.asm"+c64::basic_start(entry)!let screenptr = $20
!let x0 = $40
!let x1 = $41
!let yf = $60entry: {lda #0sta x0sta $d020sta $d021; kernal clear screenjsr $e544; set screenptr = $0400lda #<$0400sta screenptr+0lda #>$0400sta screenptr+1lda #80sta yflda #39sta x1
xloop:lda #$a0ldy x0; screenRAM[x] = 0xA0sta (screenptr), yldy x1; screenRAM[39-x] = 0xA0sta (screenptr), yclclda #160  ; line slopeadc yfsta yfbcc no_add; advance screen ptr by 40clclda screenptradc #40sta screenptrlda screenptr+1adc #0sta screenptr+1no_add:inc x0dec x1bpl xloopinf:    jmp inf
}

总共82字节,仍然有点大。有几个非常明显的字节数过多是由16位地址计算产生的:

设置screenptr值用于间接索引寻址:

        ; set screenptr = $0400lda #<$0400sta screenptr+0lda #>$0400sta screenptr+1

给screenptr加40,以前进到下一行:

        ; advance screen ptr by 40clclda screenptradc #40sta screenptrlda screenptr+1adc #0sta screenptr+1

当然,这段代码也可以再小一些,但要是能一开始就不使用16位寻址该多好?我们来看看能否避免16位寻址。

技巧1:卷轴!

我们不再画整个屏幕RAM,而是只画最后Y=24行,然后通过JSR $E8EA调用“向上卷轴”ROM函数,将整个屏幕向上滚动!

x循环变成这样:

        lda #0sta x0lda #39sta x1
xloop:lda #$a0ldx x0; hardcoded absolute address to last screen linesta $0400 + 24*40, xldx x1sta $0400 + 24*40, xadc yfsta yfbcc no_scroll; scroll screen up!jsr $e8ea
no_scroll:inc x0dec x1bpl xloop

使用这个技巧后,线的渲染过程如下:

这是这次比赛中我最喜欢的技巧。许多参赛者也都发现了这个技巧。

技巧2:自行修改的代码

设置像素值的代码大致如下:

        ldx x1; hardcoded absolute address to last screen linesta $0400 + 24*40, xldx x0sta $0400 + 24*40, xinc x0dec x1

编码后为14字节的序列:

0803: A6 22               LDX $22
0805: 9D C0 07            STA $07C0,X
0808: A6 20               LDX $20
080A: 9D C0 07            STA $07C0,X
080D: E6 22               INC $22
080F: C6 20               DEC $20

实际上还可以使用自行修改的代码(self-modifying code,SMC)写得更简洁:

        ldx x1sta $0400 + 24*40, x
addr0:  sta $0400 + 24*40; advance the second x-coord with SMCinc addr0+1dec x1

只需要13字节:

0803: A6 22               LDX $22
0805: 9D C0 07            STA $07C0,X
0808: 8D C0 07            STA $07C0
080B: EE 09 08            INC $0809
080E: C6 22               DEC $22

技巧3:使用加电状态

这次比赛中允许对环境做出大胆的假设:画线的PRG是C64加电后运行的第一个程序,退出之后也不需要干净地回到BASIC提示符下。所以任何PRG启动后的初始环境中的东西都可以使用。下面是一些PRG启动时“不变”的东西:

  • A,X,Y寄存器可以假设都为零;

  • 所有CPU标志位均为清除状态;

  • 零页(地址为$00-$ff)内容。

类似地,如果调用任何内核ROM例程,也可以借助一切它们带来的副作用:返回的CPU标志位,临时值设置到零页中,等等。

在最初的几波优化之后,所有人都把目光投向了这个机器监视窗口中,寻找任何可能有用的值:

零页实际上包含非常有用的东西:

  • $d5:39/$27 == 线长度 - 1

  • $22: 64/$40 == 线斜率计数器的初始值

使用这些可以在初始化时节省一些字节。例如 :

!let x0 = $20lda #39      ; 0801: A9 27    LDA #$27sta x0       ; 0803: 85 20    STA $20
xloop:dec x0       ; 0805: C6 20    DEC $20bpl xloop    ; 0807: 10 FC    BPL $0805

由于$d5包含值39,所以你可以将x0计数器指向$d5,这样可以跳过LDA/STA这两条指令:

!let x0 = $d5; nothing here!
xloop:dec x0       ; 0801: C6 D5    DEC $D5bpl xloop    ; 0803: 10 FC    BPL $0801

Philip的优胜作品把这个技巧发挥到了极致。回忆一下最后一个字符行的地址是$07C0(==$0400+24*40)。在初始化时,这个值不在零页中。但是,ROM的“向上卷轴”例程会临时使用零页,其副作用就是函数返回时,$D1-$d2会包含$07C0。所以,设置一个像素不需要做STA $07C0, x,而是可以用间接索引寻址模式STA ($D1), y,可以节省一个字节。

技巧4:更小的起始代码

常见的C64 PRG二进制文件包含以下部分:

  • 开头2字节:加载地址(通常为$0801)

  • 12字节的BASIC起始序列

BASIC起始序列如下所示(地址为$801-$80C):

0801: 0B 08 0A 00 9E 32 30 36 31 00 00 00
080D: 8D 20 D0     STA $D020

这里不会深入介绍令牌BASIC内存布局(https://www.c64-wiki.com/wiki/BASIC_token),只需知道这个序列大致相当于“10 SYS 2061”即可。地址2061($080D)是BASIC解释器执行SYS命令时,实际的机器码程序开始运行的地方。

这14字节似乎毫无用处。Philip、Mathlev和Geir用了一些非常巧妙的技巧去掉了BASIC序列。这个技巧要求使用LOAD "*",8,1来加载PRG,因为LOAD "*",8会忽略PRG加载地址(开头两字节),永远加载到$0801。

这里使用了两个方法:

  • 栈技巧;

  • BASIC热重置向量技巧。

栈技巧

该技巧就是用一个指向我们希望的入口点的值来填充位于$01F8处的CPU栈。具体做法是,制作一个PRG,开始为一个16位指针,指向我们的代码,并将PRG加载到$01F8:

    * = $01F8!word scroll - 1  ; overwrite stackscroll:    jsr $E8EA

BASIC加载器(参见https://www.pagetable.com/c64disasm/#F4A5)加载完成并通过RTS指令返回调用者之后,它不会返回到调用LOAD的地方,而是直接返回到我们的PRG中。

BASIC热重置向量技巧

这个技巧似乎直接阅读PRG反汇编更容易理解:

02E6: 20 EA E8    JSR $E8EA
02E9: A4 D5       LDY $D5
02EB: A9 A0       LDA #$A0
02ED: 99 20 D0    STA $D020,Y
02F0: 91 D1       STA ($D1),Y
02F2: 9D B5 07    STA $07B5,X
02F5: E6 D6       INC $D6
02F7: 65 90       ADC $90
02F9: 85 90       STA $90
02FB: C6 D5       DEC $D5
02FD: 30 FE       BMI $02FD
02FF: 90 E7       BCC $02E8
0301: 4C E6 02    JMP $02E6

注意最后一行(JMP $02E6)。JMP指令从地址$0301开始,跳转目标地址存储在地址$0302-$0303。

当这行代码加载到地址$02E6开始的内存之后,值$02E6被写入地址$0302-$0303。实际上$0302-$0303有特殊含义:它包含指向“BASIC闲置循环”的指针(参见C64内存映射:http://sta.c64.org/cbm64mem.html)。加载PRG会使用$02E6覆盖改地址,所以当BASIC解释器在热重置后试图跳转到闲置循环时,它不会进入闲置循环,而是会进入直线渲染程序!

其他与BASIC启动有关的技巧

Petri发现了另一个BASIC启动技巧(https://github.com/petrihakkinen/c64-lines/blob/master/main37.asm)可以将自己的常量注入到零页中。在这个方法中,你需要手工编写令牌BASIC启动序列,然后将自己的常量写入BASIC程序的行号中。BASIC行号(即你的常量)会在启动时存储至地址$39-$3A。非常聪明!

技巧5:非常规控制流

下面是个简化版本的x循环,它只画一条线,画完一条线后会停止执行:

        lda #39sta x1
xloop:lda #$a0ldx x1sta $0400 + 24*40, xadc yfsta yfbcc no_scroll; scroll screen up!jsr $e8ea
no_scroll:dec x1bpl xloop; intentionally halt at the end
inf:    jmp inf

但这段代码有个bug。在画线上的最后一个像素时,不应该再卷轴。因此我们需要在画最后一个像素时编写更多的分支来跳过卷轴:

        lda #39sta x1
xloop:lda #$a0ldx x1sta $0400 + 24*40, xdec x1; skip scrolling if last pixelbmi doneadc yfsta yfbcc no_scroll; scroll screen up!jsr $e8ea
no_scroll:jmp xloop
done:; intentionally halt at the end
inf:    jmp inf

这里的控制流看起来很像C编译器编译结构化程序的结果。为了跳过最后一次卷轴,这段代码使用了新的JMP abs指令,占用了3个字节。条件分支只有两个字节,因为它们使用8位相对立即数来表示分支目标。

这个“跳过最后一次卷轴”的JMP指令实际上可以避免,只需将卷轴调用移动到循环的开头,然后稍稍改一下控制流的结构。Philip想出的办法如下:

        lda #39sta x1
scroll: jsr $e8ea
xloop:lda #$a0ldx x1sta $0400 + 24*40, xadc yfsta yfdec x1     ; doesn't set carry!
inf:    bmi inf    ; hang here if last pixel!bcc xloop  ; next pixel if no scrollbcs scroll ; scroll up and continue

这段代码完全省却了3字节的JMP,还将另一个JMP改成了2字节的条件分支,总共节省了4字节。

技巧6:利用位堆积来画线

一些作品没有使用斜率计数器,而是将直线的图案堆积到了一个8位常量中,因为直线上的像素遵循一个重复的8像素图案:

int mask = 0xB6; // 10110110
uint8_t* dst = screenRAM;
for (int x = 0; x < 40; x++) {dst[x] = 0xA0;if (mask & (1 << (x&7))) {dst += 40; // go down a row}
}

其汇编代码非常简短。不过,修改后的斜率计数器更精简一些。

优胜作品

下面是Philp的34字节的优胜作品,他的代码中许多地方都非常精巧。

但为什么停在了34字节?

比赛结束后,每个人都分享了代码和心得,大家还就如何改进进行了许多讨论。在截止期限之后还出现了一些更小的版本:

●Philip - 33 字节:https://gist.github.com/fsphil/05deaa06804b9b2054260b616cafed4b

●Philip - 32 字节:https://gist.github.com/fsphil/01bda1a9dd58c219002ddd6e18b36c3f

●Petri - 31 字节:https://github.com/petrihakkinen/c64-lines/blob/master/main31.asm

●Philip - 29 字节:https://gist.github.com/fsphil/7655a394ec5f953c910e9d9369dced56

你应该读读这些代码——其中有些非常好的东西。

原文: https://nurpax.github.io/posts/2019-08-18-dirty-tricks-6502-programmers-use.html

本文为CSDN翻译,转载请注明来源出处。

【END】

Python的C位稳了!微软正式拥抱Python !

https://edu.csdn.net/topic/python115?utm_source=csdn_bw

 热 文 推 荐 

☞三大运营商回复 4G 降速;微信上线语音转文字功能;IntelliJ IDEA 2019.2.1 发布 | 极客头条

程序员为什么需要框架?

☞ 细数微软 Teams 的 14 宗“罪”!

☞ 华为暂没有推出鸿蒙手机计划;苹果否认 iPhone 辐射超标;Kotlin 1.3.50 发布 | 极客头条

☞ 我是如何通过开源项目月入 10 万的?

☞语音识别技术简史

☞意大利黑手党四大家族做了条"犯罪链", 把家族的权利被分的明明白白的……

☞Istio 庖丁解牛六:多集群网格应用场景

☞如何写出让同事无法维护的代码?

@程序员,如何用最少的字节编写 C64 可执行文件?相关推荐

  1. 程序员如何用技术变现

    如今的中国人,不再羞于谈钱,甚至有些过度追求金钱,这是时代的进步,而程序员拥有编写代码的手艺,作为一名匠人,当然可以通过手艺来赚钱.谈到程序员的变现途径,我们通常会想到的方法有: 接项目 开发小软件 ...

  2. 程序员如何用糖果实现盈利 - [别人家的程序员01]

    程序员如何用糖果实现盈利 - [别人家的程序员01] 程序员如何用糖果实现盈利 - [别人家的程序员01] 前言 CandyJapan 网站如何从零走到今天 平台收支状况 如何做分析.写代码 总结 程 ...

  3. 程序员如何用“心“表白(结尾附源码)

    写在前面:博主是一只经过实战开发历练后投身培训事业的"小山猪",昵称取自动画片<狮子王>中的"彭彭",总是以乐观.积极的心态对待周边的事物.本人的技 ...

  4. 程序员毕业1-2年如何正确编写自己简历

    程序员毕业1-2年如何正确编写自己简历 个人简历模板 个人概况 教育背景 职业技能 职业技能 项目经验 自我评价,所有证书 简历错误分析 第一份简历分析 第二份简历分析 总结 想要获取简历模板word ...

  5. 编程开发学习笔记之程序员如何用1年时间获得3年成长(图)

    2019独角兽企业重金招聘Python工程师标准>>> 编程开发学习笔记之程序员如何用1年时间获得3年成长(图) 前言 这世界存在这么一个银行,你一出生,就自动享有这家银行为你开设的 ...

  6. python绘制生日快乐图片_程序员如何用代码祝自己生日快乐(多用模板)

    原标题:程序员如何用代码祝自己生日快乐(多用模板) 本文教你如何用代码为自己庆祝生日,当然你也可以用来讨好女神,具体如何应用大家可以发散思维,例如情人节给暗恋的女孩发一个 JS 文件过去表白,或者给女 ...

  7. 装逼技巧:程序员如何用代码证明自己牛逼!

    本文秉承着:你看不懂是你SB,我写的代码就要牛逼. 1.单行写一个评级组件 "★★★★★☆☆☆☆☆".slice(5 – rate, 10 – rate);定义一个变量rate是1 ...

  8. 程序员访谈_可以用PHP编写出色的应用程序-访谈系列

    程序员访谈 I read an old post, circa 2010, on the MailChimp blog a little while ago, about their experien ...

  9. 某程序员吐槽自己追求某字节HR,暧昧半年,见面后却被告知是普通朋友!心态已崩!网友:别追HR!道行太深!...

    请点击上面 一键关注! 一个程序员小哥哥发帖痛诉自己的坎坷情路,他追求一个字节跳动的hr,两人暧昧了半年,满怀期待地见面后却被hr小姐姐告知只是普通朋友,如今心态已崩! 楼主说,字节的hr是真的漂亮, ...

最新文章

  1. Oracle中table的大小计算方式
  2. typora 公式_Typora --- 一款功能强大的高效排版编译器
  3. 开启基于Query的实例分割新思路!腾讯华科提出QueryInst
  4. Could not reliably determine the server's fully qualified domain name
  5. 原生JS实现$.ajax
  6. 第二次作业 郭昭杰 201731062608
  7. 数据采集技术python网络爬虫答案_高校邦网络数据采集与Python爬虫【带实验】章节答案...
  8. FreeSql (二)自动迁移实体
  9. Oracle前10条记录
  10. .net RestSharp使用
  11. 高德地图---行政区划分
  12. UML核心元素--参与者
  13. NPOI导出Excel自适应行高
  14. 计算机组成原理补码减法,补码加减法运算(计算机组成原理).ppt
  15. 阿姆斯特朗回旋加速喷气式阿姆斯特朗炮
  16. HyperLynx 仿真
  17. zbb20170216_spring_aop
  18. Java中“||”与“|”的区别【JAVA基础】
  19. 苹果x电池容量_关于苹果18W PD快充你想知道的,全都在这里了
  20. 2015 岁末 祝福 感恩

热门文章

  1. 我也说说刘谦在2010年春晚上的魔术作假
  2. 《上市公司信息披露电子化规范》简介
  3. Python读取IRIS数据集并转换为PaddlePaddle中使用的reader
  4. torch.Tensor.scatter_(dim, index, src, reduce=None)
  5. boost::asio向socket中异步读写数据
  6. Golang连接使用MySql5.7数据库完整步骤
  7. 塑料浮船坞行业调研报告 - 市场现状分析与发展前景预测
  8. android 控件高度和图片一样高,Android 根据图片宽高比例设置控件宽高
  9. 设置git客户端不经过代理
  10. 【IT】一些有用的链接和操作