Dentro i Sistemi Operativi – il KERNAL di Commodore

Nel precedente articolo abbiamo introdotto il Debug Monitor e abbiamo accennato alle funzioni base che dovrebbe avere un sistema operativo minimale, per consentire l’esecuzione di semplici programmi sulla macchina.

Ad essere precisi, su questo genere di macchine non è indispensabile un vero Sistema Operativo, ma torna molto utile avere a disposizione delle funzioni generiche che siano quanto più possibile indipendenti dalla piattaforma. L’indipendenza dalla piattaforma consente, teoricamente, di riutilizzare lo stesso codice su macchine diverse purché queste dispongano di un livello di astrazione compatibile.

In questa ottica di astrazione, parola chiave di questo articolo, i computer ad 8 bit della casa americana Commodore venivano dotati del cosiddetto KERNAL: Keyboard Entry Read, Network And Link.

Si, lo so, l’acronimo non vuol dire assolutamente nulla. Infatti è quasi certo che il nome KERNAL sia nato da una storpiatura della parola Kernel, e solo in un secondo momento sia stato inventato l’acronimo per giustificare la nuova parola (classico esempio di acronimo inverso).

Nella documentazione del Commodore PET2001, del 1977, si parla correttamente di Kernel. Successivamente, a quanto pare, il progettista Robert Russell scrisse erroneamente la parola KERNAL (con la A) nei suoi appunti per il Commodore VIC20 e, nello scrivere la documentazione tecnica per lo stesso, nel 1980, pare che i due addetti Neil Harris e Andy Finkel abbiano trascritto la parola con il suo nuovo/errato spelling.

L’astrazione offerta dal KERNAL consiste in una semplice ma utile raccolta di funzioni essenziali, che spaziano dalla gestione della comunicazione seriale alla manipolazione dei files, passando per le routine di gestione dello schermo e della tastiera.

Queste funzioni hanno degli indirizzi fissi in memoria (nell’ultima pagina dell’address space) ed espongono al programmatore un’interfaccia ben definita per richiedere i servizi previsti dal sistema.

Arriviamo subito ad un esempio concreto per chiarire meglio le idee: Apertura di un file e caricamento in memoria dello stesso.

In linguaggio BASIC questo sarebbe equivalente al semplicissimo comando LOAD “NOMEFILE”,D,S dove D ed S sono due numeri interi che identificano la periferica di lettura (8,1 per specificare ad esempio il primo floppy drive del bus seriale). L’interprete BASIC non contiene direttamente il codice per gestire tutto il lavoro, ma si limita a riutilizzare alcune funzioni del KERNAL che sono:

  • SetLFS per selezionare la periferica con i numeri D ed S (D = Device Number, S = Secondary Address)
  • SetNAM per selezionare il nome del file passando la stringa “NOMEFILE” come parametro
  • Load per caricare il file precedentemente selezionato in una determinata zona di memoria (variabile in base alla versione dell’interprete BASIC, alla mappa di memoria e al modello di computer Commodore usato).

Nello specifico, queste funzioni (SetLFS, SetNAM, Load) vengono identificate da specifici indirizzi di memoria (rispettivamente FFBA, FFBD e FFD5) che, a loro volta, contengono un’istruzione di salto ad un secondo indirizzo contenente il vero codice della funzione. Questo consente di avere degli indirizzi prefissati indipendentemente dalla versione del KERNAL, da cui deriva l’efficacia dell’astrazione.

Se vogliamo chiamare la funzione Load, quindi, dobbiamo semplicemente eseguire un’istruzione di Call all’indirizzo FFD5, la CPU troverà a quell’indirizzo un’ulteriore istruzione di salto (variabile in base alla versione del KERNAL) che porterà all’esecuzione della funzione vera e propria. Alla fine della funzione di Load, il KERNAL conterrà un’istruzione di Return che consentirà al programma utente di proseguire con il suo codice.

Le istruzioni Call e Return, nel linguaggio macchina dei processori 6502 e derivati, sono implementate rispettivamente dagli opcode JSR e RTS.

Jump to SubRoutine salva il Program Counter sullo stack (in realtà salva l’indirizzo della prossima istruzione meno 1) ed esegue un salto all’indirizzo specificato. ReTurn from Subroutine fa l’operazione inversa, cioè preleva il contenuto dello stack e lo riposiziona sul Program Counter (dopo aver sommato 1), eseguendo di fatto un salto all’indietro.

Come vengono passati i parametri per le funzioni? Nel nostro caso specifico, dobbiamo passare minimo 3 parametri, cioè il Device Number, il Secondary Address e la stringa contenente il File Name.

Ciascuna funzione del KERNAL ha un suo modo per ricevere i parametri e questi sono ben documentati. Tipicamente, per le funzioni con un solo parametro, basta scrivere il valore nell’accumulatore (registro A). Le funzioni che gestiscono regioni di memoria, come array o stringhe, invece prendono come parametro la lunghezza del buffer in A e l’indirizzo del buffer nella coppia di registri X e Y.

Questo documento contiene l’elenco delle funzioni del KERNAL, completo di indirizzo della funzione (da usare nell’istruzione di Call, cioè JSR), della modalità di passaggio dei parametri e di eventuali valori di ritorno. Nell’elenco sono presenti inoltre l’elenco dei registri che vengono “toccati” dalle funzioni e l’indirizzo “reale” delle funzioni. Tuttavia bisogna prendere queste informazioni con le pinze, perchè sono specifiche del Commodore 64.

Se vogliamo rispettare la filosofia di astrazione del KERNAL, dobbiamo ignorare queste informazioni e attenerci agli indirizzi fissi del cosiddetto KERNAL Vector. In particolar modo, dobbiamo sempre supporre che tutti i registri vengano modificati, e quindi salvarli sullo stack all’occorrenza, sempre per lo stesso motivo.

A titolo esemplificativo prendiamo la funzione CHRIN, che legge un byte dalla periferica di input standard (normalmente la tastiera). Secondo la tabella di prima, l’indirizzo reale della funzione sarebbe F157, ed i registri modificati sono A, che conterrà il byte letto, ed X che viene usato come variabile temporanea.

Per ottimizzare il codice potremmo essere tentati di salvare soltanto il contenuto del registro X e di fare direttamente una JSR verso l’indirizzo reale F157, piuttosto che l’indirizzo fisso FFCF.

Sebbene questo sia perfettamente lecito sulla macchina di riferimento (il Commodore 64 in questo caso), quasi sicuramente non funzionerà su tutte le altre macchine per almeno 2 motivi:

  1. Non è detto che tutte le implementazioni abbiano il proprio indirizzo reale esattamente in F157. Mentre è garantito dal KERNAL che l’indirizzo fisso FFCF sia sempre rispettato in tutte le sue versioni.
  2. Non è detto che tutte le implementazioni sporchino soltanto il registro X. Diversi modelli di computer, con differenti chip di I/O e di conseguenza diverse routine di interfacciamento, potrebbero sporcare anche il registro Y, quindi è buona norma salvare tutti i registri che ci interessano.

Tramite questo semplice esempio, abbiamo visto come sia possibile costruire un livello di astrazione molto elementare. Il KERNAL consente, se usato correttamente, di effettuare il porting di un programma in codice macchina su più modelli di computer Commodore in modo relativamente semplice.

Grazie alla sua struttura possiamo sicuramente considerarlo come un esemplare rudimentale di Sistema Operativo, perché fornisce, a modo suo, alcuni dei servizi più importanti: l’astrazione dell’hardware tramite la gestione dei files e delle periferiche.

Per concludere questa panoramica, un link utile contenente la descrizione completa delle istruzioni delle CPU della famiglia 6502 http://www.6502.org/tutorials/6502opcodes.html

Nel prossimo articolo punteremo un po’ più in alto e mostreremo un altro concetto fondamentale dei Sistemi Operativi, che si aggiunge all’astrazione dell’hardware, ed è il cosiddetto task. Tramite il task si introduce un secondo livello di astrazione, cioè l’astrazione del software. Suona strano? Lo vedremo nel prossimo articolo!

Press ESC to close