Occupi la memoria ma non dirlo a GCC, quindi GCC può memorizzare nella cache i valori in buf
durante le convocazioni di assemblea. Se vuoi usare input e output, racconta tutto a GCC.
__asm__ (
"movq %1, 0(%0)\n\t"
"movq %2, 8(%0)"
: /* Outputs (none) */
: "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
: "memory"); /* Clobbered */
Generalmente vuoi anche lasciare che GCC gestisca la maggior parte del mov
, selezione dei registri, ecc -- anche se vincoli esplicitamente i registri (rrax è ancora %rax
) lascia che le informazioni scorrano attraverso GCC o otterrai risultati inaspettati.
__volatile__
è sbagliato.
Il motivo __volatile__
esiste è così puoi garantire che il compilatore collochi il tuo codice esattamente dov'è... il che è completamente inutile garanzia per questo codice. È necessario per implementare funzionalità avanzate come le barriere di memoria, ma quasi del tutto inutile se si modificano solo memoria e registri.
GCC sa già che non può spostare questo assieme dopo il printf
perché il printf
la chiamata accede a buf
e buf
potrebbe essere rovinato dall'assemblea. GCC sa già che non può spostare l'assembly prima di rrax=0x39;
perché rax
è un input per il codice assembly. Quindi cosa significa __volatile__
prenderti? Niente.
Se il tuo codice non funziona senza __volatile__
allora c'è un errore nel codice che dovrebbe essere corretto invece di aggiungere solo __volatile__
e sperando che questo renda tutto migliore. Il __volatile__
la parola chiave non è magica e non dovrebbe essere trattata come tale.
Correzione alternativa:
È __volatile__
necessario per il codice originale? No. Basta contrassegnare correttamente gli input e i valori di clobber.
/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
The inputs and clobbered values are specified. There is no output
so that section is blank. */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");
Perché __volatile__
non ti aiuta qui:
rrax = 0x34; /* Dead code */
GCC ha il diritto di eliminare completamente la riga sopra, poiché il codice nella domanda sopra afferma di non utilizzare mai rrax
.
Un esempio più chiaro
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)");
}
Lo smontaggio è più o meno come te lo aspetti in -O0
,
movl $5, %rax
movq %rax, (global)
Ma con l'ottimizzazione disattivata, puoi essere abbastanza sciatto riguardo all'assemblaggio. Proviamo -O2
:
movq %rax, (global)
Ops! Dove ha fatto rax = 5;
andare? È un codice morto, dal %rax
non viene mai utilizzato nella funzione, almeno per quanto ne sa GCC. GCC non fa capolino all'interno dell'assieme. Cosa succede quando rimuoviamo __volatile__
?
; empty
Bene, potresti pensare a __volatile__
ti sta rendendo un servizio impedendo a GCC di scartare il tuo prezioso assembly, ma sta solo mascherando il fatto che GCC pensa che il tuo assembly non funzioni qualsiasi cosa. GCC pensa che il tuo assembly non prenda input, non produca output e non ostruisca memoria. Faresti meglio a raddrizzarlo:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}
Ora otteniamo il seguente output:
movq %rax, (global)
Meglio. Ma se dici a GCC degli input, si assicurerà che %rax
viene inizializzato correttamente per primo:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}
L'output, con le ottimizzazioni:
movl $5, %eax
movq %rax, (global)
Corretta! E non abbiamo nemmeno bisogno di usare __volatile__
.
Perché __volatile__
esiste?
Il principale uso corretto di __volatile__
è se il tuo codice assembly fa qualcos'altro oltre a input, output o ostruzione della memoria. Forse fa casino con registri speciali di cui GCC non è a conoscenza o influisce su IO. Lo vedi spesso nel kernel di Linux, ma è usato in modo improprio molto spesso nello spazio utente.
Il __volatile__
è molto allettante perché a noi programmatori C piace spesso pensare di essere quasi programmazione già in linguaggio assembly. Non erano. I compilatori C eseguono molte analisi del flusso di dati, quindi è necessario spiegare il flusso di dati al compilatore per il codice assembly. In questo modo, il compilatore può manipolare in modo sicuro il tuo pezzo di assembly proprio come manipola l'assembly che genera.
Se ti ritrovi a usare __volatile__
molto, in alternativa potresti scrivere un'intera funzione o modulo in un file assembly.
Il compilatore utilizza i registri e potrebbe sovrascrivere i valori che hai inserito in essi.
In questo caso, il compilatore usa probabilmente rbx
registrati dopo il rrbx
assegnazione e prima della sezione di assemblaggio in linea.
In generale, non dovresti aspettarti che i registri mantengano i loro valori dopo e tra le sequenze di codice assembly inline.
Leggermente fuori tema, ma mi piacerebbe approfondire un po' l'assembly inline di gcc.
Il (non) bisogno di __volatile__
deriva dal fatto che GCC ottimizza montaggio in linea. GCC ispeziona la dichiarazione di assemblaggio alla ricerca di effetti collaterali/prerequisiti e, se li trova inesistenti, può scegliere di spostare l'istruzione di assemblaggio o addirittura decidere di rimuoverla esso. Tutto __volatile__
fa è dire al compilatore "smettila di preoccuparti e mettilo lì".
Che di solito non è quello che vuoi veramente.
È qui che c'è bisogno di vincoli come in. Il nome è sovraccarico ed effettivamente utilizzato per diverse cose nell'assembly in linea di GCC:
- i vincoli specificano gli operandi di input/output utilizzati nel
asm()
blocco - i vincoli specificano la "lista clobber", che dettaglia quale "stato" (registri, codici di condizione, memoria) è influenzato dal
asm()
. - i vincoli specificano classi di operandi (registri, indirizzi, offset, costanti, ...)
- i vincoli dichiarano associazioni/binding tra entità assembler e variabili/espressioni C/C++
In molti casi, gli sviluppatori abusano __volatile__
perché hanno notato che il loro codice veniva spostato o addirittura scompariva senza di esso. Se ciò accade, di solito è piuttosto un segno che lo sviluppatore ha tentato di non per informare GCC sugli effetti collaterali / prerequisiti dell'assemblea. Ad esempio, questo codice difettoso:
register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;
asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
Ha diversi bug:
- per prima cosa, si compila solo a causa di un bug di gcc (!). Normalmente, per scrivere i nomi dei registri in assembly inline, doppio
%%
sono necessari, ma in quanto sopra se li specifichi effettivamente ottieni un errore del compilatore/assembler,/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
. - secondo, non sta dicendo al compilatore quando e dove hai bisogno/usa le variabili. Invece, presuppone il compilatore rispetta
asm()
letteralmente. Potrebbe essere vero per Microsoft Visual C++, ma non è così per gcc.
Se lo compili senza ottimizzazione, crea:
0000000000400524 <main>: [ ... ] 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: bb e1 10 00 00 mov $0x10e1,%ebx 40053e: 48 01 c3 add %rax,%rbx 400541: 48 89 da mov %rbx,%rdx 400544: b8 5c 06 40 00 mov $0x40065c,%eax 400549: 48 89 d6 mov %rdx,%rsi 40054c: 48 89 c7 mov %rax,%rdi 40054f: b8 00 00 00 00 mov $0x0,%eax 400554: e8 d7 fe ff ff callq 400430 <[email protected]> [...]Puoi trovare il tuo
add
istruzione e le inizializzazioni dei due registri e stamperà il file previsto. Se, d'altra parte, aumenti l'ottimizzazione, succede qualcos'altro:0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: 48 01 c3 add %rax,%rbx 400537: be e1 10 00 00 mov $0x10e1,%esi 40053c: bf 3c 06 40 00 mov $0x40063c,%edi 400541: 31 c0 xor %eax,%eax 400543: e8 e8 fe ff ff callq 400430 <[email protected]> [ ... ]Le tue inizializzazioni di entrambi i registri "usati" non sono più presenti. Il compilatore li ha scartati perché nulla di ciò che poteva vedere li stava usando, e pur mantenendo l'istruzione di assemblaggio l'ha messa prima qualsiasi utilizzo delle due variabili. È lì ma non fa nulla (per fortuna in realtà ... se
rax
/ rbx
era stato utilizzato chi può dire cosa sarebbe successo...).
E il motivo è che in realtà non l'hai detto GCC che l'assembly sta usando questi registri/questi valori di operando. Questo non ha niente a che fare con volatile
ma tutto con il fatto che stai usando un asm()
senza vincoli espressione.
Il modo per farlo correttamente è tramite vincoli, ovvero dovresti usare:
int foo = 1234;
int bar = 4321;
asm("add %1, %0" : "+r"(bar) : "r"(foo));
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
Questo dice al compilatore che l'assembly:
- ha un argomento in un registro,
"+r"(...)
che entrambi devono essere inizializzati prima dell'istruzione assembly e vengono modificati dall'istruzione assembly e associare la variabilebar
con esso. - ha un secondo argomento in un registro,
"r"(...)
che deve essere inizializzato prima dell'istruzione assembly e viene trattato come di sola lettura/non modificato dall'istruzione. Qui, associafoo
con quello.
Si noti che non è specificata alcuna assegnazione di registro:il compilatore lo sceglie in base alle variabili/stato della compilazione. L'output (ottimizzato) di quanto sopra:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: be e1 10 00 00 mov $0x10e1,%esi 40053e: bf 4c 06 40 00 mov $0x40064c,%edi 400543: 01 c6 add %eax,%esi 400545: 31 c0 xor %eax,%eax 400547: e8 e4 fe ff ff callq 400430 <[email protected]> [ ... ]I vincoli di assemblaggio in linea GCC sono quasi sempre necessari in una forma o nell'altra, ma possono esserci molteplici modi possibili per descrivere gli stessi requisiti al compilatore; invece di quanto sopra, potresti anche scrivere:
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
Questo dice a gcc:
- l'istruzione ha un operando di output, la variabile
bar
, che dopo l'istruzione si troverà in un registro,"=r"(...)
- l'istruzione ha un operando di input, la variabile
foo
, che deve essere inserito in un registro,"r"(...)
- l'operando zero è anch'esso un operando di input e deve essere inizializzato con
bar
Oppure, ancora un'alternativa:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
che dice a gcc:
- bla (sbadiglio - come prima,
bar
sia ingresso/uscita) - l'istruzione ha un operando di input, la variabile
foo
, che all'istruzione non interessa se si trova in un registro, in memoria o in una costante in fase di compilazione (questa è la"g"(...)
vincolo)
Il risultato è diverso dal precedente:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: bf 4c 06 40 00 mov $0x40064c,%edi 400539: 31 c0 xor %eax,%eax 40053b: be e1 10 00 00 mov $0x10e1,%esi 400540: 81 c6 d2 04 00 00 add $0x4d2,%esi 400546: e8 e5 fe ff ff callq 400430 <[email protected]> [ ... ]perché ora, GCC ha capito
foo
è una costante in fase di compilazione e incorpora semplicemente il valore in add
istruzioni ! Non è pulito?
Certo, questo è complesso e ci si abitua. Il vantaggio è che lasciando che sia il compilatore a scegliere quali registri utilizzare per quali operandi permette di ottimizzare il codice nel suo complesso; se, ad esempio, un'istruzione di assembly inline viene utilizzata in una macro e/o in un static inline
funzione, il compilatore può, a seconda del contesto di chiamata, scegliere diversi registri a diverse istanze del codice. Oppure, se un determinato valore è valutabile/costante in fase di compilazione in un punto ma non in un altro, il compilatore può personalizzare l'assembly creato per esso.
Pensa ai vincoli di assemblaggio in linea di GCC come una sorta di "prototipi di funzioni estese":dicono al compilatore quali tipi e posizioni per argomenti/valori restituiti sono, oltre a qualcosa in più. Se non specifichi questi vincoli, il tuo assembly inline sta creando l'analogo delle funzioni che operano solo su variabili globali/stato - che, come probabilmente siamo tutti d'accordo, raramente fanno esattamente ciò che intendevi.