GCCs-Assembly-Ausgabe eines leeren Programms auf x86, win32

GCCs-Assembly-Ausgabe eines leeren Programms auf x86, win32


Ich schreibe leere Programme, um Stackoverflow-Coder zu ärgern, NICHT. Ich untersuche gerade die GNU-Toolchain.


Das Folgende könnte mir jetzt zu tief gehen, aber um die leere Programm-Saga fortzusetzen, habe ich begonnen, die Ausgabe des C-Compilers zu untersuchen, das Zeug, das GNU verbraucht.


gcc version 4.4.0 (TDM-1 mingw32)

test.c:


int main()
{
return 0;
}

gcc -S test.c


    .file   "test.c"
.def ___main; .scl 2; .type 32; .endef
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
call ___main
movl $0, %eax
leave
ret

Können Sie erklären, was hier passiert? Hier ist mein Versuch, es zu verstehen. Ich habe den as verwendet Handbuch und meine minimalen x86-ASM-Kenntnisse:



  • .file "test.c" ist die Direktive für den logischen Dateinamen.

  • .def :gemäß der Dokumentation "Beginn Sie mit der Definition von Debugging-Informationen für einen Symbolnamen" . Was ist ein Symbol (ein Funktionsname/eine Variable?) und welche Art von Debugging-Informationen?

  • .scl :Dokumente sagen "Speicherklasse kann kennzeichnen, ob ein Symbol statisch oder extern ist" . Ist das dieselbe statische und extern Ich weiß von C? Und was ist diese '2'?

  • .type :speichert den Parameter "als Typattribut eines Symboltabelleneintrags" , ich habe keine Ahnung.

  • .endef :kein Problem.

  • .text :Nun, das ist problematisch, es scheint etwas zu sein, das Abschnitt genannt wird, und ich habe gelesen, dass es der Ort für Code ist, aber die Dokumentation hat mir nicht zu viel gesagt.

  • .globl "macht das Symbol für ld sichtbar." , das Handbuch ist diesbezüglich ziemlich klar.

  • _main: Dies könnte die Startadresse (?) für meine Hauptfunktion sein

  • pushl_ :Ein langer (32-Bit) Push, der EBP auf den Stack legt

  • movl :32-Bit-Verschiebung. Pseudo-C:EBP = ESP;

  • andl :Logisches UND. Pseudo-C:ESP = -16 & ESP , ich verstehe nicht wirklich, was das bringen soll.

  • call :Schiebt die IP auf den Stack (damit die aufgerufene Prozedur ihren Weg zurück finden kann) und fährt dort fort, wo __main ist. (was ist __main?)

  • movl :Diese Null muss die Konstante sein, die ich am Ende meines Codes zurückgebe. Der MOV platziert diese Null in EAX.

  • leave :Stellt den Stack nach einer ENTER-Anweisung (?) wieder her. Warum?

  • ret :geht zurück zur Befehlsadresse, die auf dem Stack gespeichert ist


Vielen Dank für Ihre Hilfe!


Antworten:



Befehle beginnend mit . sind Anweisungen an den Assembler. Dies sagt nur, dass dies "file.c" ist, diese Informationen können in die Debugging-Informationen der exe exportiert werden.



.def-Direktiven definieren ein Debugging-Symbol. scl 2 bedeutet Speicherklasse 2 (externe Speicherklasse). Typ 32 besagt, dass dieser Sumbol eine Funktion ist. Diese Nummern werden durch das pe-coff exe-Format

definiert

___main ist eine aufgerufene Funktion, die sich um das Bootstrapping kümmert, das gcc benötigt (es erledigt Dinge wie das Ausführen von statischen C++-Initialisierern und andere erforderliche Haushaltsarbeiten).



Beginnt einen Textabschnitt - Code lebt hier.



definiert das _main-Symbol als global, wodurch es für den Linker und andere eingebundene Module sichtbar wird.



Dasselbe wie _main , erstellt Debugging-Symbole, die besagen, dass _main eine Funktion ist. Dies kann von Debuggern verwendet werden.



Beginnt ein neues Etikett (es wird eine Adresse enden). Die obige .globl-Direktive macht diese Adresse für andere Entitäten sichtbar.



Speichert den alten Frame-Zeiger (EBP-Register) auf dem Stack (damit er wieder an seinen Platz gebracht werden kann, wenn diese Funktion endet)



Verschiebt den Stapelzeiger auf das ebp-Register. ebp wird oft als Frame-Zeiger bezeichnet, er zeigt auf den Anfang der Stack-Werte innerhalb des aktuellen "Frames" (normalerweise Funktion), (das Verweisen auf Variablen auf dem Stack über ebp kann Debuggern helfen)



Ands den Stack mit fffffff0, was ihn effektiv an einer 16-Byte-Grenze ausrichtet. Der Zugriff auf ausgerichtete Werte auf dem Stapel ist viel schneller, als wenn sie nicht ausgerichtet wären. All diese vorangehenden Anweisungen sind so ziemlich ein Standard-Funktionsprolog.


call        ___main

Ruft die Funktion ___main auf, die Dinge initialisiert, die gcc benötigt. Call schiebt den aktuellen Befehlszeiger auf den Stack und springt zur Adresse von ___main



Verschiebe 0 in das eax-Register, (die 0 gibt 0 zurück;) das eax-Register wird verwendet, um Funktionsrückgabewerte für die stdcall-Aufrufkonvention zu halten.



Die Leave-Anweisung ist ziemlich abgekürzt für



d.h. es "macht" das Zeug "rückgängig", das am Anfang der Funktion getan wurde - das Wiederherstellen des Frame-Zeigers und des Stacks in seinen früheren Zustand.



Gibt denjenigen zurück, der diese Funktion aufgerufen hat. Es holt den Befehlszeiger aus dem Stapel (den ein entsprechender Aufrufbefehl dort platziert hat) und springt dorthin.