GNU/Linux >> Linux Esercitazione >  >> Linux

Perché questo assembly inline non funziona con un'istruzione volatile asm separata per ogni istruzione?

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:

  1. 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 variabile bar con esso.
  2. 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, associa foo 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:

  1. l'istruzione ha un operando di output, la variabile bar , che dopo l'istruzione si troverà in un registro, "=r"(...)
  2. l'istruzione ha un operando di input, la variabile foo , che deve essere inserito in un registro, "r"(...)
  3. 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:

  1. bla (sbadiglio - come prima, bar sia ingresso/uscita)
  2. 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.


Linux
  1. Perché la sostituzione del processo Bash non funziona con alcuni comandi?

  2. Perché Bash non memorizza i comandi che iniziano con spazi?

  3. Linux – Perché Usb non funziona in Linux quando funziona in Uefi/bios?

  4. Perché "ls" richiede un processo separato per l'esecuzione?

  5. PYTHONPATH non funziona per sudo su GNU/Linux (funziona per root)

Lavorare con il kernel in tempo reale per Red Hat Enterprise Linux

Lo script Nohup per Python non funziona durante l'esecuzione in background con &

perché sftp rmdir non funziona?

Perché questa espressione regolare non funziona su Linux?

Perché USB non funziona in Linux quando funziona in UEFI/BIOS?

Perché il mio crontab non funziona e come posso risolverlo?