Elementi di disassemblaggio: il Magic Number in Jumpin Jack

Autore: eregil - Pubblicato il 05-04-2010.
(© https://ready64.org - Versione Stampabile. Riproduzione Vietata.)

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.

Una fase di gioco

I punteggi migliori con il Magic Number

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.