GNU/Linux >> Znalost Linux >  >> Linux

Proč tato inline sestava nefunguje se samostatným příkazem asm volatile pro každou instrukci?

Blokujete paměť, ale neříkáte o tom GCC, takže GCC může ukládat hodnoty v buf přes shromáždění. Pokud chcete používat vstupy a výstupy, řekněte GCC o všem.

__asm__ (
    "movq %1, 0(%0)\n\t"
    "movq %2, 8(%0)"
    :                                /* Outputs (none) */
    : "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
    : "memory");                     /* Clobbered */

Obecně také chcete nechat GCC zvládnout většinu z mov , výběr registrů atd. – i když registry explicitně omezíte (rrax je stále %rax ) nechte informace proudit přes GCC, jinak získáte neočekávané výsledky.

__volatile__ je špatně.

Důvod __volatile__ existuje, takže můžete zaručit, že kompilátor umístí váš kód přesně tam, kde je... což je zcela zbytečné záruka na tento kód. Je nezbytný pro implementaci pokročilých funkcí, jako jsou paměťové bariéry, ale téměř zcela bezcenný, pokud upravujete pouze paměť a registry.

GCC již ví, že nemůže přesunout toto sestavení po printf protože printf volání přistupuje buf a buf mohl být ucpán shromážděním. GCC již ví, že nemůže přesunout sestavení před rrax=0x39; protože rax je vstup do kódu sestavení. Co tedy dělá __volatile__ dostat tě? Nic.

Pokud váš kód nefunguje bez __volatile__ pak je v kódu chyba, kterou je třeba opravit místo pouhého přidání __volatile__ a doufat, že to všechno zlepší. __volatile__ klíčové slovo není kouzlo a nemělo by se s ním tak zacházet.

Alternativní oprava:

je __volatile__ nutné pro váš původní kód? Ne. Stačí správně označit vstupy a hodnoty 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");

Proč __volatile__ tady ti nepomůže:

rrax = 0x34; /* Dead code */

GCC má právo zcela smazat výše uvedený řádek, protože kód v otázce výše tvrdí, že nikdy nepoužívá rrax .

Jasnější příklad

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)");
}

Demontáž probíhá víceméně podle očekávání na -O0 ,

movl $5, %rax
movq %rax, (global)

Ale s vypnutou optimalizací můžete být v montáži docela nedbalí. Zkusme -O2 :

movq %rax, (global)

Jejda! Kde se stalo rax = 5; jít? Je to mrtvý kód, protože %rax se ve funkci nikdy nepoužívá — alespoň pokud GCC ví. GCC nenahlíží dovnitř sestavy. Co se stane, když odstraníme __volatile__ ?

; empty

Možná si myslíte __volatile__ dělá vám službu tím, že brání GCC odhodit vaši drahocennou sestavu, ale jen maskuje skutečnost, že si GCC myslí, že vaše sestava nedělá cokoliv. GCC si myslí, že vaše sestava nebere žádné vstupy, neprodukuje žádné výstupy a nezabírá žádnou paměť. Měl bys to raději narovnat:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}

Nyní dostaneme následující výstup:

movq %rax, (global)

Lepší. Ale pokud řeknete GCC o vstupech, zajistí, že %rax je nejprve správně inicializováno:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}

Výstup s optimalizací:

movl $5, %eax
movq %rax, (global)

Opravit! A nemusíme ani používat __volatile__ .

Proč __volatile__ existují?

Primární správné použití pro __volatile__ je, pokud váš assembler dělá něco jiného kromě vstupu, výstupu nebo clobbing paměti. Možná si zahrává se speciálními registry, o kterých GCC neví, nebo ovlivňuje IO. V linuxovém jádře to vidíte hodně, ale v uživatelském prostoru se to velmi často zneužívá.

__volatile__ klíčové slovo je velmi lákavé, protože my programátoři v jazyce C si často rádi myslíme, že jsme téměř již programování v jazyce symbolických instrukcí. Nebyly. Kompilátory C provádějí spoustu analýzy datových toků – takže musíte kompilátoru vysvětlit datový tok pro váš assembler. Tímto způsobem může kompilátor bezpečně manipulovat s vaším blokem sestavení stejně jako manipuluje se sestavou, kterou vygeneruje.

Pokud zjistíte, že používáte __volatile__ hodně, jako alternativa můžete napsat celou funkci nebo modul do souboru sestavení.


Kompilátor používá registry a může přepsat hodnoty, které jste do nich vložili.

V tomto případě kompilátor pravděpodobně používá rbx zaregistrujte se po rrbx přiřazení a před sekcí inline montáže.

Obecně byste neměli očekávat, že si registry zachovají své hodnoty po a mezi vloženými sekvencemi kódu sestavení.


Trochu mimo téma, ale rád bych navázal trochu na gcc inline Assembly.

(ne)potřeba __volatile__ vychází ze skutečnosti, že GCC optimalizuje inline montáž. GCC zkontroluje, zda v příkazu k sestavení nejsou vedlejší účinky/předpoklady, a pokud zjistí, že neexistují, může se rozhodnout přesunout instrukci sestavení nebo se dokonce rozhodnout odstranit to. Vše __volatile__ je říct kompilátoru "přestaň se o to starat a dej to tam."

Což obvykle není to, co opravdu chcete.

Zde je potřeba omezení vstupte. Název je přetížený a ve skutečnosti se používá pro různé věci v sestavení GCC:

  • omezení určují vstupní/výstupní operandy používané v asm() blok
  • omezení určují "seznam clobberů", který podrobně popisuje, jaký "stav" (registry, stavové kódy, paměť) jsou ovlivněny asm() .
  • omezení určují třídy operandů (registry, adresy, offsety, konstanty, ...)
  • omezení deklarují asociace / vazby mezi entitami assembleru a proměnnými / výrazy C/C++

V mnoha případech vývojáři zneužívají __volatile__ protože si všimli, že se jejich kód buď přesouvá, nebo dokonce mizí bez něj. Pokud k tomu dojde, je to obvykle spíše známka toho, že se vývojář pokusil ne informovat GCC o vedlejších účincích / předpokladech montáže. Například tento chybový kód:

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);

Má několik chyb:

  • za prvé se kompiluje pouze kvůli chybě gcc (!). Normálně, chcete-li zapsat názvy registrů v inline sestavení, zdvojnásobte %% jsou potřeba, ale ve výše uvedeném, pokud je skutečně zadáte, dostanete chybu kompilátoru/assembleru, /tmp/ccYPmr3g.s:22: Error: bad register name '%%rax' .
  • za druhé, neříká kompilátoru, kdy a kde proměnné potřebujete/používáte. Místo toho předpokládá kompilátor respektuje asm() doslova. To může být pravda pro Microsoft Visual C++, ale není tomu tak pro gcc.

Pokud jej zkompilujete bez optimalizace, vytváří:

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]>
[...]
Můžete najít svůj add instrukce a inicializace dvou registrů a vypíše očekávané. Pokud naopak optimalizaci zvýšíte, stane se něco jiného:
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]>
[ ... ]
Vaše inicializace obou "použitých" registrů zde již nejsou. Kompilátor je zahodil, protože je nepoužívalo nic, co viděl, a zatímco ponechal instrukci sestavení, umístil ji před jakékoli použití těchto dvou proměnných. Je tam, ale nic nedělá (naštěstí ve skutečnosti ... pokud rax / rbx byl používán kdo může říct, co by se stalo...).

A důvodem je to, že jste to ve skutečnosti neřekli GCC, že sestavení používá tyto registry / tyto hodnoty operandů. To nemá nic společného s volatile ale to vše s tím, že používáte bez omezení asm() výraz.

Způsob, jak to udělat správně je přes omezení, tj. použili byste:

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);

To říká kompilátoru, že sestavení:

  1. má jeden argument v registru, "+r"(...) že obojí musí být inicializováno před příkazem sestavení a je upraveno příkazem sestavení a přidružit proměnnou bar s tím.
  2. má druhý argument v registru, "r"(...) které je třeba inicializovat před příkazem sestavení a je považováno za pouze pro čtení / nezměněné příkazem. Zde přiřaďte foo s tím.

Všimněte si, že není specifikováno žádné přiřazení registru - kompilátor to vybere v závislosti na proměnných / stavu kompilace. (Optimalizovaný) výstup výše uvedeného:

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]>
[ ... ]
Omezení vložené sestavy GCC jsou téměř vždy nezbytná v té či oné formě, ale může existovat více možných způsobů, jak popsat stejné požadavky kompilátoru; místo výše uvedeného můžete také napsat:

asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));

To říká gcc:

  1. příkaz má výstupní operand, proměnnou bar , že po výpisu bude nalezen v registru, "=r"(...)
  2. příkaz má vstupní operand, proměnnou foo , který má být umístěn do registru, "r"(...)
  3. Operand nula je také vstupní operand a musí být inicializován pomocí bar

Nebo opět alternativa:

asm("add %1, %0" : "+r"(bar) : "g"(foo));

který říká gcc:

  1. bla (zívnutí – stejné jako předtím, bar oba vstup/výstup)
  2. příkaz má vstupní operand, proměnnou foo , u kterého je příkazu jedno, zda je v registru, v paměti nebo v konstantě v době kompilace (to je "g"(...) omezení)

Výsledek se liší od předchozího:

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]>
[ ... ]
protože nyní GCC skutečně přišel na to foo je konstanta v době kompilace a hodnotu jednoduše vloží do add návod ! Není to úhledné?

Je pravda, že je to složité a vyžaduje si zvyk. Výhodou je, že necháte vybrat kompilátor které registry použít pro jaké operandy umožňuje celkovou optimalizaci kódu; pokud je například v makru a/nebo static inline použit příkaz vložené sestavy Překladač může v závislosti na volajícím kontextu zvolit různé registry při různých instancích kódu. Nebo pokud je určitá hodnota na jednom místě vyhodnotitelná/konstantní v době kompilace, ale na jiném ne, kompilátor jí může přizpůsobit vytvořené sestavení.

Představte si omezení GCC inline Assembly jako jakési „prototypy rozšířených funkcí“ – říkají kompilátoru, jaké typy a umístění pro argumenty / návratové hodnoty jsou, plus něco víc. Pokud tato omezení neurčíte, vaše inline sestava vytváří analogii funkcí, které fungují pouze s globálními proměnnými/stavy – které, jak se pravděpodobně všichni shodneme, zřídka kdy dělají přesně to, co jste zamýšleli.


Linux
  1. Proč substituce procesu Bash nefunguje s některými příkazy?

  2. Proč Bash neukládá příkazy, které začínají mezerami?

  3. Linux – Proč USB nefunguje v Linuxu, když funguje v Uefi/bios?

  1. Proč „ls“ vyžaduje samostatný proces pro provedení?

  2. Proč nevidím MSG_EOR pro SOCK_SEQPACKET na linuxu?

  3. PYTHONPATH nefunguje pro sudo na GNU/Linux (funguje pro root)

  1. Proč Tomcat pracuje s portem 8080, ale ne s 80?

  2. Alias ​​názvu konfigurace ssh nefunguje pro scp

  3. Proč bych měl vytvořit samostatný oddíl pro /tmp?