每日最新頭條.有趣資訊

程序員,如何用最少的字節編寫 C64 可執行文件?

如何用盡可能少的字節編寫一個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上運行):

上述畫圖時使用的字元分別為$20(空白)和$a0(8x8填充方塊)。運行後應該能看到下面由兩條線組成的ASCII圖形:

使用6502匯編和匯編偽代碼,可以非常容易地將其轉換為匯編語言:

這段劃線的代碼得到的PRG文件有286字節之大。

在討論優化之前,我們先來觀察幾點:

首先,在C64上運行代碼,而C64帶有ROM例程。ROM中有許多例程,也許對我們的小程序有幫助。例如,清除螢幕只需要寫JSR $E544。

其次,在6502這種8位CPU上計算地址可能非常麻煩,會消耗許多字節。CPU也沒有乘法器,所以計算y*40+i之類的算式通常需要一系列邏輯位移操作,或者使用查找表,同樣需要佔用許多字節。為了避免乘以40的運算,我們可以逐步遞增螢幕指針:

這裡我們每次將直線斜率累加到定點計數器yf上,每當8比特累加器設置進位標誌時,就再加40。

下面是用匯編實現的累加方法:

總共82字節,仍然有點大。有幾個非常明顯的字節數過多是由16位地址計算產生的:

設置screenptr值用於間接索引尋址:

給screenptr加40,以前進到下一行:

當然,這段代碼也可以再小一些,但要是能一開始就不使用16位尋址該多好?我們來看看能否避免16位尋址。

技巧1:卷軸!

我們不再畫整個螢幕RAM,而是隻畫最後Y=24行,然後通過JSR $E8EA調用“向上卷軸”ROM函數,將整個螢幕向上滾動!

x循環變成這樣:

使用這個技巧後,線的渲染過程如下:

這是這次比賽中我最喜歡的技巧。許多參賽者也都發現了這個技巧。

技巧2:自行修改的代碼

設置像素值的代碼大致如下:

編碼後為14字節的序列:

實際上還可以使用自行修改的代碼(self-modifying code,SMC)寫得更簡潔:

只需要13字節:

技巧3:使用加電狀態

這次比賽中允許對環境做出大膽的假設:畫線的PRG是C64加電後運行的第一個程序,退出之後也不需要乾淨地回到BASIC提示符下。所以任何PRG啟動後的初始環境中的東西都可以使用。下面是一些PRG啟動時“不變”的東西:

A,X,Y寄存器可以假設都為零;

所有CPU標誌位均為清除狀態;

零頁(地址為$00-$ff)內容。

類似地,如果調用任何內核ROM例程,也可以借助一切它們帶來的副作用:返回的CPU標誌位,臨時值設置到零頁中,等等。

在最初的幾波優化之後,所有人都把目光投向了這個機器監視窗口中,尋找任何可能有用的值:

零頁實際上包含非常有用的東西:

$d5:39/$27 == 線長度 - 1

$22: 64/$40 == 線斜率計數器的初始值

使用這些可以在初始化時節省一些字節。例如 :

由於$d5包含值39,所以你可以將x0計數器指向$d5,這樣可以跳過LDA/STA這兩條指令:

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):

這裡不會深入介紹令牌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:

BASIC加載器(參見https://www.pagetable.com/c64disasm/#F4A5)加載完成並通過RTS指令返回調用者之後,它不會返回到調用LOAD的地方,而是直接返回到我們的PRG中。

BASIC熱重置向量技巧

這個技巧似乎直接閱讀PRG反匯編更容易理解:

注意最後一行(JMP $02E6)。JMP指令從地址$0301開始,跳轉目標地址存儲在地址$0302-$0303。

其他與BASIC啟動有關的技巧

Petri發現了另一個BASIC啟動技巧(https://github.com/petrihakkinen/c64-lines/blob/master/main37.asm)可以將自己的常量注入到零頁中。在這個方法中,你需要手工編寫令牌BASIC啟動序列,然後將自己的常量寫入BASIC程序的行號中。BASIC行號(即你的常量)會在啟動時存儲至地址$39-$3A。非常聰明!

技巧5:非常規控制流

下面是個簡化版本的x循環,它隻畫一條線,畫完一條線後會停止執行:

但這段代碼有個bug。在畫線上的最後一個像素時,不應該再卷軸。因此我們需要在畫最後一個像素時編寫更多的分支來跳過卷軸:

這裡的控制流看起來很像C編譯器編譯結構化程序的結果。為了跳過最後一次卷軸,這段代碼使用了新的JMP abs指令,佔用了3個字節。條件分支只有兩個字節,因為它們使用8位相對立即數來表示分支目標。

這個“跳過最後一次卷軸”的JMP指令實際上可以避免,只需將卷軸調用移動到循環的開頭,然後稍稍改一下控制流的結構。Philip想出的辦法如下:

這段代碼完全省卻了3字節的JMP,還將另一個JMP改成了2字節的條件分支,總共節省了4字節。

技巧6:利用位堆積來畫線

一些作品沒有使用斜率計數器,而是將直線的圖案堆積到了一個8位常量中,因為直線上的像素遵循一個重複的8像素圖案:

其匯編代碼非常簡短。不過,修改後的斜率計數器更精簡一些。

優勝作品

下面是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】

熱 文推 薦

獲得更多的PTT最新消息
按讚加入粉絲團