Imparare l’Assembly con Visual C++

Durante il proprio corso di studi universitario si avrà sicuramente a che fare con l’Assembly o linguaggio macchina in corsi come Fondamenti di Informatica, Calcolatori Elettronici o Architettura degli Elaboratori.

Prima di addentrarci nel mondo dell’Assembly vediamo però cosa non sempre viene detto nei corsi universitari:

A cosa può servire oggi?

-Comprensione più profonda del funzionamento dei calcolatori e dei linguaggi di programmazione
-Scrittura di driver o parti di sistemi operativi
-Ottimizzazione di parti critiche dal punto di vista delle performance di programmi
-Analisi di Dump File

La lista non è completa ma questi sono i principali motivi che spingono ad imparare l’Assembly.

Dal punto di vista accademico la cosa più importante è naturalmente la comprensione del funzionamento dei microprocessori e dei linguaggi di programmazione ed è qui che ci focalizzeremo in questo articolo.

Dal punto di vista reale la scrittura di driver o parti di sistemi operativi è rilegata a piccole nicchie di programmatori mentre l’ottimizzazione di parti critiche di programmi è soprattutto diffusa in applicazioni scientifiche e nel mondo videoludico. E’ necessario sottolineare che solamente in rari casi (ad esempio sostituendo codice C/C++ con istruzioni Assembly SSE4 che il compilatore non è in grado di generare) e in piccole parti di programmi viene utilizzato l’Assembly in quanto difficilmente si migliorano significativamente le prestazioni di un applicativo. Spesso si ottiene proprio l’effetto contrario a causa delle ormai molto complesse pipeline superscalari dei nuovi processori.

Infine l’analisi di Dump File ovvero l’analisi di file contenenti l’immagine della memoria al momento di un crash (causato per esempio da un bug dell’applicazione) è molto rilevante nel supporto tecnico di livello avanzato.

Task Manager create dump
Il Task Manager di Windows così come le eccezioni non gestite permettono di creare Dump File dei processi in esecuzione per poter risolvere i bug più ardui, sono richieste però conoscenze dell’Assembly nella maggior parte dei casi per poterli esaminare proficuamente.

Assembly, Assembler e codice macchina

Chiariamo cos’è l’Assembly, l’Assembler e il codice macchina, spesso confusi anche dai professori più prestigiosi:

- l’Assembly è il linguaggio di programmazione più vicino al linguaggio macchina vero e proprio.

MOV EAX, 10
MOV EBX, 20
ADD EAX, EBX
Esempio di listato Assembly

La lista di istruzioni Assembly viene costantemente ampliata da Intel, Amd e dagli altri produttori di processori per permettere di sfruttare al massimo ogni nuovo processore.
I manuali ufficiali per i processori Intel sono scaricabili presso http://developer.intel.com/products/processor/manuals/index.htm mentre per i processori Amd è possibile scaricare i manuali da http://developer.amd.com/DOCUMENTATION/GUIDES/Pages/default.aspx#manuals.

- l’Assembler è il programma che converte il linguaggio assembly in codice macchina, per certi versi simile al compilatore. Famosi Assembler sono MASM e TASM. Nel corso di questo articolo ci si avvarrà di Visual C++ e delle sue capacità di mixare codice C/C++ e Assembly.

- il codice macchina o linguaggio macchina è l’insieme di 1 e 0, quasi sempre rappresentati secondo la rappresentazione esadecimale per una migliore leggibilità, che il processore è capace di eseguire. E’ in genere prodotto dall’Assembler.

B8 0A 00 00 00
BB 14 00 00 00
03 C3
Codice macchina prodotto da MASM per il listato Assembly del precedente esempio

Come imparare l’Assembly

Se si hanno le basi di C/C++ il modo migliore per perfezionare le proprie conoscenze ed imparare senza perdere tempo l’Assembly è provare direttamente nei propri programmi le istruzioni Assembly anziché imparare nuovi strumenti e creare da zero applicazioni completamente in Assembly.

Dal C/C++ all’Assembly

Vediamo in questo articolo come è possibile visionare il codice Assembly e il codice macchina corrispondente al proprio programma C/C++.

Creiamo un nuovo progetto vuoto (marcando la casella Empty Project durante il wizard) chiamato CppAssembly di tipo Win32 console application e aggiungiamo ora al progetto un file chiamato CppAssembly.cpp contenente il seguente codice:

// In questo esempio viene utilizzato printf
// invece che cout per una maggiore semplicit
// del listato Assembly corrispondente
#include <stdio.h>

void main()
{
    int x = 5;
    int y = 6;
    printf("%d",x+y);
    return;
}

Impostiamo in modalità Release il nostro progetto e disabilitiamo le ottimizzazioni andando sul menù Project, scegliendo Properties/Configuration Properties/C/C++/Optimization e impostando la proprietà Optimization a Disabled (/Od) per evitare di includere nel programma compilato istruzioni per ottimizzare le prestazioni ma di intralcio per la lettura e comprensione del listato Assembly.

Optimization

Impostiamo adesso un breakpoint sulla prima istruzione (int x = 5;) e premiamo F5 per fermare l’esecuzione nel punto indicato.

Per visualizzare il codice Assembly del nostro programma facciamo click col tasto destro e andiamo su Go To Disassembly come nella seguente immagine:

Go to disassembly

Apparirà tra il resto il seguente codice Assembly:

Disassembly

Possiamo notare dopo ogni istruzione C/C++ (in nero) il relativo codice Assembly (in grigio) assieme al codice macchina e alla relativa posizione nel programma.

L’istruzione int x = 5 si trasforma (tralasciando la dichiarazione della variabile x presente in un altro punto non mostrato nell’immagine) nel linguaggio Assembly in mov dword ptr [x], 5 che, semplificando, altro non fa che copiare il valore 5 nell’indirizzo di memoria puntato da x.

Il numero in esadecimale 01351006 specifica la posizione iniziale dei byte del codice macchina nel file binario (file .exe) ove il programma è contenuto.

Il codice macchina di questa istruzione è C7 45 F8 05 00 00 00 ed è teoricamente possibile ma del tutto inutile se non in casi molto particolari scrivere manualmente tutto il programma in codice macchina.

Per esempio per codificare manualmente questa istruzione dovremmo andare a pagina 689 dell’Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2A: Instruction Set Reference, A-M e cercare il codice relativo all’istruzione MOV tra un registro e un valore immediato.

MOV istr
Codice relativo all’istruzione MOV riportato dal manuale Intel

Oltre a tale codice dovremmo aggiungere il codice del puntatore al registro (definito anch’esso tra i manuali) e il valore immediato. Tutto questo è una fatica spesso inutile dato che la corrispondenza tra codice macchina e linguaggio Assembly è uno ad uno (1:1) quindi non ottimizzabile manualmente, ed è questo il motivo principale per cui sono nati gli Assembler.

Concludendo Visual C++ offre tra i suoi potenti strumenti di Debug la visualizzazione dell’Assembly che si dimostra molto utile nello studio del linguaggio e nella sua comprensione grazie a conoscenze già consolidate di C/C++. Torneremo nel prossimo articolo sulle potenzialità di Visual C++ e vedremo come analizzare i registri della CPU e la memoria.

6 Responses to “Imparare l’Assembly con Visual C++”

  • Student:

    Molto interessante! Non vedo l’ora che esca il seguito!

  • Ariosto:

    E’ possibile anche visualizzare il codice Assembly di programmi senza sorgente con Visual C++?

  • Leonardo:

    Certo, è possibile debuggare qualsiasi programma (fatta eccezione per i driver) in esecuzione sul proprio computer. Per analizzare programmi complessi senza codice sorgente però ti consiglio di guardare OllyDbg, un potente programma che offre ulteriori funzionalità proprio per comprendere codice senza sorgente.

  • JoeBennet:

    ciao ti volevo chiedere un piccolo favore… devo Realizzare un programma in assembly che permetta di crittografare e decrittografare una stringa inserita dall’utente.
    Per crittografare la stringa si utilizza una parola chiave e si procede sommando ogni carattere della stringa con il corrispondente carattere della chiave; al raggiungimento dell’ultimo carattere della chiave, si ricomincia dal primo carattere della chiave stessa fino al raggiungimento della lunghezza della stringa.
    Però onestamente non so da dove cominciare… una mano potresti darmela?

  • Leonardo:

    Ciao Joe, se non sai da dove partire la prima cosa da fare è scrivere il programma in C/C++ (come quasi sempre si fa quando si programma in Assembly al giorno d’oggi). Ho abozzato il programma che devi fare in due funzioni, Crypt e Decrypt che non ritornano nessun valore (sono funzioni void) ma modificano l’array di caratteri passato come ultimo argomento. Ecco il codice:

    #include
    using namespace std;
    void Crypt(char* clearMsg, char* pwd, char* cryptedMsg)
    {
    int msgIterator = 0; // Serve per tenere traccia del carattere del messaggio che stiamo processando
    int pwdIterator = 0; // Serve per tenere traccia del carattere della password che abbiamo usato
    while(clearMsg[msgIterator]!=0) // Cicla tutto il messaggio in chiaro
    {
    if(pwd[pwdIterator]==0)
    {
    pwdIterator=0; // Se la password è finita riutilizza il primo carattere
    }
    cryptedMsg[msgIterator]=clearMsg[msgIterator] + pwd[pwdIterator];
    msgIterator++;
    pwdIterator++;
    }
    cryptedMsg[msgIterator]=0; // termina il messaggio criptato
    }
    void Decrypt(char* cryptedMsg, char* pwd, char* decryptedMsg)
    {
    int msgIterator = 0; // Serve per tenere traccia del carattere del messaggio che stiamo processando
    int pwdIterator = 0; // Serve per tenere traccia del carattere della password che abbiamo usato
    while(cryptedMsg[msgIterator]!=0) // Cicla tutto il messaggio criptato
    {
    if(pwd[pwdIterator]==0)
    {
    pwdIterator=0; // Se la password è finita riutilizza il primo carattere
    }
    decryptedMsg[msgIterator]=cryptedMsg[msgIterator] – pwd[pwdIterator];
    msgIterator++;
    pwdIterator++;
    }
    decryptedMsg[msgIterator]=0; // termina il messaggio in chiaro
    }
    void main()
    {
    char clearMsg[]=”testo in chiaro”;
    char password[]=”pwd”;
    char cryptedMsg[255];
    char decryptedMsg[255];
    Crypt(clearMsg,password,cryptedMsg); // Cripta il messaggio
    cout << "Messaggio Criptato: " << cryptedMsg << endl;
    Decrypt(cryptedMsg,password,decryptedMsg); // Decripta il messaggio
    cout << "Messaggio Decriptato: " << decryptedMsg << endl;
    return;
    }

    Una volta che hai il programma in C/C++ funzionante bisogna iniziare a riscrivere mano a mano le routine in Assembly. Se hai letto con attenzione questo articolo avrai già capito che si può partire dal codice Assembly generato dal compilatore C++ e pulirlo, migliorarlo o modificarlo a piacere secondo le proprie necessità.
    Ad esempio la routine Crypt codificata dal compilatore in Assembly diventa:

    PUBLIC ?Crypt@@YAXPAD00@Z ; Crypt
    ; Function compile flags: /Odtp /RTCu
    ; COMDAT ?Crypt@@YAXPAD00@Z
    _TEXT SEGMENT
    _msgIterator$ = -8 ; size = 4
    _pwdIterator$ = -4 ; size = 4
    _clearMsg$ = 8 ; size = 4
    _pwd$ = 12 ; size = 4
    _cryptedMsg$ = 16 ; size = 4
    ?Crypt@@YAXPAD00@Z PROC ; Crypt, COMDAT
    ; {
    push ebp
    mov ebp, esp
    sub esp, 8
    ; int msgIterator = 0; // Serve per tenere traccia del carattere del messaggio che stiamo processando
    mov DWORD PTR _msgIterator$[ebp], 0
    ; int pwdIterator = 0; // Serve per tenere traccia del carattere della password che abbiamo usato
    mov DWORD PTR _pwdIterator$[ebp], 0
    $LN3@Crypt:
    ; while(clearMsg[msgIterator]!=0) // Cicla tutto il messaggio in chiaro
    mov eax, DWORD PTR _clearMsg$[ebp]
    add eax, DWORD PTR _msgIterator$[ebp]
    movsx ecx, BYTE PTR [eax]
    test ecx, ecx
    je SHORT $LN2@Crypt
    ; {
    ; if(pwd[pwdIterator]==0)
    mov edx, DWORD PTR _pwd$[ebp]
    add edx, DWORD PTR _pwdIterator$[ebp]
    movsx eax, BYTE PTR [edx]
    test eax, eax
    jne SHORT $LN1@Crypt
    ; {
    ; pwdIterator=0; // Se la password è finita riutilizza il primo carattere
    mov DWORD PTR _pwdIterator$[ebp], 0
    $LN1@Crypt:
    ; }
    ; cryptedMsg[msgIterator]=clearMsg[msgIterator] + pwd[pwdIterator];
    mov ecx, DWORD PTR _clearMsg$[ebp]
    add ecx, DWORD PTR _msgIterator$[ebp]
    movsx edx, BYTE PTR [ecx]
    mov eax, DWORD PTR _pwd$[ebp]
    add eax, DWORD PTR _pwdIterator$[ebp]
    movsx ecx, BYTE PTR [eax]
    add edx, ecx
    mov eax, DWORD PTR _cryptedMsg$[ebp]
    add eax, DWORD PTR _msgIterator$[ebp]
    mov BYTE PTR [eax], dl
    ; msgIterator++;
    mov ecx, DWORD PTR _msgIterator$[ebp]
    add ecx, 1
    mov DWORD PTR _msgIterator$[ebp], ecx
    ; pwdIterator++;
    mov edx, DWORD PTR _pwdIterator$[ebp]
    add edx, 1
    mov DWORD PTR _pwdIterator$[ebp], edx
    ; }
    jmp SHORT $LN3@Crypt
    $LN2@Crypt:
    ; cryptedMsg[msgIterator]=0; // termina il messaggio criptato
    mov eax, DWORD PTR _cryptedMsg$[ebp]
    add eax, DWORD PTR _msgIterator$[ebp]
    mov BYTE PTR [eax], 0
    ; }
    mov esp, ebp
    pop ebp
    ret 0
    ?Crypt@@YAXPAD00@Z ENDP ; Crypt

    il codice Assembly generato è facilmente comprensibile visto che hai come commento il codice C/C++ del programma e sai già quello che fa. Per concludere bisogna adattare il codice Assembly ottenuto in questo modo in base al proprio assemblatore ad es. MASM o TASM per renderlo compilabile.

  • Pippo:

    Una guida davvero come non ce ne sono altre!

Leave a Reply