4 Stimmen

Bus-Fehler: Inline x86-Assembler mit GCC asm{}-Block unter Mac OS X, Konvertierung von MSVC nackter Funktion

Ich erhalte einen "Bus-Fehler", wenn ich versuche, diesen Code mit gcc 4.2.1 auf Snow Leopard kompiliert ausführen

#include <stdio.h>

/*__declspec(naked)*/ void
doStuff(unsigned long int val, unsigned long int flags, unsigned char *result)
{
    __asm{
        push eax
        push ebx
       push ecx
        push edx

        mov eax, dword ptr[esp + 24]//val
        mov ebx, dword ptr[esp + 28]//flags
        //mov ecx, dword ptr[esp + 32]//result

        and eax, ebx
        mov result, eax

        pop edx
        pop ecx
        pop ebx
        pop eax

        ret
    }
}

int main(int argc, char *argv[])
{
    unsigned long val = 0xAA00A1F2; 
    unsigned long flags = 0x00100001;   
    unsigned char result = 0x0;

    doStuff(val, flags, &result);   
    printf("Result is: %2Xh\n", result);

    return 0;
}

Ich verwende den folgenden Befehl zum Kompilieren gcc -fasm-blocks -m32 -o so so.c ohne jegliche Fehler oder Warnungen. Ich versuche, einige Assembler-Anweisungen in der Funktion doStuff() auszuführen und die Antwort dem Ergebnis zuzuweisen. Was mache ich falsch?

(Die gcc Befehl auf diesem Mac ist wahrscheinlich tatsächlich Clang oder eine Apple-Modifikation, da der Mainline-GCC keine Unterstützung für -fasm-blocks .)

Hinweis: Dies funktionierte in Visual Studio unter Windows problemlos, aber ich musste declspec(naked) auskommentieren, damit gcc es auf dem Mac kompilieren konnte.

5voto

Dean Pucsek Punkte 259

Der Grund für den Bus-Fehler liegt darin, dass Sie das Programm ret in Ihrem Assembler-Code. ret bewirkt, dass die Programmkontrolle an die Rücksprungadresse am oberen Ende des Stapels übergeben wird, die Sie durch die Verwendung von push y pop . Ich würde dringend empfehlen, nachzuschauen, was ret in der Intel Instruction Set Reference.

Im Folgenden finden Sie Code, den ich auf einem iMac mit Mac OS X 10.6.7 kompiliert habe und der erfolgreich läuft.

#include <stdio.h>

/*__declspec(naked)*/ void
doStuff(unsigned long int val, unsigned long int flags, unsigned char *result)
{
  __asm
    {
        push eax
        push ebx
        push ecx

        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 ecx
        pop ebx
        pop eax
      }
}

int main(int argc, char *argv[])
{
  unsigned long val =   0xAA00A1F2;
  unsigned long flags = 0x00100002;
  unsigned char result = 0x0;

  doStuff(val, flags, &result);
  printf("Result is: %2Xh\n", result);

  return 0;
}

Bemerkenswerte Änderungen sind:

  1. Entfernung von ret in der Inline-Montage
  2. Verwendung des Registers ebp anstelle von esp um auf die Parameter von doStuff
  3. Ändern von flags zu sein 0x00100002

Die Änderung (1) behebt den Busfehler, (2) macht die Referenzierung der Parameter ein wenig konsistenter und (3) ist nur eine schnelle Möglichkeit, um sicherzustellen, dass die Funktion wie erwartet funktioniert.

Zu guter Letzt empfehle ich Ihnen, sich mit dem GNU Debugger, GDB, vertraut zu machen, falls Sie das noch nicht getan haben. Mehr Informationen dazu finden Sie auf der Projektseite http://www.gnu.org/software/gdb/ sowie Informationen über die Mac-Implementierung und das Tutorial unter https://developer.apple.com/library/archive/documentation/DeveloperTools/gdb/gdb/gdb_toc.html .

2voto

ninjalj Punkte 40810

Compiler fügen Prologe und Epiloge zu Funktionsaufrufen hinzu. Diese Prologe und Epiloge kümmern sich um die Einrichtung von Stackframes, die Reservierung von Stackspace für lokale Variablen, die Zerstörung von Stackframes und die Rückkehr zum Aufrufer.

Ein typischer Prolog für eine Funktion ohne lokale Variablen bei Verwendung eines Rahmenzeigers könnte wie folgt aussehen:

push ebp
mov ebp, esp

Dies speichert den Frame Pointer des Aufrufers auf dem Stack und macht den aktuellen Frame Pointer gleich dem Stack Pointer zur Zeit des Funktionseintritts.

Der entsprechende Epilog würde lauten:

pop ebp
ret

der den vorherigen Rahmenzeiger wiederherstellt und zum Aufrufer zurückkehrt.

Wenn Sie gcc anweisen, keine Frame-Zeiger zu verwenden ( -fomit-frame-pointer ), ist der entsprechende Prolog leer, und der Epilog enthält lediglich eine ret .

Das __declspec(naked) ist wahrscheinlich ähnlich wie die von gcc __attribute__((naked)) ( gccs Funktionsattribute ), das nur für einige Architekturen und nicht auf x86 funktioniert. Unter gcc überlässt man also die Rückgabe an den Aufrufer besser dem Compiler, wie Dean Pucsek geraten hat.

0voto

Peter Cordes Punkte 279904

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:

  1. 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).
  2. Verschieben Sie die asm in einen separaten .s Datei, und deklarieren Sie einfach einen Prototyp in C oder C++.
  3. 使用方法 __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.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X