Didattica

Didattica di Tutanum

Logica e architettura

Il set di istruzioni e l'assembler

Nella pagina dedicata alla microarchitettura abbiamo spinto il naso dentro il motore del processore, abbiamo seguito il percorso dati con i suoi registri, la sua unità aritmetica e logica e i suoi tre bus, e abbiamo visto la Mic-1 eseguire un programma memorizzato un passo alla volta, microistruzione dopo microistruzione, mentre la macchina IJVM prendeva vita come per magia. Era un'esperienza preziosa, eppure rimaneva qualcosa di frustrante in tutta quella precisione elettrica, perché il processore che abbiamo osservato obbediva a ordini scritti in una lingua che nessun essere umano parla davvero, e in questa pagina saliamo finalmente di un piano, quello in cui la macchina smette di essere un aggeggio e diventa un interlocutore, promettendoti che al termine della lettura capirai esattamente quali parole capisce e come puoi scrivergliele tu.

Il piano a cui saliamo ha un nome tecnico, architettura del set di istruzioni, che in gergo si abbrevia nelle sigla ISA e che il libro di Tanenbaum definisce con una frase da tenere a mente, cioè l'aspetto che la macchina assume agli occhi di un programmatore in linguaggio macchina. Osserva bene questa definizione, perché essa dice che per comunicare con la macchina contano soltanto alcune cose visibili, il modello di memoria con le sue celle numerate, i registri con i loro nomi, i tipi di dati riconosciuti e il vocabolario dei comandi accettati, mentre tutto il resto, pipeline, cache, previsioni dei salti, resta dietro il bancone, invisibile e per questo irrilevante per chi scrive il codice.

Questo vocabolario funziona come un contratto fra due mondi, e chiunque si metta a progettare una CPU deve prima decidere quali istruzioni la sua creatura capirà, quanti registri avrà, come saranno fatti i dati e quanti indirizzi potrà toccare, prima ancora di disegnare una singola porta logica. Tanenbaum osserva che un buon contratto deve soddisfare due parti in causa, i progettisti dell'hardware che chiedono istruzioni facili da realizzare in modo efficiente anche con le tecnologie future, e i progettisti del software che chiedono regolarità e completezza, perché un compilatore, il programma che traduce i linguaggi umani in linguaggio macchina, si trova in difficoltà quando certe scelte apparentemente ovvie gli vengono negate. Per rendere l'idea di quanto questo contratto sia preso sul serio, alcune architetture come lo SPARC pubblicano un documento formale di definizione che permette a produttori diversi di costruire chip che eseguono esattamente lo stesso codice, mentre Intel ha perfino fatto causa ai cloni per proteggere il vocabolario del suo Pentium.

Il primo capitolo del contratto descrive come è organizzata la memoria vista da questo piano, e la risposta è che tutti i computer dividono la memoria in celle consecutive e che la dimensione più comune della cella è di otto bit, cioè un byte, scelta nata dal fatto che i caratteri della tabella ASCII occupano sette bit e che con un bit di parità aggiuntivo riempiono la cella esattamente. I byte poi vengono raggruppati in parole di quattro o otto byte, trentadue o sessantaquattro bit, con istruzioni dedicate alla loro manipolazione, e molte macchine esigono che le parole siano allineate, cominciando cioè a indirizzi multipli della loro lunghezza, perché così la memoria lavora più speditamente. C'è persino una scelta di gusto su come disporre i byte dentro la parola, con il Pentium che mette il byte meno significativo all'indirizzo più basso e lo SPARC che fa l'opposto, un dettaglio che ha tormentato generazioni di programmatori quando i file creati su una macchina venivano letti sull'altra.

Il secondo capitolo elenca i tipi di dati che la macchina riconosce e sa trattare, e comincia dai numeri interi, che esistono in lunghezze di otto, sedici, trentadue e sessantaquattro bit, con o senza segno. Un intero senza segno di trentadue bit usa tutti i bit per il valore e conta da zero fino a due alla trentaduesima meno uno, estremi inclusi, cioè poco più di quattro miliardi, mentre lo stesso numero di bit in complemento a due, il trucco che hai imparato nella pagina sulla logica digitale, concede un bit al segno e copre valori da meno due alla trentunesima fino a due alla trentunesima meno uno. Per i numeri con la virgola esistono i formati in virgola mobile di trentadue, sessantaquattro e talvolta centoventotto bit regolati dallo standard IEEE 754, e per la finanza ereditata dal linguaggio COBOL esistono persino i decimali codificati con quattro bit per cifra, due cifre per byte, che hanno lasciato in eredità il famigerato baco del millennio, nato dal risparmio di due cifre per rappresentare l'anno.

A fianco dei numeri vivono i tipi non numerici, e i più quotidiani sono i caratteri, codificati secondo l'ASCII a sette bit o secondo Unicode a sedici bit, spesso raggruppati in stringhe delimitate da un carattere speciale di fine. Poi ci sono i valori booleani, vero e falso, dove la convenzione dice che zero significa falso e ogni altro valore significa vero, con il singolo bit riservato ai casi in cui tanti booleani si accorpano in una mappa di bit, come quando una parola di trentadue bit tiene traccia di trentadue blocchi liberi su un disco. Infine arriva il tipo che forse più ti incuriosirà, il puntatore, che altro non è che un indirizzo di macchina, un numero che indica dove qualcosa si trova in memoria, e se hai seguito la pagina precedente ne hai già incontrati tre vestiti da personaggi, il program counter che punta all'istruzione corrente, il puntatore allo stack che indica la cima della pila e il puntatore alle variabili locali della Mic-1.

Passiamo ora al cuore del contratto, la forma delle istruzioni, e scopriamo che ogni istruzione nasce da due pezzi, un codice operativo che dice cosa fare e poi un numero variabile di indirizzi che dicono dove prendere gli operandi e dove depositare il risultato, con macchine che si accontentano di zero indirizzi e macchine che ne prevedono uno, due o tre. Un'istruzione a tre indirizzi somma il contenuto di B al contenuto di C e deposita il totale in A, raccontando tutto in una riga sola, mentre le macchine a due indirizzi costringono a sovrascrivere uno degli addendi, e quelle a un indirizzo si appoggiano a un registro speciale chiamato accumulatore che partecipa di nascosto a ogni calcolo, un po' come il carrello della spesa che in certi negozi devi usare per forza. Alcune macchine scelgono istruzioni tutte della stessa lunghezza, cosa che semplifica la decodifica ma spreca spazio perché ogni istruzione deve essere lunga quanto la più lunga, altre accettano istruzioni di lunghezze diverse che riempiono la memoria con parsimonia ma rendono più faticoso capire dove comincia la prossima, e la scelta fra regolarità ed economia è una delle decisioni più pesanti che il progettista deve prendere.

Dove abitano gli operandi lo decidono le modalità di indirizzamento, e per capirle conviene figurarsi un'istruzione come un ordine di consegna a domicilio. Nell'indirizzamento immediato la merce viaggia dentro il modulo di consegna stesso, cioè il valore è scritto dentro l'istruzione ed è pronto all'uso senza ulteriori giri, comodissimo per le piccole costanti ma limitato dallo spazio disponibile. Nell'indirizzamento diretto il modulo riporta l'indirizzo completo della casa, e la consegna va sempre a quella porta, il che va bene per le variabili globali che vivono sempre nello stesso posto. Nell'indirizzamento a registro il modulo indica un armadietto del deposito veloce, cioè uno dei registri del processore, ed è di gran lunga il modo più usato perché i registri si aprono in un lampo e hanno numeri corti. Nell'indirizzamento a registro indiretto il modulo riporta il numero di un armadietto dentro cui sta scritto il vero indirizzo di consegna, così la stessa istruzione può servire case diverse a ogni esecuzione, capacità indispensabile per scorrere un vettore. Nell'indirizzamento indicizzato infine il modulo riporta una via di base e uno spiazzamento, come il nome della strada più il numero civico, e l'indirizzo effettivo nasce dalla somma fra un registro e una costante.

Quest'ultima capacità merita una storia, perché nei primi calcolatori mancava e i programmatori arrangiavano i cicli facendo riscrivere alle istruzioni il proprio indirizzo a ogni giro, un'idea attribuita a John von Neumann stesso e oggi guardata con orrore, dato che un programma che si modifica da solo è incomprensibile da leggere, non può essere condiviso tra processi e funziona male con le cache moderne. L'indirizzamento a registro indiretto e quello indicizzato sono nati proprio per porre fine a quella fantasia, e offrono un'ultima lezione pratica raccontata da Tanenbaum, e cioè che le macchine eleganti offrono ogni scelta possibile oppure una sola, perché le vie di mezzo costringono i compilatori a tarare scelte per cui non sono preparati.

Con i dati e gli indirizzi a posto possiamo elencare i tipi di istruzioni, e la sorpresa è che bastano circa mezza dozzina di famiglie per coprire ogni programma mai scritto, il che risponde alla domanda silenziosa di chi comincia, e cioè come faccia una macchina che capisce poche decine di gesti a far girare videogiochi e browser. La prima famiglia è quella dei trasferimenti dati, dove per trasferimento si intende in realtà una copia, perché l'originale resta intatto nella sua locazione, e Tanenbaum fa notare che nel linguaggio comune il verbo funziona in tutt'altro modo, visto che quando diciamo che Mario si è trasferito da New York alla California intendiamo che il soggetto si sia spostato davvero, senza lasciare una copia di se stesso sulla costa est, con varianti chiamate load quando salgono dalla memoria ai registri, store quando scendono, e move quando viaggiano fra registri. La seconda famiglia è l'aritmetica intera, addizioni, sottrazioni, moltiplicazioni e divisioni, cui si aggiungono le operazioni booleane AND, OR, NOT e XOR, quest'ultima simpatica perché tende a inserire in media tanti uno quanti ne toglie.

Le operazioni booleane nascondono un trucco quotidiano che vale la pena vedere una volta nella vita, l'estrazione di bit con la maschera. Immagina una parola di trentadue bit che trasporta quattro caratteri da otto bit ciascuno e vuoi isolare il secondo carattere per mostrarlo sullo schermo, e la mossa è fare l'AND della parola con una maschera che tiene a uno soltanto gli otto bit del secondo carattere e azzera tutti gli altri, poi far scorrere il risultato di sedici posizioni verso destra, e il carattere emerge pulito, mentre per inserire un carattere nuovo si usa l'OR dopo aver preparato il terreno con un'altra maschera. Accanto alle operazioni a due operandi esistono quelle unarie, come lo scorrimento dei bit verso destra o sinistra, che moltiplicano e dividono per potenze di due, così che per calcolare diciotto volte n un compilatore furbo somma n scorso di quattro bit, cioè sedici volte n, con n scorso di un bit, cioè due volte n, ottenendo il prodotto con due scorrimenti e una somma anziché con una moltiplicazione vera.

La famiglia più romantica è quella dei confronti e dei salti, perché è quella che permette alla macchina di prendere decisioni. Dopo certe operazioni il processore aggiorna delle piccole spie chiamate flag, bandierine, tipicamente una che segnala il risultato zero, una il segno negativo, una l'eccedenza chiamata overflow e una il riporto uscito dal bit più a sinistra, e un'istruzione di confronto esamina due operandi e alza le spie corrispondenti senza mai provocare un overflow, il che è più difficile di quanto sembra, dato che una sottrazione fra un positivo grande e un negativo grande potrebbe traboccare mentre il confronto deve restare corretto sempre. Viene poi un dettaglio che confonde i principianti, e cioè che tre bit possono ordinare da zero a sette oppure da meno quattro a più tre a seconda che li leggi senza o con segno, per cui gli insiemi di istruzioni offrono due famiglie di salti, una per ciascuna lettura. Terminata l'ispezione, un salto condizionato legge le spie e sposta l'esecuzione all'indirizzo scritto nell'istruzione soltanto quando la condizione regge, per esempio tornando all'inizio di un ciclo finché il puntatore resta minore della fine del vettore.

Questi salti hanno una storia che merita di essere raccontata, perché quando i programmi diventarono grandi i programmatori persero il filo della sequenza di esecuzione proprio a causa dei salti, che facevano saltellare il program counter qua e là come un grilletto nervoso, e nel 1968 Edsger Dijkstra pubblicò una lettera famosa dal titolo provocatorio che diceva l'istruzione goto è da considerarsi dannosa, scatenando la rivoluzione della programmazione strutturata, con i suoi cicli e le sue scelte annidate che aiutano la mente umana. C'è un'ironia gentile nel fatto che i programmi compilati, quelli che questo capitolo studia, sono pieni di salti a ogni piè sospinto, dato che il compilatore traduce i costrutti strutturati proprio in salti condizionati, così che chi legge l'assembler convive ancora oggi con il grillo parlante che Dijkstra voleva bandire.

L'ultima grande famiglia è quella delle chiamate di procedura, le istruzioni che permettono a un programma di chiamare in aiuto un sottoprogramma e di riprendere poi il filo esatto da dove l'aveva lasciato, e il problema centrale è tener traccia dell'indirizzo di ritorno. Depositarlo in una cella fissa di memoria è la soluzione peggiore, perché la seconda chiamata annidata sovrascriverebbe quella della prima, e appoggiarlo nella prima parola della procedura funziona finché una procedura chiama se stessa, il gioco della ricorsione che gli informatici adorano. La soluzione vittoriosa è la pila, lo stack visto nella pagina precedente, dove l'istruzione di chiamata spinge in cima l'indirizzo dell'istruzione successiva e salta alla procedura, che al termine estrae quel valore e lo rimette nel program counter, così ogni invocazione trova il proprio indirizzo di ritorno al sicuro, anche quando la ricorsione impila chiamate su chiamate come piatti nella credenza.

Seguiamo una chiamata con calma, perché è il gesto che sottende ogni funzione di ogni linguaggio. Il programma principale arriva a una riga che invoca la funzione per calcolare, poniamo, il massimo tra due voti, l'istruzione di chiamata calcola l'indirizzo della riga successiva, lo deposita sulla pila e salta al corpo della funzione, dove trovi un prologo che salva il puntatore delle variabili locali del chiamante, aggiorna i puntatori e riserva spazio per le variabili della nuova esecuzione, un territorio chiamato record di attivazione che oggi è quello in cima alla pila. Quando la funzione ha finito, l'epilogo ripristina la situazione, il valore di ritorno viaggia di solito in un registro, e l'istruzione di ritorno preleva dalla pila l'indirizzo salvato e lo rimette nel program counter, così l'esecuzione riprende esattamente dalla riga dopo la chiamata, come un corriere che, consegnata la merce, torna al punto della sua corsa segnato sul block notes. Per rendere prologhi ed epiloghi veloci il Pentium 4 offre persino istruzioni dedicate chiamate ENTER e LEAVE, segno di quanto questi passaggi contino.

Per godere la potenza di questo meccanismo basta il gioco delle torri di Hanoi, in cui tre pioli portano dischi d'oro di grandezza decrescente e il compito è spostarli tutti su un piolo diverso senza mai posare un disco sopra uno più piccolo, e la soluzione elegante è una procedura che chiama se stessa, spostando prima tutti i dischi tranne uno sul piolo di mezzo, poi l'ultimo sul piolo d'arrivo, poi la pila dal mezzo all'arrivo. Con tre dischi la chiamata iniziale genera altre tre chiamate e in totale sette mosse, con quattro dischi le mosse salgono a quindici, e la leggenda dei monaci che giocano con sessantaquattro dischi d'oro dice che il mondo finirà quando avranno finito, il che mette in prospettiva quanto conti uno stack che ricorda da solo ogni strada del ritorno, anche a incastri profondissimi.

Per dare a tutte queste idee un volto, il libro presenta quattro macchine caratteriali, quattro esempi di ISA scelti per mostrare la varietà delle scelte possibili, e il primo è il Pentium 4, che assomiglia a un patriarca ingombro di storia, discendente per linea diretta dal 4004 a quattro bit passando per l'8008, l'8080, l'8086 e l'80386 che fissò l'architettura IA-32 a trentadue bit, e porta con sé tre modalità di funzionamento, una che lo finge un antico 8088 senza protezioni, una che finge un 8088 protetto per i vecchi programmi di MS-DOS e una protetta a quattro livelli di privilegio, con registri dal nome melodico come EAX, EBX, ECX, EDX e istruzioni irregolari con fino a sei campi di cui cinque opzionali, tanto intricate che quando Intel esaurì i codici operativi disponibili dovette inventare un byte di fuga che annuncia un secondo byte di codice, e tutto questo groviglio esiste per un motivo nobile, e cioè che ogni programma scritto per i predecessori deve continuare a funzionare.

Il secondo volto è l'UltraSPARC III, il minimalista ordinato, nato dai laboratori di Berkeley negli anni ottanta dove si teorizzò la filosofia RISC e portato sul mercato da Sun nel 1987, e il suo ISA pubblico è così pulito che il documento di definizione permette a chiunque di costruire chip compatibili. Tutte le sue istruzioni sono lunghe esattamente trentadue bit e allineate in memoria, le istruzioni aritmetiche prendono due registri sorgente e un registro destinazione, persino l'indirizzo di una lettura in memoria nasce dalla somma di due registri o di un registro con una costante di tredici bit compresa fra meno quattromilacentonovantasei e più quattromilacentonovantacinque, quasi ogni operazione avviene fra registri con l'unica eccezione delle istruzioni load e store, e un registro, lo R0, è cablato allo zero in modo che non possa mai essere modificato, un'eleganza che i progettisti si concedono quando il vocabolario è piccolo e regolare.

Il terzo volto è l'8051, il piccolo operaio parsimonioso che non cerca la gloria dei computer da scrivania e vive dentro semafori, radiosveglie e milioni di oggetti comuni, nato negli anni ottanta quando i clienti di Intel chiesero un chip che tenesse processore, memoria e controllori di ingresso e uscita dentro lo stesso involucro per abbattere i costi, e le sue istruzioni misurano da uno a tre byte, i suoi spazi di memoria sono due mondi separati di sessantaquattro kilobyte l'uno, quello dei programmi in sola lettura e quello dei dati, dentro il chip trovano posto quattro kilobyte di memoria permanente e centoventotto byte di memoria di lavoro, e custodisce persino sedici byte di quella memoria indirizzabili bit per bit, dal bit zero al bit centoventisette, con istruzioni per accenderli, spegnerli e interrogarli, pensate per pilotare direttamente interruttori e spie. Il suo registro di stato commuta fra quattro banchi di otto registri in un lampo, così può rispondere a valanghe di interruzioni senza perdere un colpo, e per questo è ancora oggi uno dei processori più diffusi al mondo.

Il quarto volto è il più strano, perché è un volto senza corpo, ed è la JVM, la macchina virtuale di Java che hai già incontrato nella sua versione didattica IJVM mentre osservavi la Mic-1. Il suo set di istruzioni, il bytecode, codice a byte, non è eseguito da alcun processore di silicio, e vive invece dentro macchine virtuali costruite in software, interpreti e compilatori appena in tempo, just-in-time, che traducono i bytecode nel linguaggio della macchina ospite man mano che servono, dentro i browser, i telefoni e milioni di programmi. È una macchina a pila, nel senso che le istruzioni prendono i loro operandi in cima alla pila anziché dai registri, e dimostra che un set di istruzioni è un'idea indipendente dal silicio, perché lo stesso contratto può essere onorato da un impianto elettrico oppure da un programma.

Questi quattro volti si dispongono lungo una vecchia linea di demarcazione che conviene conoscere per nome, lo scontro fra CISC e RISC, dove CISC, computer con set di istruzioni complesso, indica la filosofia del Pentium 4 con le sue istruzioni sofisticate che fanno cose articolate, indirizzi di memoria ammessi come operandi in molti comandi e formati variabili, e RISC, computer con set di istruzioni ridotto, indica la filosofia dell'UltraSPARC con le istruzioni semplici e fisse, i tre indirizzi e la regola che la memoria si tocca soltanto con load e store. Il motivo didattico per cui il libro ci fa studiare entrambe le filosofie, con l'8051 come terzo termine minuscolo, sta nel fatto che ogni progettista di ISA affronta le stesse decisioni, e vedere dove due scuole hanno piazzato i propri confini, e con quali conseguenze sui compilatori e sulle prestazioni, è l'unico modo per capire davvero il mestiere.

Arriviamo ora all'assembler, il ponte fra il linguaggio della macchina e la tua scrivania, e scopriamo che il linguaggio macchina puro, la sfilza di numeri binari che la Mic-1 eseguiva, è scritto in una forma che nessuno programma più da decenni, perché ricordare che la sequenza 05 significa somma a EAX una costante richiede una memoria d'acciaio, mentre ricordare che ADD significa aggiungi viene naturale. Il linguaggio assembler è la scrittura umana del linguaggio macchina, in cui ogni istruzione porta un nome simbolico, detto mnemonico, come MOV per muovere, ADD per sommare e JMP per saltare, e in cui anche gli indirizzi diventano parole scelte da te, così che a ciascuna riga del programma assembler corrisponde esattamente una parola del linguaggio macchina, il che lo distingue da ogni linguaggio di livello più alto e gli concede un privilegio invidiabile, l'accesso a tutte le funzionalità della macchina, bit di overflow compresi, che i linguaggi come Java tengono nascosti.

Vediamo all'opera un programma assembler vero, il calcolo della somma di un vettore di mille ventiquattro interi, e leggiamolo riga per riga com'è giusto che sia. La prima istruzione, MOV R1,#0, svuota il registro R1 che custodirà la somma, e il cancelletto dichiara il valore immediato, la merce dentro il modulo di consegna. La seconda, MOV R2,#A, scrive in R2 l'indirizzo di partenza del vettore A. La terza, MOV R3,#A+4096, scrive in R3 il primo indirizzo oltre il vettore, perché mille ventiquattro interi di quattro byte occupano quattromilanovantasei byte. Sotto l'etichetta CICLO, che funziona da cartello stradale piantato nel listato a cui i salti possono puntare, sta ADD R1,(R2), che somma alla somma l'elemento puntato da R2, con le parentesi tonde a promettere l'indirizzamento indiretto. Seguono ADD R2,#4, che fa avanzare il puntatore di una parola, CMP R2,R3, che confronta i due puntatori alzando le spie, e BLT CICLO, che ordina di tornare al cartello finché R2 resta minore di R3. Nota la bellezza rivelata da Tanenbaum, e cioè che il ciclo non contiene alcun indirizzo di memoria vero, perché tutto il giro è fatto di registri, costanti e un cartello, e nota anche che questa è in realtà un programma per il Pentium 4 travestito, con nomi addolciti per la lettura, perché la sintassi standard di quel processore rasenta per davvero l'assurdo.

Chi traduce questo listato in numeri è l'assembler, e il suo mestiere nasconde un problema garbato, i riferimenti in avanti, perché la prima riga del programma potrebbe saltare a un'etichetta definita all'ultima riga, e l'assemblatore non può tradurre il salto finché non sa dove vive il cartello. La soluzione classica si chiama assemblaggio a due passate, e la prima passata è una ricognizione in cui l'assembler legge tutto il programma annotando su un quaderno interno, la tabella dei simboli, il valore di ogni etichetta e di ogni nome, contando gli indirizzi con un contatore che avanza della lunghezza di ciascuna istruzione, esattamente come il program counter che hai imparato a conoscere. Finita la ricognizione ogni simbolo ha un valore noto, e nella seconda passata l'assembler rilegge il programma e traduce riga per riga usando il quaderno, così che il salto verso il futuro trova la sua destinazione già scritta, e tutto il meccanismo, per quanto modesto, è il primo traduttore vero e proprio che incontriamo nel percorso.

L'assembler però sa fare di più che tradurre, e per dimostrarlo ti presento le pseudoistruzioni e le macro. Le pseudoistruzioni, chiamate anche direttive, sono ordini rivolti all'assembler anziché alla macchina, comandi come DD che riserva una parola di memoria per una variabile, PROC e ENDP che delimitano una procedura, PUBLIC e EXTERN che regolano quali nomi si vedono fra file diversi, e INCLUDE che importa un file dentro un altro. Le macro vanno oltre, perché permettono di dare un nome a una sequenza di istruzioni, per esempio quattro righe che scambiano il contenuto di due variabili, e poi di invocare quel nome dove serve, con l'assembler che durante la traduzione sostituisce il nome con il corpo, parametri inclusi, come un cuoco che stende una volta sola la sfoglia per preparare tanti ravioli identici. Il punto affascinante è il momento in cui avviene la sostituzione, durante l'assemblaggio e non durante l'esecuzione come accade per le chiamate di procedura, così che nel programma finale non resta alcuna traccia delle macro, e questo gioco di manipolazione del testo, espansione dopo espansione, è un anticipo del mestiere dei compilatori.

Resta la domanda onesta, e cioè perché leggere l'assembler oggi, quando i linguaggi di alto livello fanno quasi tutto. Le ragioni sono almeno quattro, e la prima è la messa a punto delle prestazioni, perché nei programmi reali una piccola parte del codice, talvolta il dieci per cento, consuma la quasi totalità del tempo, e riscrivere quella parte in assembler può valere un fattore due o tre, come racconta il calcolo del libro in cui un programma costa dieci anni di lavoro in alto livello e cento secondi di esecuzione, cinquanta anni e trentatré secondi se riscritto tutto in assembler, oppure quindici anni e quaranta secondi se solo il dieci per cento critico viene rifatto a mano, un compromesso che vince su entrambi i fronti. La seconda ragione è la memoria scarsa, perché le smart card, le schede intelligenti, e i piccoli controllori hanno pochi kilobyte e ogni byte risparmiato conta. La terza è la comprensione dei compilatori, che devono generare proprio questo linguaggio per vivere. La quarta ragione è tua, se ti piace la sicurezza informatica, perché quando un programma si blocca il debugger, il cacciatore di errori, ti mostra proprio l'assembler, e capire che cosa fa davvero un eseguibile, magari uno sospetto, significa poterlo leggere nella sua lingua originale anziché fidarsi di etichette gentili. Per questo la strada che scende verso il linguaggio C, il ponte fra alto e basso livello, e verso la pagina su come si costruisce il software parte proprio da qui.

La teoria, per quanto raccontata con calma, lascia un velo di nebbia finché resta soltanto letta, quindi adesso appoggia la spiegazione e vieni a sporcarti le dita sulla tastiera con sei esperimenti, tutti al computer, senza fili sporchi né corrente da maneggiare, ordinati dal più semplice al più articolato, e ciascuno ti farà toccare con mano un pezzo di quanto hai letto.

Per il primo esperimento apri nel browser un simulatore a otto bit, per esempio easy6502, che emula il processore 6502 del mitico computer vintage, oppure un simulatore dell'8051 come quello che trovi cercando EdSim51 in rete, così puoi fare conoscenza con uno dei quattro personaggi del capitolo. Scrivi il programma che somma due numeri, nel 6502 sarà qualcosa come LDA #$03 per caricare tre nel registro A, ADC #$04 per sommarvi quattro con l'aiuto del riporto, e STA $0200 per depositare il risultato in memoria, poi assembla con un clic e fai avanzare il programma un passo alla volta osservando i registri A, X e Y e le spie cambiare ad ogni mossa, perché vedere il sette apparire nella cella di memoria dopo aver assistito a ogni singola istruzione è un'esperienza che la sola lettura non regala.

Per il secondo esperimento prendi il tuo processore reale, quello del computer che hai davanti, e traduci a mano tre o quattro istruzioni vere cercandone il significato in rete con una ricerca del tipo riferimento dei codici operativi x86, che trovi documentato su siti dedicati e sulla voce giusta di Wikipedia. Un esempio per cominciare è la sequenza B8 2A000000, dove il primo byte dice al processore di caricare una costante nel registro EAX e i quattro byte successivi, letti nel modo detto little-endian, cioè con l'estremità piccola per prima, contengono il valore, e la cifra esadecimale 2A vale quarantadue, dunque l'intera sequenza significa proprio scrivi quarantadue in EAX. Se usi Linux puoi ottenere all'istante un'intera sinfonia di istruzioni da tradurre aprendo il terminale, scrivendo objdump -d seguito dal nome di un piccolo programma, e riconoscendo nei mnemonici che compaiono i gesti che questo capitolo ti ha raccontato.

Per il terzo esperimento resta sulla carta e scrivi in pseudocodice, cioè in italiano ordinato e rigoroso, la funzione che calcola il massimo fra due numeri, qualcosa del tipo se a è maggiore di b allora il massimo vale a altrimenti il massimo vale b, e poi immagina come quelle frasi diventerebbero istruzioni riga per riga, con un confronto che alza le spie, un salto condizionato verso un'etichetta nel ramo che prende b, una copia che porta il vincitore nella cella del risultato e un secondo salto che salta la parte del b per raggiungere la fine. Quando avrai scritto questa piccola coreografia capirai nel profondo che ogni if di ogni linguaggio è, a questo livello, un confronto seguito da un salto, e che le scelte dei linguaggi di alto livello sono abiti cuciti sopra questi gesti spartani.

Per il quarto esperimento osserva un compilatore mentre lavora, servendoti per esempio di Compiler Explorer, il sito che mostra in due riquadri il codice che scrivi e l'assembly che il compilatore genera man mano che batti. Scrivi nel riquadro di sinistra una riga che somma due interi, per esempio la somma di due voti, e guarda nel riquadro di destra comparire coppie di istruzioni mov e add, i nostri vecchi amici vestiti di qualche lettera di contorno, poi modifica l'espressione e osserva il codice ribellarsi e riorganizzarsi in tempo reale, così che ti resti chiaro per sempre che il compilatore è un traduttore della stessa famiglia dell'assembler, solo molto più industrioso.

Per il quinto esperimento fai il percorso inverso con un disassemblatore online, per esempio quello ospitato da defuse.ca, dove puoi incollare i byte B8 2A000000 del secondo esperimento e vedere il linguaggio macchina tornare a mnemonici leggibili, oppure incollare dei byte presi da un piccolo eseguibile che hai compilato tu e ammirare il viaggio di ritorno dalla terra dei numeri alla terra delle parole. Prova a riconoscere nelle righe che compaiono i gesti del capitolo, i movimenti, le somme, i salti con i loro cartelli, e rifletti sul fatto che ogni programma che gira sul tuo computer può essere letto in questa forma da chiunque, il che è una lezione di umiltà per chi scrive software e una lezione di potere per chi lo studia.

Per il sesto e ultimo esperimento torna alla pagina precedente e cerca in rete un simulatore della IJVM o della Mic-1, ce ne sono diversi liberi realizzati attorno al libro di Tanenbaum, poi carica un piccolo programma che contiene una chiamata di procedura, per esempio una funzione che calcola il quadrato di un numero, e fallo avanzare passo dopo passo tenendo d'occhio la pila e i suoi puntatori. Osserva il momento esatto in cui l'istruzione di invocazione deposita sulla pila l'indirizzo di ritorno e il puntatore alle variabili, guarda la pila crescere mentre la procedura lavora, e osserva infine l'istruzione di ritorno ripescare l'indirizzo salvato e restituirlo al program counter, così che l'esecuzione riprenda dalla riga successiva alla chiamata, e avrai visto con i tuoi occhi lo stack fare il mestiere per cui tutti i linguaggi del mondo gli affidano i ritorni.

Questa pagina attinge soprattutto al capitolo sull'architettura del set di istruzioni e al capitolo sul livello del linguaggio assembler del libro Architettura dei calcolatori di Andrew S. Tanenbaum, dove i tipi di dati, i formati, le modalità di indirizzamento, le quattro macchine esempio, le due passate dell'assembler e le macro ricevono trattazione piena, con le torri di Hanoi a fare da collaudo ricorsivo. Ora che sai quali parole capisce il processore e come si scrivono in forma umana, il gradino successivo scende nel piano sottostante per capire dove quelle istruzioni vivono mentre aspettano di essere eseguite e come viaggiano, ed è la ragione per cui la prossima pagina è dedicata alla memoria, ai bus e all'ingresso/uscita, che ti aspetta con le sue strade e i suoi incroci.