Appendice E.   Assembly AT&T e linguaggio c

In questa appendice vediamo come far convivere codice assembly e codice c in uno stesso programma eseguibile.

Il motivo per cui questo può essere importante è che certe funzioni possono essere scritte in assembly al fine di sfruttare in pieno le caratteristiche dell'hardware e di ottimizzare al massimo le prestazioni, mentre possono rimanere in c tutte quelle operazioni, come l'input e l'output, in cui esso è di utilizzo molto più semplice.

In parte questa convivenza l'abbiamo già sperimentata usando chiamate a funzioni del c in alcuni programmi assembly (vedi paragrafo 3.15); adesso esploriamo altre possibilità date da: assembly inline e richiamo di funzioni esterne assembly da un programma c.

E.1   L'assembly inline

In molti ambienti (e GNU/Linux è uno di questi) è possibile inserire istruzioni assembly in un programma c; si parla in tal caso di assembly inline.

La sintassi da utilizzare è la seguente:

__asm__ ( 
istruzioni assembly
: operandi di output (opzionali)
: operandi di input (opzionali)
: lista dei registri modificati (opzionale)
);

All'inizio dobbiamo scrivere asm preceduta e seguita da due underscore.

Il primo parametro, è costituito dall'insieme delle istruzioni assembly, ognuna chiusa da «;»; tali istruzioni devono essere ognuna tra virgolette a meno che non si scrivano sulla stessa riga.

Il secondo parametro è costituito dagli operandi che ospiteranno i relativi risultati (opzionali perché la nostra routine può non prevedere valori di output).

Il terzo parametro sono i valori di input (opzionali in quanto possono essere assenti).

L'ultimo parametro è la lista dei registri modificati (clobbered) nell'ambito delle istruzioni assembly (opzionale perché potremmo non modificarne alcuno o gestire il loro salvataggio e ripristino con lo stack).

Vediamo subito un piccolo esempio in cui inseriamo la numerazione delle righe per la successiva descrizione.(1)

      1 /*
      2 Programma:     inline1.c
      3 Autore:        FF
      4 Data:          gg/mm/aaaa
      5 Descrizione:   Primo esempio di assembly inline 
      6 */
      7 #include <stdio.h>
      8 int main()
      9 {
     10    int val1, val2; 
     11    printf("Inserire val1: ");
     12    scanf("%d",&val1);
     13    printf("Inserire val2: ");
     14    scanf("%d",&val2);
     15    __asm__("movl %0, %%eax; movl %1, %%ebx;"
     16       "xchgl %%eax, %%ebx;"
     17       : "=r" (val1),
     18         "=r" (val2)
     19       : "r" (val1),
     20         "r" (val2)
     21       : "%eax", "%ebx"
     22    );
     23    printf ("Valori scambiati val1=%d val2=%d\n",val1,val2);
     24    return 0;
     25 }   

Le righe fino alla 14 e da 23 a 25 contengono «normali» istruzioni c che non richiedono commenti.

Alle righe 15 e 16 troviamo le istruzioni assembly; notiamo che per fare uso dei registri occorre raddoppiare la quantità di «%» nel prefisso in quanto un solo «%» viene usato per gli alias.

In queste istruzioni gli alias sono %0 e %1 associati ai primi (e unici) due operandi di input.

Le tre istruzioni assembly sono molto semplici: vengono posti i due operandi di input rispettivamente in eax e ebx e poi i valori dei due registri vengono scambiati.

Alle righe 17 e 18 vengono specificati gli operandi di output %0 e %1 associati rispettivamente alle variabili val1 e val2; la presenza del simbolo di «=» davanti al nome indica che si tratta di valori in output e si richiede che per essi il sistema userà dei registri generali (simbolo «r»).

Alle righe 19 e 20 vengono specificati gli operandi di input con lo stesso criterio.

Con la riga 21 si indicano i registri modificati nella routine assembly.

Infine, alla riga 22, si chiude la definizione di tale routine.

La necessità dell'indicare i registri modificati sta nel fatto che non sappiamo a priori quali registri l'assembly userà per ospitare gli operandi che abbiamo dichiarato, quindi dobbiamo informarlo di quali sono i registri che devono essere riservati per le operazioni svolte dalle istruzioni della routine.

Nella figura E.3 vediamo la compilazione e l'esecuzione del programma.

Figura E.3.

figure/asm-esec-inline1

Nella fase di assegnazione degli operandi si possono anche specificare con esattezza i registri o le etichette di memoria da associare (invece di usare «r») indicandoli per esteso o con le seguenti abbreviazioni:

Nel prossimo esempio vediamo un programma che chiede in input un valore intero n e poi ne calcola il quadrato in assembly come somma dei primi n dispari; il risultato ovviamente viene visualizzato con istruzioni del c.(2)

      1 /*
      2 Programma:     inline2.c
      3 Autore:        FF
      4 Data:          gg/mm/aaaa
      5 Descrizione:   Secondo esempio assembly inline: quadrato = somma di n dispari 
      6 */
      7 #include <stdio.h>
      8 int main()
      9 {
     10    int n,q; 
     11    printf("Inserire n: ");
     12    scanf("%d",&n);
     13    __asm__("xorl %%eax, %%eax;"
     14       "addl %%ebx, %%ebx;"
     15       "movl $1, %%ecx;"
     16       "ciclo: addl %%ecx, %%eax;"
     17       "addl $2, %%ecx;"
     18       "cmpl %%ecx, %%ebx;"
     19       "jg ciclo;"
     20       : "=a" (q)
     21       : "b"  (n)      
     22    );
     23    printf ("Quadrato = %d\n",q);
     24    return 0;
     25 }   

Commentiamo le righe della routine assembly iniziando da quelle di assegnazione degli operandi.

In questo caso, alle righe 20 21, vengono associate le variabili q e n direttamente ai registri eax (in output) e ebx (in input) e quindi non serve specificare la lista dei registri modificati (infatti il relativo parametro qui è assente).

Alla riga 13 viene azzerato il registro eax che funge da accumulatore, mentre alla riga 14 si raddoppia il valore di ebx che corrisponde a n in modo che il ciclo sui numeri dispari continui per valore del contatore minore si 2*n.

Alla riga 15 si imposta il contatore ecx e alla successiva inizia il ciclo con l'accumulo del valore del contatore in eax.

Alla riga 17 si incrementa di due il contatore e poi si confronta con ebx proseguendo il ciclo se quest'ultimo è maggiore (righe 18 e 19).

Nella figura E.5 vediamo l'esecuzione del programma.

Figura E.5.

figure/asm-esec-inline2

E.2   Chiamata di procedure assembly da programmi c

Un'altra alternativa per far convivere codice c e assembly in ambiente GNU/Linux consiste nel richiamo da un programma c di moduli assembly esterni.

Questo è possibile alle seguenti condizioni:

Occorre poi ricordare che i parametri eventualmente passati alla funzione assembly si trovano nello stack a partire dall'ultimo a destra, mentre l'eventuale valore di ritorno va inserito in al, o ax, o eax, o edx:eax, secondo che sia lungo 8, 16, 32 o 64 bit.

Chiariamo tutto con un esempio che consiste ovviamente in due sorgenti: uno per il programma principale in c e uno per la funzione assembly.

Nel programma principale predisponiamo l'input di due valori interi, il richiamo della funzione assembly per il calcolo della potenza con base ed esponente i due valori e la stampa del risultato.(3)

/*
Programma:     c-asm.c
Autore:        FF
Data:          gg/mm/aaaa
Descrizione:   Richiamo di asm da c - main in c 
*/
extern int pot_asm(int b, int e);
#include <stdio.h>
int main()
{
   int base, esp, potenza; 
   printf("Inserire base: ");
   scanf("%d",&base);
   printf("Inserire esponente: ");
   scanf("%d",&esp);
   potenza=pot_asm(base,esp);
   printf ("Valore della potenza = %d\n",potenza);
   return 0;
}   

Nella funzione assembly effettuiamo il calcolo della potenza come successione di prodotti.(4)

/*
Programma:     c-asm-funz.s
Autore:        FF
Data:          gg/mm/aaaa
Descrizione:   Richiamo di asm da c - funzione asm per potenza con succ. * 
*/
.data
.text
.globl pot_asm             # funzione deve essere globale
pot_asm:
   pushl %ebp              # salvo %ebp precedente (event. chiamate nidificate)
   movl %esp, %ebp         # imposto %ebp
   movl 8(%ebp), %ebx      # leggo primo par. (base) dopo 8 byte dalla cima
                           # della pila: 4 per ebp e 4 per ind. di rientro
   movl 12(%ebp), %ecx     # leggo altro parametro (espon)
   xorl %edx, %edx         # azzero %edx coinvolto nella mul
   movl $1, %eax           # imposto accumulatore
ciclo:    
   mull %ebx               # %eax = %eax*%ebx 
   loopl ciclo              # il ciclo va fatto %ecx (espon) volte
                           # al termine il ris si trova in %eax o in
                           # edx:%eax come atteso dal chiamante    
   popl %ebp               # ripristino %ebp precedente
   ret
   

Notiamo come, trattandosi di una procedura assembly, ci siamo comportati, relativamente alla gestione dello stack e all'istruzione di terminazione, allo stesso modo di quando abbiamo scritto procedure «normali» inglobate nei sorgenti assembly.

Per compilare separatamente i due sorgenti eseguiamo i comandi:

gcc -c -o c-asm.o c-asm.c

gcc -c -o c-asm-funz.o c-asm-funz.s

e poi colleghiamoli con il comando:

gcc -o c-asm c-asm.o c-asm-funz.o

Nella figura E.8 sono mostrati appunto tali comandi seguiti dall'esecuzione dell'eseguibile ottenuto.

Figura E.8.

figure/asm-esec-c-asm


1) una copia di questo file, dovrebbe essere disponibile anche qui: <allegati/programmi-assembly/inline1.c>.

2) una copia di questo file, dovrebbe essere disponibile anche qui: <allegati/programmi-assembly/inline2.c>.

3) una copia di questo file, dovrebbe essere disponibile anche qui: <allegati/programmi-assembly/c-asm.c>.

4) una copia di questo file, dovrebbe essere disponibile anche qui: <allegati/programmi-assembly/c-asm-funz.s>.