Categoria: Programmazione
L'emulatore VICE mette a disposizione un monitor integrato che si rivela un potente strumento di debug e disassemblaggio di quanto stiamo eseguendo sul C64 emulato. In questo articolo vedremo un caso abbastanza semplice di ciò che possiamo fare con un minimo di pazienza e di applicazione, e anche con poche basi di linguaggio assembly: scopriremo come, nel gioco "Jumpin Jack", viene derivato il cosiddetto "High Score Magic Number", un numero che viene mostrato in calce alla classifica dei migliori punteggi e che varia in funzione del punteggio migliore ottenuto nella sessione di gioco.
Il "Magic Number" veniva utilizzato in una competizione per corrispondenza, per verificare se il punteggio ad esso relativo fosse stato realmente ottenuto da un giocatore: per partecipare alla competizione era necessario comunicare sia il proprio punteggio migliore che il "Magic Number" ad esso relativo, e se qualcuno avesse partecipato alla competizione sostenendo di aver ottenuto un punteggio notevole, ma il "Magic Number" non fosse stato corrispondente a quello previsto, tale punteggio, a ragion veduta, si sarebbe potuto considerare fasullo.
Per cominciare, invochiamo il monitor mentre è visualizzata la schermata dei punteggi migliori. Utilizzando il comando "m" cerchiamo di ottenere un dump della parte inferiore dello schermo. Nel nostro caso siamo abbastanza fortunati: lo schermo è in modalità testuale e non è stato rilocato dalla posizione standard a $0400. Se fosse stato rilocato avremmo dovuto ricavarne l'attuale posizione in memoria dai registri del VIC. Vediamo le righe di dump che ci interessano:
(C:$1eb4) m 0760
>C:0760 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
>C:0770 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
>C:0780 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
>C:0790 20 20 20 20 20 20 20 20 20 d3 c7 c4 20 c7 c8 c6
>C:07a0 c7 20 d2 c2 ce d1 c4 20 cc c0 c6 c8 c2 20 cd d4
>C:07b0 cc c1 c4 d1 20 c8 d2 20 20 dc df dd 20 20 20 20
>C:07c0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
>C:07d0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
>C:07e0 20 20 20 20 20 20 20 20 d8 b2 ea 20 44 e5 a9 15
Inutile rimarcare che questo gioco utilizza un set di caratteri ridefinito. Con poca fatica e un pizzico di intuito scopriamo che i codici POKE delle lettere partono da $C0 (lettera A) e quelli delle cifre sono da $DA (cifra 0) a $E3 (cifra 9). Notate la presenza di parole inframmezzate da spazi ($20), ad esempio "cc c0 c6 c8 c2" = "MAGIC". "dc df dd" è la codifica POKE del nostro magic number (le cifre 2 5 3).
Ci chiediamo dunque se la stringa sia stata semplicemente copiata sullo schermo da un'altra zona di memoria. Utilizzando il comando "hunt" del monitor, proviamo a ricercare una sequenza di byte presa dalla frase nella pagina video. Ancora una volta abbiamo fortuna:
(C:$07f0) hunt 0800 9fff d3 c7 c4 20 c7 c8 c6 c7
6c44
Vediamo il dump:
(C:$1e54) m 6c00
>C:6c00 ad b0 03 d0 19 a2 1a a9 00 9d 00 04 a9 0f 9d 00
>C:6c10 d8 e8 e0 28 d0 f1 a9 1a 8d a6 65 4c 96 65 a9 28
>C:6c20 4c 18 6c ee e4 03 f0 03 4c 3f 1e a9 f8 8d e4 03
>C:6c30 ce e5 03 ae e5 03 bd 44 6c 9d 99 07 a9 00 9d 99
>C:6c40 db 4c 3f 1e d3 c7 c4 20 c7 c8 c6 c7 20 d2 c2 ce
>C:6c50 d1 c4 20 cc c0 c6 c8 c2 20 cd d4 cc c1 c4 d1 20
>C:6c60 c8 d2 20 20 dc df dd a9 f0 8d e4 03 a9 23 8d e5
>C:6c70 03 4c 00 1f a2 00 bd 90 05 9d 00 7f bd 90 d9 9d
>C:6c80 28 7f e8 e0 28 d0 ef 60 ae 50 03 bd 00 50 c9 06
Notiamo che a $6C44 inizia effettivamente la codifica della stringa "the high score magic number is " seguita dalle cifre del magic number, alle locazioni $6C64-$6C66. Questo è un importante risultato: ora sappiamo che lo stesso magic number viene memorizzato sotto forma di codici POKE delle sue cifre. Possiamo dunque ricercare la routine in cui viene calcolato, individuando anzitutto dove avvenga la sua memorizzazione nelle locazioni suddette. Ancora con il comando "hunt", scopriamo:
(C:$6d20) hunt 0800 cfff 8d 64 6c
1104
(C:$6d20) hunt 0800 cfff 8d 65 6c
1120
(C:$6d20) hunt 0800 cfff 8d 66 6c
1119
Abbiamo individuato nientemeno che le STA $6C64, STA $6C65 e STA $6C66 ($8D è il codice esadecimale di STA con indirizzamento assoluto). Naturalmente, se non avessimo trovato questi comandi avremmo potuto tentare con delle varianti, ad esempio uno STA $6C64,X (9D 64 6C) o altro.
In pochi tentativi individuiamo una porzione di codice interessante:
(C:$1326) d 1100
.C:1100 98 TYA
.C:1101 18 CLC
.C:1102 69 DA ADC #$DA
.C:1104 8D 64 6C STA $6C64
.C:1107 8A TXA
.C:1108 18 CLC
.C:1109 69 64 ADC #$64
.C:110b A0 00 LDY #$00
.C:110d 38 SEC
.C:110e E9 0A SBC #$0A
.C:1110 90 04 BCC $1116
.C:1112 C8 INY
.C:1113 4C 0D 11 JMP $110D
.C:1116 18 CLC
.C:1117 69 E4 ADC #$E4
.C:1119 8D 66 6C STA $6C66
.C:111c 98 TYA
.C:111d 18 CLC
.C:111e 69 DA ADC #$DA
.C:1120 8D 65 6C STA $6C65
.C:1123 4C C0 13 JMP $13C0
Tralasciamo per il momento un'analisi approfondita e notiamo come il codice appaia incompleto. Non ha senso infatti che un calcolo inizi con un TYA quando il valore di Y non è stato impostato e potrebbe essere un valore arbitrario proveniente da chissà quali altre routine.
Tuttavia non è alle locazioni precedenti che dobbiamo cercare l'inizio della routine. Questo è confermato da un dump della memoria precedente. Notate come prima di $1100 sia presente una serie di $D8, $DA, $D9 e cioè nulla che somigli a degli opcode:
(C:$11b0) m 1080 112f
>C:1080 8d 71 03 a9 50 4c 92 66 ff ff ff ff ff ff ff 01
>C:1090 00 ff ff 00 00 20 dd 69 a9 ea 8d 15 03 a9 31 8d
>C:10a0 14 03 a5 c5 c9 40 d0 fa a9 af 8d 14 03 a9 41 8d
>C:10b0 15 03 4c dd 69 a9 00 8d d0 03 ee c6 03 60 a9 00
>C:10c0 8d d0 03 4c f5 0c 20 b9 11 4c 8c 11 a9 89 8d 00
>C:10d0 03 a9 6d 8d 01 03 a9 e3 8d 18 03 a9 10 8d 19 03
>C:10e0 4c e4 10 40 a9 08 20 d2 ff 4c 89 6d 00 00 00 00
>C:10f0 dc d8 d8 d8 da da da da da da da d9 d9 d9 d9 d9
>C:1100 98 18 69 da 8d 64 6c 8a 18 69 64 a0 00 38 e9 0a
>C:1110 90 04 c8 4c 0d 11 18 69 e4 8d 66 6c 98 18 69 da
>C:1120 8d 65 6c 4c c0 13 48 ad 01 d0 38 e9 02 90 03 8d
Cerchiamo dunque se c'è una JMP a $1100 e se il codice precedente questa JMP sia un buon candidato per essere la prima parte della nostra routine. In poco tempo troviamo:
(C:$2000) hunt 0800 cfff 4c 00 11
12fc
Possiamo così individuare la routine completa:
(C:$2000) d 12d7
.C:12d7 A2 00 LDX #$00 ; INIZIA DA QUI
.C:12d9 A9 00 LDA #$00 ; leggi l'high score
.C:12db 18 CLC ; le cui 6 cifre, gia' codificate (DA = 0),
.C:12dc 7D BA 1F ADC $1FBA,X ; si trovano a partire da $1FBA
.C:12df E8 INX ; esegui la somma dei loro codici POKE
.C:12e0 E0 06 CPX #$06 ; tralasciando i riporti, ottenendo cosi'
.C:12e2 D0 F7 BNE $12DB ; un numero da $00 a $FF.
.C:12e4 AA TAX ; consulta le tabelle a $4000, $7900, $7800
.C:12e5 BD 00 40 LDA $4000,X ; e associa a questo risultato
.C:12e8 AA TAX ; il contenuto di una locazione
.C:12e9 BD 00 79 LDA $7900,X ; da $7800 a $78FF (il magic number)
.C:12ec AA TAX ; che successivamente
.C:12ed BD 00 78 LDA $7800,X ; andremo a memorizzare nella schermata.
.C:12f0 A0 00 LDY #$00 ; resetta Y (per determinare le centinaia)
.C:12f2 38 SEC ; decrementa
.C:12f3 E9 64 SBC #$64 ; di 100
.C:12f5 90 04 BCC $12FB ; se A<0, vai alla stampa della cifra
.C:12f7 C8 INY ; altrimenti incrementa di 1 Y
.C:12f8 4C F2 12 JMP $12F2 ; e ripeti
.C:12fb AA TAX ; memorizza in X il resto
.C:12fc 4C 00 11 JMP $1100 ; continua a $1100
(C:$1326) d 1100
.C:1100 98 TYA ; Y contiene le centinaia
.C:1101 18 CLC ; aggiungi il codice POKE
.C:1102 69 DA ADC #$DA ; dello zero
.C:1104 8D 64 6C STA $6C64 ; e scrivi la prima cifra
.C:1107 8A TXA ; A=X
.C:1108 18 CLC ; incrementa
.C:1109 69 64 ADC #$64 ; di 100 (compensa l'operazione precedente)
.C:110b A0 00 LDY #$00 ; resetta Y (stiamo determinando le decine)
.C:110d 38 SEC ; decrementa A
.C:110e E9 0A SBC #$0A ; di 10
.C:1110 90 04 BCC $1116 ; se A<0, vai alla stampa della cifra
.C:1112 C8 INY ; altrimenti incrementa Y
.C:1113 4C 0D 11 JMP $110D ; e ripeti
.C:1116 18 CLC ; aggiungi il codice POKE
.C:1117 69 E4 ADC #$E4 ; dello zero + #$0A (compensa l'SBC sopra)
.C:1119 8D 66 6C STA $6C66 ; e scrivi la terza cifra
.C:111c 98 TYA ; Y contiene le decine
.C:111d 18 CLC ; aggiungi il codice POKE
.C:111e 69 DA ADC #$DA ; dello zero
.C:1120 8D 65 6C STA $6C65 ; e scrivi la seconda cifra
.C:1123 4C C0 13 JMP $13C0 ; continua con l'esecuzione...
Il codice si può considerare suddiviso in tre parti.
Prima parte ($12D7-$12E3): qui si legge l'high score (che è a sua volta memorizzato come codici POKE da $1FBA a $1FBF) e si sommano i valori dei suoi codici POKE con delle ADC, ignorando tutti i riporti oltre la seconda cifra esadecimale; si ottiene dunque un numero da $00 a $FF.
Questo è il dump della memoria all'inizio dell'esecuzione del gioco (tutti i punteggi migliori sono impostati a "000000" ossia sei $DA, e tutti i nomi sono riempiti con degli spazi ($20). Richiedendo questo dump dopo aver giocato alcune volte, potrete riconoscere i punteggi migliori che avrete ottenuto.
(C:$2020) m 1fb0 1fff
>C:1fb0 20 20 20 20 20 20 20 20 20 20 da da da da da da
>C:1fc0 20 20 20 20 20 20 20 20 20 20 da da da da da da
>C:1fd0 20 20 20 20 20 20 20 20 20 20 da da da da da da
>C:1fe0 20 20 20 20 20 20 20 20 20 20 da da da da da da
>C:1ff0 20 20 20 20 20 20 20 20 20 20 da da da da da da
Seconda parte ($12E4-$12EF): il numero ottenuto viene usato come indice, per leggere il contenuto di una locazione da $4000 a $40FF. Tale contenuto viene a sua volta utilizzato per leggere da un'altra tabella che comincia alla locazione $7900. Ancora una volta il contenuto determina una locazione a partire da $7800: il Magic Number dunque è sempre il contenuto di una locazione da $7800 a $78FF, e non è calcolato direttamente dal punteggio migliore.
Terza parte ($12F0-$12FE e $1100-$1125): qui è implementato un algoritmo che converte il Magic Number (inizialmente contenuto nell'accumulatore) in numero decimale, tramite divisioni successive. Ciascuna cifra è incrementata del codice POKE del carattere della cifra zero e memorizzata in $6C64-$6C66 come abbiamo visto sopra.
Per capire meglio il funzionamento dell'algoritmo di conversione, vediamo l'esempio con il magic number uguale a 253.
Quando l'esecuzione arriva a $12F0, l'accumulatore contiene $FD (253). Il registro Y viene inizializzato a 0. Tra $12F2 e $12FA, all'accumulatore si sottrae $64 (100) ottenendo $99, e il carry rimane acceso, segno che l'operazione non ha avuto un risultato negativo: il registro Y viene incrementato e si ripete l'operazione. Stavolta otteniamo $35 e il carry è sempre acceso, quindi nuovamente incrementiamo Y che vale $02 e ripetiamo. La terza volta l'accumulatore dopo la SBC vale $D1 e stavolta il carry viene spento, perché $35 - $64 ha un risultato negativo ($D1 è in effetti il risultato di $135 - $64). La BCC fa passare l'esecuzione a $12FB e Y rimane impostato a $02, che corrisponde appunto alle centinaia di 253.
A questo punto si copia l'accumulatore in X per liberare il registro: al valore di Y dobbiamo sommare il codice POKE del carattere della cifra zero ($DA) per ottenere $DC, e questa operazione deve avvenire nell'accumulatore; infine recuperiamo da X il valore originario dell'accumulatore ($12FB-12FE, $1100-1107). Incrementando di $64 ($1108-110A) otteniamo nuovamente il resto della divisione precedente, ossia $35.
Ora, dopo un reset di Y, abbiamo un ciclo del tutto simile al precedente, in cui stavolta ad ogni iterazione si sottrae 10 ($0A): nel nostro esempio, il ciclo è ripetuto incrementando Y fino al valore $05, e alla sesta iterazione il carry si resetta, segnalando che abbiamo un risultato negativo ($110B-$1115).
Ancora una volta, per riottenere il resto della divisione per 10 (ossia $03) dovremmo incrementare di $0A, ma poiché successivamente incrementeremmo il risultato di $DA, eseguiamo una sola addizione di $E4, ottenendo il codice POKE della terza cifra, ossia $DD, che memorizziamo nella sua locazione ($1116-111B).
L'ultima operazione è trasferire da Y all'accumulatore la cifra corrispondente alle decine, aggiungere il solito $DA e memorizzare il codice POKE nella locazione apposita ($111C-$1122).
Il flusso di esecuzione abbandona quindi la nostra routine con una JMP.