Nicht ret
aus dem Inneren eines asm
Block oder Anweisung außer bei __attribute__((naked))
.
Treffen Sie keine Annahmen über [ebp+x]
Halten bestimmter C-Variablen in nicht-nackten Inline-Asm.
@Dean Pucsek's Antwort (mit [ebp+8]
, 12, 16 in einer nicht-nackten Funktion) kann zufällig mit deaktivierter Optimierung funktionieren, aber es bricht spektakulär 1 in einem normalen -O2
bauen . ( doStuff
in seinen Aufrufer einfügt, der unterschiedliche Args bei [ebp + 8]
, 12, und 16). Es sei denn, Sie haben es in einer separaten Kompiliereinheit von einem Aufrufer und verwenden nicht -flto
.
Sie haben drei Möglichkeiten, abgesehen davon, dass Sie Inline-Asm für so etwas Triviales gar nicht erst verwenden:
- Entfernen Sie die
ret
und ändern Sie es so, dass es benannte C-Variablen für die Args verwendet anstatt Annahmen darüber zu treffen, dass diese Funktion nicht inline ist. (Sie können également 使い道 __attribute__((noinline))
wenn Sie wollen, dass es aus irgendeinem Grund nicht inline ist, aber es gibt keinen Vorteil, die Aufrufkonvention hart zu kodieren, und keine Notwendigkeit, wenn es nicht in einer naked
Funktion).
- Verschieben Sie die asm in einen separaten
.s
Datei, und deklarieren Sie einfach einen Prototyp in C oder C++.
- 使用方法
__attribute__((naked))
die jetzt für x86 von clang unterstützt wird. (Und GCC, aber der Mainline-GCC selbst unterstützt keine -fasm-blocks
. Sie verwenden eine Apple-Version von GCC, oder tatsächlich Clang, das als gcc
für Shell-Skripte/Makefiles, wie es das aktuelle MacOS tut).
Konvertierung in tatsächliches Inline-Asm ohne naked
o noinline
wird inline (im Gegensatz zu den Optionen 2 und 3), so dass es etwas weniger ineffizient ist, aber es ist immer noch ein asm Block und nicht eine GNU C asm-Anweisung wie diese (siehe den Godbolt-Link unten für diese Anweisung in einer Funktion)
asm("and %[flags], %[outval]" // AT&T syntax: op src, dst. clang always parses this way, gcc with -masm=intel treats inline asm as Intel-syntax
: [outval]"=r"(*(unsigned char(*)[4])result) // 4-element uchar array. Normally type-pun deref is strict-aliasing UB, but GCC documents this for asm.
: "0"(val) /*pick same register as output 0*/, [flags]"r" (flags) // reg, mem, or immediate source
: // no clobbers
);
Es ist ein bisschen seltsam, dass Ihr aktuelles asm 4 Bytes in einem unsigned char*
Ausgabe (deshalb habe ich nicht einfach verwenden konnte "=r"(*result)
), aber ich vermute, es ist ein Char-Array, oder tatsächlich zeigt auf eine unaligned dword irgendwo? Ich habe das erhalten, anstatt dem Compiler zu sagen, dass wir nur das untere Byte der Ausgabe wollen.
Dies würde dem Compiler erlauben, beide Eingänge in Registern zu haben und die mov
Anweisungen für Sie. Siehe https://stackoverflow.com/tags/inline-assembly/info . Aber natürlich ist es immer noch eine undurchsichtige Inline-Asm-Anweisung, die der Optimierer nicht durchschauen kann oder so, also https://gcc.gnu.org/wiki/DontUseInlineAsm wenn Sie es vermeiden können.
Mit einer "rmi"
o "r,m,i"
Constraint, eigentlich wäre GCC in der Lage, intelligent eine sofortige oder Speicher zu wählen, aber Clang ist dumm in dieser Hinsicht und wählt immer Speicher. Oder für x,y,z
wählt immer die erste Option, weshalb ich die Registrierung an die erste Stelle gesetzt habe.
Sie würden dieses Problem nicht haben, wenn Sie val &= flags;
memcpy(result, &val, sizeof(val));
- können sowohl GCC als auch clang optimal die and
mit minimaler Verschwendung mov
Anweisungen.
Weiterhin Verwendung von asm-Blöcken im MSVC-Stil
Wenn Sie wirklich ineffiziente asm-Blöcke im MSVC-Stil wollen, die zwingt den Compiler, die Ein- und Ausgänge im Speicher und nicht in Registern zu haben verwenden
// without __attribute__((naked))
void
doStuff(unsigned long int val, unsigned long int flags, unsigned char *result)
{
__asm // don't push/pop inside the asm block: the compiler still sees all touched registers as clobbered and saves itself if necessary
{
mov eax, [val] // compiler will fill in [esp+x] or [ebp+y] or whatever for C var names
and eax, [flags] // memory-source AND is fine
mov ecx, [result] // load the pointer variable
mov [ecx], eax // deref it, storing 4 bytes to result[0..3]
}
// for non-void functions: beware MSVC supports falling off the end without a return statement, with a value left in EAX by an asm{} block. (Even respecting that EAX result after inlining this function into another).
// clang -fasm-blocks doesn't: it's undefined behaviour in C++, or in C if the caller uses it.
}
Godbolt mit clang14.0 -O3 -m32 -fasm-blocks -Wall -fno-pie
- ist die eigenständige Version so effizient wie möglich, da die klobige stack-args-Aufrufkonvention und die asm{}
Block, der es nicht besser macht.
doStuff(unsigned long, unsigned long, unsigned char*):
// start of inline asm
mov eax, dword ptr [esp + 4]
and eax, dword ptr [esp + 8]
mov ecx, dword ptr [esp + 12]
mov dword ptr [ecx], eax
// end of inline asm
ret
Ein Testaufruf zeigt, dass er sicher (aber ineffizient) inlined wie man es von einem asm{}
Block):
// test caller
unsigned char global_charbuf[4];
void foo(unsigned long int flags, unsigned char *result) {
// result unused, unless you edit to pass it instead
doStuff(1234, flags, global_charbuf); // safe after inlining: stores 1234 to the stack
}
foo(unsigned long, unsigned char*):
sub esp, 12 // space for uchar *result local var
mov eax, dword ptr [esp + 16] // foo's flags arg
mov dword ptr [esp + 8], 1234
mov dword ptr [esp + 4], eax // This copy seems unnecessary; asm should be able to simply reference foo's stack arg
mov dword ptr [esp], offset global_charbuf // It's not preserving their relative order.
// start of inline asm
mov eax, dword ptr [esp + 8]
and eax, dword ptr [esp + 4]
mov ecx, dword ptr [esp]
mov dword ptr [ecx], eax
// end of inline asm
add esp, 12
ret
Derselbe Testaufrufer, der dieselben Argumente an eine Wrapper-Funktion mit GNU C übergibt asm("..." :outputs :inputs :clobbers)
kompiliert zu nicht perfektem, aber viel weniger schrecklichem asm. Die Godbolt Link für Quelle für das.
bar(unsigned int, unsigned char*)
mov eax, dword ptr [esp + 4] # compiler-generated loads of the "r" register inputs
mov ecx, 1234 # clang is incapable of getting inline asm to use an AND ecx, 1234 unless we *only* allow an immediate source. Or maybe use __builtin_constant_p() around multiple separate blocks.
// start of asm
and ecx, eax # =r output picked EAX, "r" input picked ECX
// end of asm
mov dword ptr [global_charbuf], ecx # Compiler-generated store of the "=r" output
ret
Sie sollten nicht manuell push
/ pop
Registern innerhalb einer asm{}
Block, entweder mit MSVC oder clang -fasm-blocks
. Beide analysieren Ihr ASM und finden heraus, welche Register tatsächlich geschrieben werden, und behandeln den Block als Clobbering dieser Register. (Und wenn nötig, lassen Sie die enthaltende Funktion alle diese Register, die in der aufrufenden Konvention aufruferhalten sind, in der Funktion, in die die Funktion schließlich eingefügt wird, speichern/wiederherstellen).
Anscheinend haben einige sehr frühe C++-Implementierungen, wie z. B. Borland Turbo C++, die no parsen, so dass Sie manuell push/pop machen mussten, um dem Compiler nicht auf die Füße zu treten. Aber das klingt noch klobiger und ineffizienter; zum Glück sind diese Zeiten längst vorbei.
Fußnote 1: kaputtes asm, wenn Sie das falsch verstehen
// FIXME: use __attribute__((naked)) and put the RET back in, with [ESP+x] addressing
/*__declspec(naked)*/ void
broken_doStuff(unsigned long int val, unsigned long int flags, unsigned char *result)
{
__asm
{
push ebx // keep one unnecessary push, just the one you'd need in an actual naked function to not violate the calling convention. EAX and ECX are call-clobbered. So is EDX, but not EBX.
mov eax, dword ptr[ebp + 8] //val
mov ebx, dword ptr[ebp + 12] //flags
mov ecx, dword ptr[ebp + 16] //result
and eax, ebx
mov [ecx], eax
pop ebx
}
}
void foo(unsigned long int flags, unsigned char *result) {
doStuff(1234, flags, result);
// broken: inlines the asm that assumes 3 args on the stack
}
Kompiliert über Godbolt mit clang14.0 -m32 -O3 -fasm-blocks
. Die eigenständige Definition von doStuff
ist in Ordnung; ineffizient, aber funktioniert. Das Problem ist, wenn es in die foo
:
foo(unsigned long, unsigned char*):
push ebp // a push or pop in the inline asm makes clang use EBP as a frame pointer
mov ebp, esp
push ebx // generated by clang, since asm writes this call-preserved reg
// no mov dst, 1234 anywhere:
// clang doesn't see doStuff using its "val" arg
// Start of inline asm
push ebx
mov eax, dword ptr [ebp + 8] // wants val, actually loads foo's first arg, flags
mov ebx, dword ptr [ebp + 12]
mov ecx, dword ptr [ebp + 16] // these also access the wrong things
and eax, ebx
mov dword ptr [ecx], eax
pop ebx
// end of inline asm
pop ebx // compiler-generated epilogue
pop ebp // not leave, it assumes inline asm balanced the stack, and knows it didn't allocate any stack space itself.
ret
Dies führt normalerweise zu einem Absturz, wenn versucht wird, mit einer unsigned char *result
für die es Müll geladen hat, von irgendwo im Stack-Frame des Aufrufers über foo
Stack-Args.
Ich habe das Push/Pop eingebaut, um zu zeigen, dass der Compiler bereits sieht, dass EBX geschrieben wird und es speichert/wiederherstellt. Wenn Sie das herausnehmen, wird EBP auch nicht als Rahmenzeiger eingerichtet, da er weiß, dass sich der Stack innerhalb der asm-Anweisung nicht bewegt, so dass Dinge wie val
kann erweitern auf [esp+y]
anstelle von [ebp+x]
.
Dies respektiert auch nicht -mregparm=3
um eine Register-Arg-Aufrufkonvention zu verwenden. Ein regulärer asm{ mov eax, [val] }
würde, obwohl der Compiler trotzdem nur die val
zu speichern, weil die gesamte Semantik von asm{}
Blöcke sind so konzipiert, dass alle Eingänge im Speicher liegen und keine Register von Anfang an mit Eingängen belegt sind.