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.
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:
|
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)
|
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.
|
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:
"a": per eax;
"b": per ebx;
"c": per ecx;
"d": per edx;
"D": per edi;
"S": per esi;
"m": per una etichetta di memoria.
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)
|
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.
|
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:
nel programma c i moduli assembly devono essere dichiarati come esterni;
nel modulo assembly la procedura deve essere dichiarata globale tramite la direttiva .globl (questo la rende richiamabile in altri moduli);
si devono compilare separatamente i due moduli per ottenere i rispettivi file oggetto e poi collegarli in un unico eseguibile.
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)
|
Nella funzione assembly effettuiamo il calcolo della potenza come successione di prodotti.(4)
|
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.
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>.