Tutti i linguaggi di programmazione che utilizziamo ogni giorno sono “ad alto livello”: significa che essi astraggono i dettagli del funzionamento di un elaboratore informatico e sono lontani dalle caratteristiche peculiari del “linguaggio macchina”. Il Fortran fu il linguaggio che per primo, negli anni ’50, permise di tradurre logiche più vicine al modo di pensare umano in quelle proprie del linguaggio macchina.
Il linguaggio macchina è basato su un alfabeto detto binario perché consente l’utilizzo di due soli simboli, generalmente indicati con 0 e 1. Tale linguaggio definisce poi le istruzioni elementari, codificate in forma numerica, che un elaboratore – o meglio, il processore – è capace di svolgere (si sente spesso parlare dei vari instruction set che una CPU può gestire, che possono essere più o meno ampi e focalizzati su determinate tipologie di operazioni).
Il codice binario è quindi utilizzato al livello più basso da tutti i sistemi informatici adoperati fino ad oggi perché sono previsti due soli stati (0 e 1, acceso/spento; le cose stanno radicalmente cambiando con l’avvento dei computer quantistici: Google ha conquistato la supremazia quantistica: cosa significa), perché la gestione di due soli numeri (0 e 1) è conveniente, semplice, efficace ed affidabile e perché tutte le operazioni e le attività che possono essere svolte usando il sistema decimale possono essere portate a termine anche con un sistema numerico binario.
Si prenda in considerazione il numero binario 00100000: partendo da destra si moltiplichi ciascuna cifra per le potenze del 2 in ordine crescente. Contare da destra quindi e elevare a potenza a seconda della posizione: in questo caso 0*20+0*21+0*22+0*23+0*24+1*25+0*26+0*27 = 32
.
Quindi 00100000 equivale a 32 nel sistema decimale mentre, per esempio, 00101011 equivale a 43.
La procedura contraria, per passare dal sistema decimale al codice binario, consiste nel dividere il numero di partenza per due annotando 1 se il risultato desse resto (altrimenti 0).
Al termine delle varie divisioni, basterà leggere quanto scritto in precedenza dal basso verso l’altro per ottenere il codice binario.
Provate a digitare calc
nella casella di ricerca di Windows, a eseguire la calcolatrice quindi a scegliere la versione Programmatore: esaminando quanto appare accanto a HEX, DEC, OCT e BIN, si potrà convertire istantaneamente un numero nelle varie notazioni esadecimale, decimale, ottale e, infine, binario.
Il bit (acronimo di binary digit) è appunto una cifra binaria (0 o 1) e rappresenta l’unità di misura della quantità d’informazione. Sequenze di bit permettono al processore di eseguire le istruzioni e i comandi operativi (linguaggio macchina).
Il byte è semplicemente una sequenza formata da 8 bit che può assumere al massimo 256 valori (28) ovvero dal binario 0 a 11111111. Il byte può essere pensato come l’unità di informazione necessaria per codificare un singolo carattere. Non per niente la tabella extended ASCII usa 8 bit per codificare 256 caratteri differenti (si pensi alla combinazioni di tasti richiamabili da tastiera tenendo premuto ALT
più un codice numerico): vedere, a tal proposito, questa tabella ASCII.
Parlando di reti di telecomunicazioni: Differenza tra Megabit e Megabyte: come non cadere in errore.
Va comunque tenuto presente che non esiste e non può esistere un unico linguaggio macchina perché differenti sono le architetture hardware utilizzate sui computer e sulla vastissima schiera di dispositivi con i quali si ha quotidianamente a che fare oggi.
Un codice macchina sviluppato per l’architettura x86 non può essere direttamente utilizzato, per esempio, sulla piattaforma ARM: Differenza tra processori ARM e x86.
Per programmare in linguaggio macchina è necessario conoscere aspetti legati al funzionamento a basso livello del processore che rendono difficilmente praticabile questo tipo di attività.
Ad un livello un po’ più alto rispetto al linguaggio macchina, si pone il linguaggio assembly. Assembly è comunque un linguaggio di programmazione a basso livello che risulta comunque poco gestibile e difficilmente comprensibile.
Mentre il codice macchina è formato da sequenze di bit, Assembly è comunque composto da istruzioni mnemoniche.
In Assembly usano istruzioni come MOV, PUSH, POP
come istruzioni di trasferimento, ADD, SUB, CMP, MUL, DIV
come istruzioni aritmetiche, AND, OR, XOR, NOT
come istruzioni logiche, JMP, CALL, RET, LOOP
e così via come istruzioni di controllo, HLT, WAIT
come controllo della macchina e molte altre ancora.
Si tratta di istruzioni che fanno direttamente riferimento ai registri della CPU e della memoria per accedere al contenuto delle variabili, per scambiare i contenuti dei vari operandi, per trasferire dati, per compiere operazioni aritmetiche, per mettere a confronto valori e – in seguito ai controlli effettuati – attivare salti durante il flusso di esecuzione del programma.
Si pensi che un’istruzione ad alto livello di tipo if (a == b) c = d else b = b+1
ovvero la classica struttura di controllo if…then…else in Assembly diventa qualcosa di simile:
mov ax, a
cmp ax, b
jne Else
mov ax, d
mov c, ax
jmp EndIF
Else: inc b
EndIF: …
Agendo a basso livello, una volta individuati i salti giusti, è ad esempio possibile alterare il funzionamento di qualunque programma.
Il vantaggio derivante dai linguaggi di alto livello è che essi sono più intuitivi da usare, adottano una sintassi e uno specifico insieme di regole, il codice può essere generalmente compilato su più architetture diverse ed è quindi possibile ottenere il codice macchina senza dover riscrivere il codice ad alto livello.
Lo svantaggio è che il codice scritto con un linguaggio di alto livello non potrà essere eseguito direttamente dal processore: alcuni linguaggi usano compilatori per creare un file eseguibile a partire dal codice sorgente mentre altri linguaggi di programmazione ricorrono a dei software che fungono da interpreti e trasformano le istruzioni in codice macchina durante l’esecuzione del programma. I programmi interpretati sono ovviamente più lenti e meno efficienti rispetto a quelli compilati.
Mentre Python è un esempio di linguaggio interpretato, il linguaggio C è un esempio di linguaggio che usa la compilazione. Java è un linguaggio “semi-compilato” perché il programma Java viene dapprima compilato come bytecode quindi interpretato ed eseguito dalla speciale Java Virtual Machine (JVM), offerta nelle versioni compatibili con tutte le piattaforme hardware-software in circolazione. Diversamente rispetto a un eseguibile, lo stesso bytecode Java è quindi utilizzabile su tutte le piattaforme a patto di aver installato l’opportuna JVM.
Per Android Google ha a suo tempo messo a punto la macchina virtuale Dalvik (il cui utilizzo è stato al centro di una vertenza legale con Oracle durata anni e anni: Per i giudici Google ha violato la proprietà intellettuale Oracle per sviluppare Android) che si occupa proprio di interpretare il bytecode Java per poi sostituirla con il motore ART (Android Runtime) che compila il codice dell’applicazione sul dispositivo al momento della sua installazione quindi non più al momento dell’esecuzione stessa del software con enormi vantaggi sul piano delle prestazioni e della gestione delle risorse.
Disassemblare e decompilare programmi
Termini come “disassemblare” o “decompilare” un programma vengono spesso usati a sproposito.
Un disassembler è uno strumento che trasforma il codice macchina di un programma in un formato più facilmente leggibile usando appunto il linguaggio Assembly.
Viceversa, un decompilatore è un software che può essere utilizzato per annullare il processo di compilazione e tornare al codice sorgente ad alto livello.
In passato esistevano molteplici decompilatori per Visual Basic 6 piuttosto efficaci che consentivano di ottenere codice sorgente perfettamente funzionante e facilmente leggibile a partire da qualunque eseguibile.
Per le applicazioni Windows più moderne citiamo software come CodeReflect, JustDecompile, .NET Reflector e ILSpy.
Nel caso dei pacchetti Android APK, usare il termine “decompilare” – come abbiamo visto – non è un termine propriamente corretto: i file APK vanno più propriamente considerati come archivi; una variante del formato .JAR utilizzato in Java per distribuire raccolte di classi.
Per effettuare il reverse engineering di un’applicazione Android e studiarne il comportamento, suggeriamo quindi di seguire le istruzioni riportate nell’articolo Modificare un’app Android e alterarne il comportamento.
Un debugger, infine, è un software progettato per l’analisi e l’eliminazione dei bug introdotti nel codice di qualunque programma. Un programma come lo storico OllyDbg veniva in passato usato per tracciare i registri, riconoscere le funzioni e i parametri passati alle principali librerie standard, le chiamate alle API del sistema operativo, salti condizionali, tabelle, costanti, variabili e stringhe. Ormai non più sviluppato (l’ultima versione risale al 2013), OllyDbg può lavorare sui programmi a 32 bit mentre una release per intervenire sulle applicazioni a 64 bit non è mai stata rilasciata.
A prendere il posto di OllyDbg, x64dbg software opensource che consente di portare a livello Assembly il codice macchina delle applicazioni a 32/64 bit in esecuzione in ambiente Windows.
Di solito, per studiare il funzionamento di un programma (esattamente come si faceva a suo tempo con OllyDbg) anche con x64dbg si parte dalla ricerca di una stringa (ad esempio quanto mostrato alla comparsa di una finestra di dialogo o di un messaggio d’errore) quindi si procede con l’analisi dei salti condizionati presenti a monte (segno evidente della presenza di una struttura di controllo if…then…else).
L’impostazione di breakpoint consentirà di seguire passo-passo l’esecuzione di un programma ed esaminarne il comportamento, anche in risposta alle eventuali modifiche applicate utilizzando il debugger.
E gli editor esadecimali o hex editor?
Un editor esadecimale è un programma che utilizza una notazione esadecimale per rappresentare ogni byte con una coppia di caratteri. I byte il cui contenuto coincide con un carattere visualizzabile secondo la codifica ASCII vengono rappresentati con il carattere tipografico corrispondente.
HxD è un hex editor gratuito per Windows che permette di esaminare il contenuto di qualunque file. Scorrendo il contenuto dei file eseguibili, si potranno notare etichette e stringhe di caratteri che compaiono nell’interfaccia dell’applicazione.
Un editor esadecimale – seppur per questa specifica esigenza possa bastare anche un semplice editor di testo – permette anche di accertare la reale tipologia di un file: File danneggiato, attenzione ai formati.
Il noto Resource Hacker consente di modificare stringhe ed elementi grafici contenuti negli eseguibili in maniera molto semplice.
Da ultimo, è importante evidenziare che la modifica del comportamento di un eseguibile compilato dallo sviluppatore è un’operazione che non viene permessa. Può essere ammissibile solo per superare eventuali incompatibilità come previsto nella Direttiva 2009/24/CE del Parlamento europeo e del Consiglio, del 23 aprile 2009 , relativa alla tutela giuridica dei programmi per elaboratore (articolo 15).