Capita in qualche caso (cioè nello 0,00001% 😉 ) che, scrivendo un programma C, si debba inserire un pezzetto di codice assembly per fare delle operazioni non contemplate dal C, come usare una specifica istruzione del processore, richiamare manualmente una syscall, oppure semplicemente scrivere un pezzo di codice altamente ottimizzato. Se non volete creare un file separato apposta per questo codice, l’assembly inline è quello che fa per voi.

Sintassi

Il codice assembly che vogliamo inserire deve per default rispettare la sintassi AT&T, che è quella usata maggiormente in ambito *nix, e viene usata anche dall’assemblatore su cui si appoggia GCC. Se invece preferiamo usare la sintassi Intel, dobbiamo aggiungere “-masm=intel” alla linea di comando di GCC:

gcc -masm=intel file.c -o file

Io negli esempi userò sempre la sintassi AT&T, non perchè mi piaccia, ma giusto per tenere tutto nella configurazione di default.
Detto ciò, vediamo come è composta la sintassi di base per aggiungere assembly inline:

asm( "codice assembly" );

Molto semplice vero? 😀

Il codice assembly viene inserito in una stringa (che eventualmente può essere suddivisa in più righe); le varie istruzioni che ne compongono il codice devono essere separate da un delimitatore, che può essere il carattere newline ('\n'), oppure il punto e virgola (';'). Tipicamente insieme al newline viene anche aggiunto un carattere di tabulazione ('\t') subito dopo: questo perchè la stringa viene passata all’assemblatore così com’è, a parte l’aggiunta di parametri che vedremo dopo; usare il newline e la tabulazione permette di ottenere un listing del listato assembly generato dal compilatore (in GCC si richiede specificando lo switch -S) molto più ordinato, utile se dobbiamo analizzarlo; se invece non ci interessa vedere il codice generato, usare il punto e virgola come delimitatore permette di leggere meglio il listato C.

Questi tutti sono esempi validi di sintassi:

asm( "movl %eax, %ebx;" );    // punto e virgola come delimitatore
asm( "movl %eax, %ebx\n" );   // newline come delimitatore
asm( "movl %eax, %ebx\n\t" ); // newline + tabulazione
 
asm( "movl  $5, %eax;"         // il codice si può anche disporre su più linee
     "addl $12, %ebx;"
     "subl %eax, %ebx;"
);

Sintassi estesa

La sintassi di base è molto limitata: come faccio se voglio accedere alle variabili che ho dichiarato nel programma C ? E’ qui che entra in gioco la sintassi estesa:

asm(
    "codice assembly"
    : /* Lista registri Output */
    : /* Lista registri Input  */
    : /* Lista registri sporcati */
);

Le regole per la scrittura del codice assembly sono le stesse dette sopra, con la sola differenza che, se vogliamo riferirci ad un registro, dobbiamo porre due “%” davanti al suo nome, cioè "%%eax", oppure "%%edi"; il perchè lo vedremo fra poco.

Le tre “liste” sono la parte più interessante: ci permettono di definire dei “parametri” che possiamo usare per collegare il codice assembly al codice C. Vediamo un esempio:

int varA=2, varB;
 
asm (
    "movl %1, %%eax;"
    "movl %%eax, %0;"
    : "=r"(varB)     /* output */
    : "r"(varA)      /* input  */
    : "%eax"         /* registri sporcati */
);

In questo esempio (che serve per copiare i dati della variabile “varA” in “varB”, cioè un complicato “varB = varA”), vediamo l’uso di tutte e tre le liste. Iniziamo con quella di output:

"=r"(varB)

Questa “dichiarazione” indica a GCC che si sta definendo un nuovo parametro (di output), che scriverà i suoi dati nella variabile “varB”, e questi dati saranno a disposizione del codice assembly attraverso un registro general-purpose ("r", l’uguale “=” indica che è un registro di output, anche se questa informazione è un duplicato, bisogna metterlo).

Bene, abbiamo detto a GCC che vogliamo un parametro di output, e gli abbiamo dato tutte le informazioni che vuole; ora come facciamo ad usare questo parametro nel codice assembly? Semplice: in base all’ordine in cui li definiamo, GCC assegna ai nostri parametri un numero progressivo; poichè questo è il primo parametro che definiamo, ha numero 0, e nel codice lo indicheremo con "%0". Stesso discorso per i parametri di input: ne abbiamo uno che prende i suoi dati da “varA” e li mette in un registro (che viene scelto dal compilatore), a cui si può accedere nel codice mediante "%1".
Ora, prima di analizzare la terza lista, ricapitoliamo il codice: con il primo “movl” mettiamo in “eax” il contenuto del secondo parametro (“%1”, che corrisponde a varA), e con il secondo mettiamo nel parametro 0 (varB, di output) il contenuto di eax, completando la copia. Noi abbiamo scelto “eax” come registro temporaneo in modo del tutto arbitrario, ma come facciamo a sapere che non contenga dati che servivano al compilatore? Noi non possiamo, ma possiamo dire al compilatore che noi lo usiamo, che lo “sporchiamo” con i nostri dati. E’ a questo che serve la terza lista: gli indichiamo tutti i registri che usiamo, in modo che lui possa salvarseli, o semplicemente evitare di usarli.

Nell’esempio successivo, lo stesso codice viene scritto in un modo diverso, in modo che non serva il registro eax; in questo modo, nessun registro (oltre a quelli già usati per i dati) viene sporcato, quindi non serve che lo specifichiamo tra i registri sporcati, e addirittura possiamo omettere il “due punti” che separa la lista di input dalla lista clobbered:

int varA=2, varB;
 
asm (
    "movl %1, %0;"
    : "=r"(varB)     /* output */
    : "r"(varA)      /* input  */
);

Bene, dopo tutta questa fatica, vediamo un esempio di programma completo 😀

#include <stdio.h>
 
int main()
{
    int varA=5, varB=3;
 
    asm(
        "addl %2, %1\n"
        : "=r" (varA)
        : "0" (varA), "r" (varB)
    );
 
    printf( "%d\n", varA );
    return 0;
}

Dichiariamo 2 variabili, le inizializziamo, e poi sommiamo i due valori (riportando il risultato in varA) per poi visualizzarli. Possiamo vedere in questo codice come si può usare la stessa variabile sia come input, sia come output: prima indichiamo varA come parametro di output, specificando “=r” come modalità. Poi, quando ri-specifichiamo varA come parametro di input, invece di ri-specificare “r” come modalità, gli indichiamo il numero del parametro da cui vogliamo copiare la modalità; in questo caso, la precedente dichiarazione di varA ottiene l’indice 0, quindi specifichiamo questo numero nella modalità. C’è anche un secondo input, varB, che viene registrato nella lista di input separandolo con una virgola da varA.

Ora potreste chiedere, che indice uso per identificare il registro che è sia di input che di output? L’indice 0 (cioè quello della dichiarazione di output), o l’indice 1 (cioè quello della dichiarazione di input) ? Non ho trovato una risposta, ma il compilatore non fa differenza tra uno e l’altra, quindi si possono usare entrambi; nel codice sopra, ho usato l’indice di input.

Un’altro esempio per chiarire quando specificare i registri “sporcati”:

asm (
    "cld;"
    "rep stosl;"
    : /* nessun registro di output */
    : "c" (count), "a" (fill), "D" (dest)
    : "%ecx", "%edi"
);

Il codice sopra riempie una zona di memoria puntata da “dest” con il valore di “fill” per “count” volte. Qui vediamo che come modalità non è stato indicato un registro generico “r”, ma per count è stato indicato ecx, per “fill” eax, e per “dest” edi. Così l’istruzione stosl si ritroverà i valori corretti già nei loro registri dedicati. Ora, perchè indichiamo ecx e edi come registri sporcati? Sono già nei parametri di input, il compilatore dovrebbe conoscere già che sono usati, no? E poi, perchè non abbiamo messo anche eax?

E’ tutto molto semplice: l’istruzione “rep stosl”, durante la sua esecuzione, modifica i valori di ecx e edi, quindi alla fine dell’esecuzione, non contengono più i loro valori originali. Questo il compilatore non lo sa, e quindi glielo indichiamo noi. Eax non ha bisogno di essere specificato perchè non viene modificato.

Volatile

Se il nostro codice fa delle operazioni piuttosto critiche, è conveniente porre “volatile” subito dopo “asm”, così:

asm volatile ( ... );

In questo modo GCC non opererà nessuna ottimizzazione sul codice finale, in modo da non modificare l’ esecuzione del nostro codice originale. Se dobbiamo fare solo un po’ di calcoli, non mettere “volatile” permette a GCC di integrare il vostro codice nei suoi schemi di ottimizzazione, in modo da avere prestazioni superiori.

Altre Modalità

GCC permette di definire molte altre modalità (chiamate constraints nei testi inglesi) per i parametri, eccole elencate:

  • Registri generici (r)
    Questa modalità che abbiamo già visto, indica al compilatore di inserire il parametro in un registro ad uso generale (eax, ebx, ecx, edx, esi, edi), non importa quale sia.
  • Registri generici principali (q)
    E’ simile alla precedente modalità, solo che sceglie solo tra i registri eax, ebx, ecx e edx, tralasciando esi e edi.
  • Registro specifico (a, b, c, d, S, D)
    Con queste modalità possiamo indicare al compilatore un registro specifico in cui inserire il dato:

    Mod. Registro/i
    a %eax, %ax, %al
    b %ebx, %bx, %bl
    c %ecx, %cx, %cl
    d %edx, %dx, %dl
    S %esi, %si
    D %edi, %di
  • Memoria (m)
    Indica che l’operando si trova in memoria, e l’operazione modificherà direttamente la memoria, invece di spostare il dato prima in un registro, e poi trascrivendo il risultato. Questa modalità permette un’efficienza maggiore quando si usano le variabili C.
  • Modalità corrispondente
    Nel campo modalità viene specificato l’indice del parametro da cui prendere la modalità. Lo abbiamo visto nel programmino precedente.

Conclusioni

Con questo lungo articolo concludo la trattazione dell’ assembly inline. Ho cercato di spiegare al meglio che potevo le caratteristiche principali di questa tecnica; ci sono altre cose ancora da approfondire, come altre modalità, o tecniche particolari sulla gestione dei parametri per chi non si accontenta 😉
Ho tratto questa guida da questo sito inglese:
http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
In fondo a quella pagina potete trovare altri link molto interessanti dotati anche di svariati esempi.
Se vi serve qualche consiglio, lasciate pure un commento 😀