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í:
- 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ěnnoubar
s tím. - 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ďtefoo
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:
- příkaz má výstupní operand, proměnnou
bar
, že po výpisu bude nalezen v registru,"=r"(...)
- příkaz má vstupní operand, proměnnou
foo
, který má být umístěn do registru,"r"(...)
- 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:
- bla (zívnutí – stejné jako předtím,
bar
oba vstup/výstup) - 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.