GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Welche Bedeutung haben die einzelnen Zeilen der Assembly-Ausgabe eines C hello world?

So geht's:

        .file   "test.c"

Der ursprüngliche Quelldateiname (wird von Debuggern verwendet).

        .section        .rodata
.LC0:
        .string "Hello world!"

Ein nullterminierter String ist im Abschnitt ".rodata" enthalten ("ro" bedeutet "nur lesen":Die Anwendung kann die Daten lesen, aber jeder Versuch, sie zu schreiben, löst eine Ausnahme aus).

        .text

Jetzt schreiben wir Dinge in den ".text"-Abschnitt, wo der Code hinkommt.

.globl main
        .type   main, @function
main:

Wir definieren eine Funktion namens "main", die global sichtbar ist (andere Objektdateien können sie aufrufen).

        leal    4(%esp), %ecx

Wir speichern im Register %ecx den Wert 4+%esp (%esp ist der Stapelzeiger).

        andl    $-16, %esp

%esp wird leicht modifiziert, so dass es ein Vielfaches von 16 wird. Für einige Datentypen (das Gleitkommaformat, das double von C entspricht und long double ), ist die Leistung besser, wenn die Speicherzugriffe auf Adressen erfolgen, die ein Vielfaches von 16 sind. Dies wird hier nicht wirklich benötigt, aber wenn es ohne das Optimierungs-Flag verwendet wird (-O2 ...), tendiert der Compiler dazu, ziemlich viel generischen, nutzlosen Code zu produzieren (d.h. Code, der in manchen Fällen nützlich sein könnte, aber hier nicht).

        pushl   -4(%ecx)

Das hier ist etwas seltsam:an dieser Stelle das Wort an der Adresse -4(%ecx) ist das Wort, das vor andl ganz oben auf dem Stapel war . Der Code ruft dieses Wort ab (das übrigens die Rücksendeadresse sein sollte) und schiebt es erneut. Diese Art von emuliert, was mit einem Aufruf von einer Funktion erhalten würde, die einen 16-Byte-ausgerichteten Stapel hatte. Meine Vermutung ist, dass diese push ist ein Überbleibsel einer Argumentkopiersequenz. Da die Funktion den Stapelzeiger angepasst hat, muss sie die Funktionsargumente kopieren, die über den alten Wert des Stapelzeigers zugänglich waren. Hier gibt es außer der Rücksprungadresse der Funktion kein Argument. Beachten Sie, dass dieses Wort nicht verwendet wird (noch einmal, dies ist Code ohne Optimierung).

        pushl   %ebp
        movl    %esp, %ebp

Dies ist der Prolog der Standardfunktion:Wir speichern %ebp (da wir es ändern werden), dann setzen Sie %ebp auf den Stapelrahmen zeigen. Danach %ebp wird verwendet, um auf die Funktionsargumente zuzugreifen, wodurch %esp entsteht wieder frei. (Ja, es gibt kein Argument, also ist es für diese Funktion nutzlos.)

        pushl   %ecx

Wir speichern %ecx (Wir brauchen es beim Beenden der Funktion, um %esp wiederherzustellen auf den Wert, den es vor andl hatte ).

        subl    $20, %esp

Wir reservieren 32 Bytes auf dem Stack (denken Sie daran, dass der Stack „runterwächst“). Dieser Platz wird verwendet, um die Argumente für printf() zu speichern (Das ist übertrieben, da es ein einziges Argument gibt, das 4 Bytes verwendet [das ist ein Zeiger]).

        movl    $.LC0, (%esp)
        call    printf

Wir „pushen“ das Argument auf printf() (d.h. wir stellen sicher, dass %esp zeigt auf ein Wort, das das Argument enthält, hier $.LC0 , die die Adresse der konstanten Zeichenfolge im Rodata-Abschnitt ist). Dann rufen wir printf() an .

        addl    $20, %esp

Wenn printf() zurückgibt, entfernen wir den für die Argumente zugewiesenen Platz. Diese addl bricht ab, was der subl oben getan.

        popl    %ecx

Wir stellen %ecx wieder her (oben gedrückt); printf() möglicherweise geändert haben (die Aufrufkonventionen beschreiben, welche Register eine Funktion ändern kann, ohne sie beim Beenden wiederherzustellen; %ecx ist ein solches Register).

        popl    %ebp

Epilog der Funktion:Dies stellt %ebp wieder her (entspricht dem pushl %ebp oben).

        leal    -4(%ecx), %esp

Wir stellen %esp wieder her auf seinen Anfangswert. Der Effekt dieses Opcodes ist das Speichern in %esp den Wert %ecx-4 . %ecx wurde im Opcode der ersten Funktion gesetzt. Dadurch werden alle Änderungen an %esp rückgängig gemacht , einschließlich andl .

        ret

Funktion beenden.

        .size   main, .-main

Dies legt die Größe des main() fest Funktion:zu jedem Zeitpunkt während des Zusammenbaus ". " ist ein Alias ​​für "die Adresse, an der wir gerade Dinge hinzufügen". Wenn hier eine weitere Anweisung hinzugefügt würde, würde sie an die durch ". angegebene Adresse gehen ". Also ".-main ", hier ist die genaue Größe des Codes der Funktion main() . Der .size weist den Assembler an, diese Informationen in die Objektdatei zu schreiben.

        .ident  "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"

GCC liebt es einfach, Spuren seiner Wirkung zu hinterlassen. Dieser String landet als eine Art Kommentar in der Objektdatei. Der Linker wird es entfernen.

        .section        .note.GNU-stack,"",@progbits

Ein spezieller Abschnitt, in dem GCC schreibt, dass der Code einen nicht ausführbaren Stack aufnehmen kann. Dies ist der Normalfall. Ausführbare Stacks werden für einige spezielle Anwendungen benötigt (nicht Standard-C). Auf modernen Prozessoren kann der Kernel einen nicht ausführbaren Stapel erstellen (ein Stapel, der eine Ausnahme auslöst, wenn jemand versucht, einige Daten, die sich auf dem Stapel befinden, als Code auszuführen); dies wird von einigen Leuten als "Sicherheitsfeature" angesehen, weil das Platzieren von Code auf dem Stack eine gängige Methode ist, um Pufferüberläufe auszunutzen. Mit diesem Abschnitt wird die ausführbare Datei als "kompatibel mit einem nicht ausführbaren Stack" markiert, was der Kernel gerne als solches bereitstellt.


    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ecx
    subl    $20, %esp

Diese Anweisungen sind in Ihrem C-Programm nicht vergleichbar, sie werden immer am Anfang jeder Funktion ausgeführt (aber es hängt vom Compiler/der Plattform ab)

    movl    $.LC0, (%esp)
    call    printf

dieser Block entspricht Ihrem Aufruf von printf(). Die erste Anweisung legt ihr Argument (einen Zeiger auf „Hallo Welt“) auf den Stack und ruft dann die Funktion auf.

    addl    $20, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret

Diese Anweisungen sind dem ersten Block entgegengesetzt, sie sind eine Art Stack-Manipulationszeug. immer auch ausgeführt


Hier ist eine Ergänzung zu @Thomas Pornin 's Antwort.

  • .LC0 lokale Konstante, z. B. String-Literal.
  • .LFB0 Anfang der lokalen Funktion,
  • .LFE0 lokale Funktion endet,

Das Suffix dieser Bezeichnung ist eine Zahl und beginnt bei 0.

Dies ist die gcc-Assembler-Konvention.


Linux
  1. So fügen Sie Text am Anfang jeder Zeile in Vim ein

  2. Was sind die Verantwortlichkeiten jeder Pseudo-Terminal (pty)-Komponente (Software, Master-Seite, Slave-Seite)?

  3. Wie kann ich von der Befehlszeile aus feststellen, auf welcher Version von Os X ich mich befinde?

  4. Die Zeichen jeder Zeile mit Wc zählen?

  5. Was bedeutet in der Ausgabe von Ps?

Iteration über jede Zeile der Ausgabe von ls -l

Was ist der zweite Status in der IP-Link-Show-Ausgabe

Was ist die Pufferspalte in der Ausgabe von free?

Was bedeutet 1 am Ende des awk-Skripts?

Was bedeutet curl -k -i -X ​​unter Linux?

Was ist das Reverse-DNS-Befehlszeilendienstprogramm?