Come fa un computer a “leggere” Python, Java o qualsiasi altro linguaggio, se in realtà comprende solo 0 e 1? E perché, a volte, un codice apparentemente perfetto si trasforma in un “errore di segmentazione”, in un “comportamento indefinito” o in un messaggio come “utente non trovato” (anche quando l’utente esiste davvero)? Il computer è stupido. Ma molto, molto veloce e obbediente.
Cominciamo con una verità spiacevole e fondamentale, che di solito viene nascosta dietro parole sofisticate come “innovazione”, “intelligenza artificiale”, “architettura cloud-native” o “siamo tutti basati sui microservizi, quindi possiamo scalare orizzontalmente”. La dura realtà è questa: i computer non capiscono e non comprendono assolutamente nulla. Letteralmente. Non hanno alcun concetto del mondo. Per loro non esistono “documenti”, “report” o “utenti”. Tutto ciò che esiste sono impulsi elettrici, interpretati come zero e uno. Esaminiamo alcune delle nostre astrazioni preferite e vediamo come appaiono nella “cucina” del processore.
Per un computer, la variabile totalSum equivale semplicemente a questo: “Ehi, memoria! Dammi i dati che si trovano, per esempio, all’indirizzo 0x7ffeefbff58c (011111111111111011101111101111111111010110001100). E non fare domande.”
Una “classe Cliente con campi e metodi” non è altro che un insieme strutturato di byte in memoria. Qualcuno (il compilatore o l’interprete) ha stabilito che i primi quattro byte rappresentino l’ID, i successivi cinquanta il nome e che, a un certo offset, ci sia un puntatore a un pezzo di codice da eseguire quando viene chiamato il metodo .save().
L’oggetto stesso non ne è minimamente consapevole.
Il “ciclo for da 1 a 10” è, a basso livello, solo una sequenza di comandi primitivi:
- caricare il valore 1 in un registro;
- eseguire il corpo del ciclo;
- incrementare il valore del registro;
- confrontarlo con 10;
- se non è uguale, tornare al punto 2 con un salto incondizionato (
JMP).
Nessun romanticismo: solo confronti e salti.
La “sezione tabellare di un documento” è, in genere, una regione di memoria o una tabella separata in un database, a cui si accede seguendo regole rigide incorporate nella piattaforma (come in 1C). Per il processore si tratta semplicemente di una serie di letture e scritture su indirizzi diversi.
La “logica di business splendidamente astratta, incapsulata e distribuita” è il punto in cui il computer è particolarmente soddisfatto. Per lui è solo codice sparso in vari indirizzi di memoria, collegato tramite tabelle di metodi virtuali o puntatori a funzione.
E se questa logica è così splendidamente astratta che nemmeno un essere umano riesce a trovarla, per il processore significa semplicemente meno lavoro: resterà tranquillamente inattivo, in attesa della prossima istruzione.
Ridotto alla sua essenza, l’intero lavoro del processore è una ripetizione infinita dello stesso insieme di azioni primitive:
FETCH (esempio in assembly: ldr x0, [PC, #offset])
Legge un numero (istruzione o dato) dalla memoria o dalla cache.
DECODE (decodifica, eseguita direttamente dall’hardware)
Determina cosa significa l’istruzione, usando la logica interna e l’ALU (unità aritmetico-logica).
EXECUTE (esecuzione, ad esempio: add x1, x2, x3)
Somma, sottrae, moltiplica, confronta o sposta bit.
WRITE-BACK (scrittura del risultato, ad esempio: str x1, [x4, #offset])
Salva il risultato in un registro o in memoria.
Infine, può eseguire un salto condizionato o incondizionato (branch), ad esempio b label_name, verso un altro indirizzo di memoria in base al risultato ottenuto.
E questo è tutto.
Il ciclo Fetch-Decode-Execute è l’alfa e l’omega dell’esistenza del processore, una “calcolatrice” estremamente motivata ma molto limitata: esegue operazioni primitive a una velocità di miliardi al secondo, creando l’illusione di essere “intelligente” o almeno di “capire” ciò che sta facendo.
Ed ecco il paradosso principale: se il computer è così stupido e funziona solo con zeri, uni e salti, perché noi sviluppatori non scriviamo programmi in un linguaggio “leggi-confronta-salta”, ma utilizziamo Python, Java, C#, o persino 1C con i suoi “documenti” e “cataloghi”?
La risposta si trova nel regno della grande pigrizia — e genialità — umana. Per evitare di comunicare direttamente con il processore nel suo linguaggio primitivo, l’assembler (che è solo una forma leggibile del codice macchina), abbiamo inventato astrazioni e intermediari.
Immagina di dover dare istruzioni a questo lavoratore velocissimo ma ingenuo. Potresti dirgli ogni mattina: “Pippo, vai alla scrivania (indirizzo 0x…), prendi il foglio A, portalo alla scrivania B, confrontalo con il foglio B; se sono uguali, vai all’armadietto…”. È noioso e frustrante.
Invece, si creano livelli di controllo. Tu scrivi istruzioni in un linguaggio comprensibile: “Pippo, confronta la ricevuta della merce in arrivo con i saldi del magazzino.” Il tuo vice responsabile (il compilatore o l’interprete) prende queste istruzioni e le traduce in un elenco dettagliato ma adatto a Pippo: “1. Vai alla cartella ‘Documenti’. 2. Prendi il foglio superiore…” E Pippo (il processore) esegue questo elenco in modo rapido e meccanico, senza alcuna comprensione del significato complessivo dell’operazione.
Lo stesso accade nella programmazione. I linguaggi di alto livello (Python, 1C, JavaScript e così via) sono il nostro modo di parlare di “documenti” e “logica”. Compilatori, interpreti e macchine virtuali (come quelle di 1C o Java) sono i traduttori che trasformano i nostri desideri di alto livello in miliardi di istruzioni primitive, stupide ma incredibilmente veloci, per il processore.
L’ironia è che più un linguaggio è semplice e chiaro per noi, più lavoro devono fare questi intermediari per convincere l’hardware — stupido ma velocissimo — a fare ciò che vogliamo. Questo è il fondamento di tutta la programmazione: costruiamo castelli in aria usando astrazioni che, alla fine, devono essere realizzate con miliardi di mattoni, salti e fili.
Comprendere questo divario tra il nostro modo di pensare e quello della macchina è il primo passo per costruire questi castelli non solo belli, ma anche affidabili.
Come Python diventa “qualcosa che puoi fare”
Chiunque abbia appena iniziato a imparare Python avrà incontrato la frase: “Python è un linguaggio di programmazione interpretato” in qualche corso “Python in 24 ore”. Di solito uno sviluppatore alle prime armi annuisce, ignora la frase e passa subito a scrivere:
print("Hello, World!")
Ma dietro queste parole apparentemente noiose si nasconde un’intera fabbrica che trasforma il tuo splendido, quasi pseudo-codice, in azioni reali sul processore. Vediamo cosa succede dietro le quinte.
Fase 1: dal codice ai “pensieri” dell’interprete (AST)
Hai scritto una funzione semplice ed elegante in un file python:
def add(a, b): return a + bprint(add(2, 3))
Per te, questo file contiene una logica. Per il sistema operativo, invece, è solo un file di testo con estensione .py. Cosa succede quindi quando premi Invio nel terminale ed esegui python script.py? La prima cosa che accade è che il parser legge il testo, un po’ come un insegnante che corregge un tema, e ne controlla la grammatica. Non valuta ancora il significato del codice, ma verifica che due punti, parentesi e parole chiave siano al posto giusto. Se tutto è corretto, Python costruisce una struttura chiamata Albero Sintattico Astratto (Abstract Syntax Tree, AST).
Puoi immaginare un AST come uno schema strutturato delle istruzioni, in cui:
- la radice dell’albero rappresenta l’intero modulo;
- i rami sono le istruzioni (
def,print); - le foglie sono gli elementi concreti: nomi (
add,a,b), letterali (2,3), operatori (+).
Un AST non è più testo, ma non è ancora qualcosa che il computer possa eseguire direttamente. È una rappresentazione interna, strutturata e comoda da analizzare. A questo punto Python è già in grado di individuare errori di sintassi evidenti, ma non può ancora eseguire il programma.
Fase 2: compilazione… in codice non macchina (ciao, bytecode!)
Qui molti rimangono sorpresi. Nonostante venga definito “interpretato”, Python compila il codice. Ma non lo compila in istruzioni macchina per processori Intel o AMD. Lo compila in un linguaggio intermedio speciale: il bytecode Python.
Questo è un punto fondamentale. Il bytecode NON è codice macchina. È un insieme di istruzioni destinate alla Python Virtual Machine (PVM). Puoi vederla così:
- Il codice macchina è il linguaggio nativo del processore: velocissimo, difficile da usare e specifico per ogni architettura (x86, ARM, ecc.).
- Il bytecode Python è il linguaggio interno universale di CPython: una sorta di “esperanto” per i programmi Python, indipendente dall’hardware sottostante.
Il compilatore Python prende l’AST e lo trasforma in una sequenza di istruzioni byte. Per la funzione add, il bytecode potrebbe apparire così (ad esempio usando il modulo dis):
LOAD_FAST 0 (a) # Carica la variabile 'a' nello stack LOAD_FAST 1 (b) # Carica la variabile 'b' nello stack BINARY_ADD # Somma i due valori in cima allo stack RETURN_VALUE # Restituisce il risultato
Ogni comando (LOAD_FAST, BINARY_ADD, ecc.) è rappresentato da uno o più byte, da cui il nome bytecode.
Perché tutta questa complessità?
È proprio il bytecode a conferire a Python alcuni dei suoi superpoteri principali:
Multipiattaforma
Lo stesso file .pyc (bytecode compilato) può essere eseguito su Windows, Linux e macOS. È la macchina virtuale Python installata su ciascun sistema a gestire le differenze di processore, non il programma.
(Alcune) ottimizzazioni
La compilazione in bytecode permette di analizzare il codice una sola volta e applicare piccole ottimizzazioni, come il pre-calcolo delle costanti.
Velocità (relativa)
Interpretare bytecode è comunque più veloce che analizzare e fare parsing del testo sorgente a ogni esecuzione.
Sì, a volte sembra davvero “un uomo che legge un libro su come leggere un libro”. Ma funziona. Ed è sorprendentemente comodo.
Tutto il “dolore” di Python
Perché allora Python a volte è veloce?
Perché:
- molte operazioni interne sono implementate in C (ad esempio somme,
list.append, gran parte di NumPy); - esistono soluzioni JIT (come PyPy) e ottimizzazioni di compilazione avanzate (ad esempio PGO);
- nel calcolo numerico e nel machine learning, il lavoro pesante non viene svolto da Python, ma da librerie scritte in C, C++ o Fortran.
A questo punto, il tuo programma ha completato la trasformazione:
Testo (.py) → Albero Sintattico (AST) → Bytecode (.pyc)
È pronto per l’esecuzione. Ma chi esegue davvero questi comandi byte, e come? Lo vedremo nel prossimo passaggio, quando entra in scena l’“esecutore” principale: la macchina virtuale, il nostro laborioso — e un po’ lento — intermediario tra codice elegante e hardware stupido, ma velocissimo.
Prendiamo un’idea semplice che chiunque può capire:
“Se il numero è maggiore di dieci, visualizza un messaggio.”
Questa idea può essere espressa in centinaia di linguaggi di programmazione. Ma ciò che accade dopo è un viaggio straordinario, in cui la stessa destinazione finale viene raggiunta attraverso strade completamente diverse, guidate da intermediari differenti.
Codice sorgente: la prospettiva umana
Python (una persona scrive come preferisce):
x = 12
if x > 10:
print("maggiore")
Per noi esseri umani, la differenza tra linguaggi sta quasi solo nella sintassi: due punti e rientri contro punto e virgola, parole chiave in inglese o in russo. La logica è identica. Per il computer, invece, si tratta di specifiche tecniche completamente diverse, scritte in dialetti differenti.
Cosa “vede” davvero il processore: la dura realtà dell’hardware
Il processore non conosce né if, né print, né alcun altra parola ad alto livello. Il suo mondo è fatto di registri, celle di memoria e salti condizionali. Alla fine di tutte le trasformazioni, riceve ed esegue una sequenza di istruzioni primitive che, concettualmente, assomiglia a questa:
- MOV (caricamento): inserisce in un registro del processore il valore preso da una posizione di memoria (dove si trova
x). - CMP (confronto): confronta il valore nel registro con il numero 10.
- JLE (salto condizionale): se il risultato del confronto è “minore o uguale” (cioè non maggiore), allora salta all’indirizzo successivo al blocco di output; altrimenti, continua l’esecuzione.
- CALL: richiama una subroutine situata a un indirizzo di memoria specifico, incaricata di visualizzare una stringa sullo schermo o di registrarla. Prima della chiamata, l’indirizzo della stringa
"maggiore"deve essere preparato in memoria o in un registro. - Prosegue con le istruzioni successive.
Questo è tutto ciò che riguarda il lavoro “intellettuale” del processore. È il suo linguaggio universale, indipendente dal fatto che si tratti di Intel, AMD o Apple.
Scegliere un linguaggio significa, in larga misura, scegliere un ecosistema e una guida che accompagnino il codice dai concetti di alto livello fino ai comandi di basso livello. Comprendere la profondità di questo percorso aiuta a smettere di chiedersi perché la stessa operazione, in ambienti diversi, possa avere prestazioni e costi di debug completamente differenti.
Assembler in codice binario
Diciamo spesso: “tutto si riduce a zero e uno”. Ma cosa significa davvero? Non si tratta di un codice segreto o di un semplice “formato di archiviazione dei dati”. È un principio fisico fondamentale dell’informatica. Indaghiamo.
Indizio n. 1: tutto è numeri. Anche i comandi.
La prima rivelazione fondamentale è questa: qualsiasi comando eseguito dal processore è un numero. Il processore non legge lettere. Legge numeri che arrivano fisicamente ai suoi pin di ingresso. Un’istruzione del processore può essere vista come una parola macchina: uno schema di bit con una struttura rigidamente definita. In genere è composta da:
-
Bit di ordine superiore (opcode)
Il codice dell’operazione. Risponde alla domanda: “Cosa devo fare?”
(ad esempio:1101= somma,1000= confronta,0110= salta se la condizione è vera). -
Bit centrali (operandi)
Rispondono alla domanda: “Su cosa devo operare?”
(registri o indirizzi di memoria, ad esempio001= registro AX,010= registro BX). -
Bit di ordine inferiore (immediati / indirizzi)
Specificano “dove” o “con quale valore” operare
(per esempio+64o-128come offset per un salto relativo).
Così, la tua costruzione di alto livello:
if (x > 10)
dopo tutte le trasformazioni diventa qualcosa di simile a questa razione numerica per il processore:
[CMP] [REGISTRO_X] [VALORE_10] // Confronta [JG] [OFFSET_BLOCCO_DI_OUTPUT] // Salta se maggiore (Jump if Greater
Ogni elemento tra parentesi quadre è, in realtà, un numero in formato binario. Il processore riceve un flusso di questi numeri ed esegue semplicemente la logica integrata nella sua microarchitettura:
“Opcode CMP: attiva l’ALU per il confronto.” “Opcode JG: controlla i flag e, se necessario, modifica il contatore di programma.”
Il tuo if, in codice macchina, è letteralmente un comando numerico “salta o non saltare”.
Indizio n. 2: la natura fisica del binario. Perché non la trinità?
Perché proprio 0 e 1? Perché non 0, 1 e 2? La risposta non è filosofica, ma ingegneristica. I componenti elettronici fondamentali (i transistor) possono mantenere in modo affidabile due soli stati stabili e facilmente distinguibili:
- “0” logico: bassa tensione (~0 V). “Interruttore aperto”, nessuna corrente significativa.
- “1” logico: alta tensione (ad esempio +3,3 V). “Interruttore chiuso”, la corrente scorre.
Un terzo stato intermedio (“mezzo acceso”) sarebbe instabile, rumoroso e lento. Lavorare con esso a miliardi di commutazioni al secondo sarebbe un incubo ingegneristico. Il sistema binario rappresenta quindi un compromesso ideale:
- Robustezza. È facile distinguere tra “segnale presente” e “segnale assente”, anche in presenza di disturbi.
- Semplicità. Le porte logiche fondamentali (AND, OR, NOT) si costruiscono con pochi transistor.
- Scalabilità. Combinando questi elementi semplici si può creare una logica arbitrariamente complessa. Un singolo transistor è un pessimo computer. Miliardi di transistor organizzati correttamente formano un processore moderno.
Riassunto dell’indagine: l’astrazione come una torta a strati
L’intera catena di trasformazioni assomiglia a una matrioska o a una torta a più strati:
- Livello fisico. Transistor, corrente, tensione → stati stabili → bit (0/1).
- Livello logico. Sequenze di bit interpretate come numeri (dati e codici macchina).
- Livello architetturale. I numeri (codici macchina) causano azioni elementari: addizione, salto, scrittura.
- Livello di runtime. Le macchine virtuali (Python, 1C) traducono il loro bytecode in sequenze di codice macchina.
- Livello del linguaggio. Il compilatore o l’interprete trasforma il tuo
ifin istruzioni per la macchina virtuale. - Il tuo codice (codice sorgente). Tu scrivi semplicemente: if (x > 10)
“Ciao, mondo!” non è affatto un comando singolo
A un principiante sembra che accada questo:
print("Hello") → output sullo schermo: Hello
In realtà, stai avviando una mini-serie:
- chiamata a
print - conversione dell’oggetto in stringa
- codifica della stringa in byte (UTF-8)
- invio a
stdout - il sistema operativo decide dove scrivere (terminale, file, pipe)
- il driver del terminale disegna i simboli
- il sistema video aggiorna l’immagine
- il monitor illumina i pixel
Volevi un semplice “ciao”. Quello che hai ottenuto è una catena di sistemi operativi, driver e hardware che finge di essere “solo un output”.
Conclusione
- Il computer comprende solo istruzioni macchina, che in memoria appaiono come zeri e uno.
- Python, 1C o persino Emojicode non interessano direttamente il processore: vengono eseguiti da livelli intermedi (interpreti, piattaforme, macchine virtuali).
- L’intero mondo della programmazione è fatto di strati di astrazione che ci evitano di scrivere “vai all’indirizzo 0x…” al posto di
for.
Se il tuo codice funziona, significa che milioni di piccoli meccanismi hanno comunicato correttamente. Se non funziona, significa che qualcuno nella catena ha deciso: “Oggi non sono disponibile.” Oppure che il tuo codice non ha alcun senso. Sta a te stabilire quale delle due.



