Perchè è così difficile se non impossibile risalire da un programma compilato al suo sorgente se non volontariamente divulgato?

Questo dipende dal fatto che le istruzioni “comprensibili” per la CPU
del computer sono estremamente elementari: sono in generale costituite da
un “codice operativo” e da un numero variabile di operandi che in generale
sono gli indirizzi nella memoria in cui si trovano i valori degli operandi.
Tali istruzioni costituiscono il cosiddetto “linguaggio macchina”.

La difficoltà di comprensione di un tale
programma consiste nel fatto che anche una operazione logicamente
semplice (ad esempio: trovare in una directory un file di dato nome)
viene effettivamente implementata utilizzando
un grande numero di operazioni elementari.

Proprio per consentire la scrittura di programmi complessi sono stati sviluppati
i “linguaggi ad alto livello”, ovvero linguaggi le cui istruzioni
indicano operazioni assai complesse in modo sintetico, usando parole o frasi
abbastanza vicine al linguaggio naturale in modo che siano facilmente
comprensibili. Ad esempio un linguaggio ad alto livello identifica i “dati”
di un problema mediante nomi simbolici che hanno un evidente significato,
mentre nessun significato immediato avrebbe l’indirizzo della locazione di
memoria che contiene tale valore.
Una volta che il programma scritto nel linguaggio ad alto livello
è stato compilato (ovvero tradotto nel linguaggio macchina)
si perde ogni relazione fra la rappresentazione simbolica ed il corrispondente
programma ed è quindi di fatto impossibile risalire dal codice
macchina al programma simbolico che lo ha generato.

Pensiamo ad un semplicissimo esempio: supponiamo di avere un
programma che verifica se in una directory esiste un file
di nome pippo.exe. Una delle informazioni da cercare nel codice macchina,
volendo comprenderne il
funzionamento, sarebbe la stringa che rappresenta il nome del file.
Sappiamo che tale stringa deve comparire nella sua
rappresentazione in codice ASCII come
segue:

01110000011010010111000001110000
01101111001011100110010101111000
01100101000000000010001001110100

È evidente la difficoltà di interpretare la sequenza di bit
come rappresentativa di una stringa di caratteri.

Se a questo si aggiungono numerosi altri dettagli (come ad esempio
il fatto che una stessa locazione di memoria può essere utilizzata
per scopi diversi nel corso dello stesso programma)
è facile rendersi conto che,
anche se in linea teorica il codice macchina ha
esattamente lo stesso significato del programma simbolico che lo ha generato,
il comprendere questo significato (ovvero l’azione prodotta dall’esecuzione del programma)
risulta estremamente complesso, se non impossibile.

Ciò detto, occorre però aggiungere che l’operazione
non è del tutto impossibile. Tenendo conto del fatto che
per comprendere il funzionamento di un programma non è indispensabile
risalire esattamente al codice sorgente che lo ha generato, e sfruttando
il fatto che i compilatori tendono a generare particolari
sequenze di istruzioni nella traduzione del linguaggio sorgente, è
possibile generare dal codice macchina una rappresentazione a livello
più alto, anche se non identica all’originale,
che può essere di più facile comprensione.

Tecniche di questo tipo sono comunemente utilizzate, ad esempio,
nell’analisi dei
cosiddetti virus per capirne le caratteristiche e trovare gli “antidoti”.

Per approfondimenti su argomenti correlati, si possono consultare le
seguenti risposte: