Gli attuali dispositivi informatici utilizzano il linguaggio binario per gestire, memorizzare i dati e comunicare. Il codice binario è usato al livello più basso da tutti i sistemi informatici adoperati fino ad oggi.
Esso prevede due soli stati (0 e 1, acceso/spento): lunghe sequenze numeriche composte da bit 0 e 1 permettono quindi di esprimere qualunque numero e codificare qualsiasi oggetto.
Gli stati 1 e 0 si hanno quando, rispettivamente, è o non è applicata una tensione sul circuito elettronico.
Con l’avvento dei computer quantistici tutto è destinato a cambiare: il qubit permette infatti di conservare ed elaborare più valori contemporaneamente non limitandosi ai classici 0 e 1. In un altro articolo abbiamo visto cosa sono e quali problemi risolvono i computer quantistici.
Linguaggio macchina e Assembly: cosa sono
Con l’espressione linguaggio macchina ci si riferisce al linguaggio con cui sono scritti i programmi eseguibili. Esso si basa sull’utilizzo del codice binario ed è eseguito direttamente dalla CPU. L’abbiamo visto nell’articolo dedicato a come funziona un processore.
Non esiste un unico linguaggio macchina: ogni processore può comprendere e gestire un suo particolare linguaggio macchina. In generale, a parte alcune differenze specifiche nella sintassi, i linguaggi macchina condividono principi e concetti molto simili.
Se si prende in esame una stessa architettura, ad esempio x86, ARM o RISC-V, tutti i processori compatibili con quell’architettura (Instruction Set Architecture, ISA) sono in grado di comprendere lo stesso linguaggio macchina, anche al netto di differenze hardware importanti.
In altri nostri articoli abbiamo visto cos’è la piattaforma x86, com’è nata e perché è “feudo” di Intel e AMD nonché le differenze tra ARM e x86.
Combinando sequenze di 1 e 0, come accennato in precedenza, si ottengono “parole” e “frasi” che nel linguaggio macchina vanno a formare delle istruzioni. Queste ultime sono ordini imperativi con cui viene chiesto al processore di eseguire un’operazione che interviene sullo stato interno del sistema. Per esempio usando le istruzioni del linguaggio macchina si può leggere il contenuto di una locazione di memoria oppure calcare la somma dei valori contenuti in due registri della CPU.
A un livello più alto rispetto al linguaggio macchina si pone Assembly, un linguaggio decisamente piuttosto ostico perché prevede ad esempio l’utilizzo di riferimenti diretti al contenuto dei registri del processore e della memoria. Per Assembly valgono le considerazioni che abbiamo già fatto nel caso del linguaggio macchina: struttura e sintassi sono legate a doppio filo con ogni specifico tipo di processore.
Linguaggi di programmazione ad alto livello
Un linguaggio di programmazione ad alto livello converte in Assembly e poi in codice macchina, l’unica lingua che il processore è in grado di capire, tutte le istruzioni fornite utilizzando strutture e sintassi umanamente più comprensibili.
A differenza del linguaggio macchina e di Assembly, la sintassi e le regole di programmazione di ciascun linguaggio ad alto livello sono semplici da usare. Inoltre è possibile usare uno stesso linguaggio di programmazione ad alto livello su più piattaforme differenti: su ciascuna di esse è possibile ottenere codice macchina compatibile a partire dallo stesso codice ad alto livello.
Nell’articolo sulle app coding abbiamo presentato alcune tra le migliori applicazioni per imparare a programmare: tutte quelle app spiegano come sviluppare con linguaggi di programmazione ad alto livello.
Differenza tra compilazione e interpretazione
Il codice sorgente di un programma, detto anche “listato”, è una sequenza di dichiarazioni di variabili, di operazioni di inizializzazione delle variabile tramite assegnamento, dichiarazioni di costanti, istruzioni e funzioni, strutture di controllo del flusso di esecuzione che vengono redatte secondo un certo paradigma di programmazione.
Come dicevamo in precedenza, a partire dal medesimo codice sorgente è possibile ottenere un programma eseguibile perfettamente funzionante su architetture (ISA) differenti.
L’attività di traduzione del codice sorgente scritto con il linguaggio ad alto livello in linguaggio macchina è detta compilazione. Il software compilatore consente di produrre un eseguibile compatibile con l’architettura scelta.
Si ottiene così codice ottimizzato anche se la compilazione può richiedere molto tempo a seconda della dimensione e della struttura del codice sorgente.
Con l’interpretazione viene tradotta ed eseguita ogni singola istruzione del programma. Con un approccio nettamente più lento rispetto alla compilazione, il codice sorgente del programma viene letto, elaborato ed eseguito senza creare alcun file eseguibile. Il codice non è in questo caso ottimizzato per la macchina in uso e risulta più ridondante rispetto al codice compilato.
Non sempre la compilazione è la scelta numero uno: il caso più emblematico è quello delle applicazioni Web. Non è infatti possibile perdere tempo a compilare ogni volta del codice: l’approccio interpretato assicura risultati migliori perché l’esperienza d’uso con il browser deve essere necessariamente fluida e veloce.
Il codice JavaScript è ormai alla base del funzionamento di qualunque applicazione Web e nel caso delle applicazioni più ricche il codice JavaScript utilizzato può essere davvero articolato, pesante e complesso.
JavaScript ha come caratteristica principale quella di essere un linguaggio interpretato: il codice caricato dal browser non viene compilato ma eseguito direttamente.
Con l’obiettivo di massimizzare le prestazioni, Google ha ad esempio introdotto il compilatore just-in-time (JIT) a livello di browser: esso effettua una “traduzione dinamica” durante l’esecuzione del codice piuttosto che in una fase precedente. In questo modo è possibile unire i vantaggi della compilazione del bytecode a quelli della compilazione nativa ottenendo prestazioni comparabili con quelle di una compilazione nel linguaggio macchina.
Il bytecode è linguaggio intermedio più astratto che si pone tra il linguaggio macchina e il linguaggio di programmazione. Esso è usato per descrivere le operazioni che costituiscono un programma riducendo la dipendenza dalla piattaforma hardware.
Il linguaggio ad alto livello che storicamente, fin dagli albori, ha utilizzato il bytecode è Java; altre piattaforme, col tempo, hanno iniziato a usare lo stesso approccio.
Nel caso di JavaScript, vista la crescente complessità del codice delle applicazioni Web più ricche, il compilatore JIT si serve appunto del bytecode per ottimizzare l’esecuzione del codice da parte del browser.
In un altro articolo abbiamo visto come funziona il motore JavaScript Google V8 e come faccia uso del compilatore JIT e di una serie di tecniche per ottenere codice macchina ottimizzato.
La delicata operazione di traduzione di codice JavaScript nel linguaggio macchina porta inevitabilmente con sé alcune problematiche di sicurezza. Pensate che codice progettato per essere caricato con una qualunque pagina Web viene poi trasformato in linguaggio macchina ed eseguito al livello più basso possibile su qualunque architettura.
Per questo motivo è essenziale aggiornare Chrome e tutti i browser derivati da Chromium per sanare tempestivamente le eventuali vulnerabilità di sicurezza ed è per la stessa ragione che Microsoft ha deciso di usare in Edge un approccio più draconiano usando Super Duper Mode per bloccare l’uso del compilatore JIT per tutti i siti che non si visitano spesso o che possono potenzialmente causare problemi.