Autor Thema: [solved] NASM: C-Funktion mit Parametern aufrufen  (Gelesen 9464 mal)

Meuchelfix77

  • Beiträge: 4
    • Profil anzeigen
Gespeichert
« am: 05. February 2013, 22:17 »
Hallo LowLevel-Community,
gleich vorweg: Ich bin ziemlich neu in Assembler! :|
Meine Frage: (Wie) ist es möglich aus (N)ASM eine Funktion aufzurufen, die ich in C (Compiler: GCC 4.6.2) geschrieben habe?
Bzw: Wie man eine Funktion aufruft weiss ich bereits, aber wie kann ich ihr Parameter übergeben?

Testweise habe ich mir ein kleines C-Programm geschrieben, welches eine eigene Funktion (fn) aufruft, die einfach die beiden integer-Parameter addiert und zurückgibt.
Dieses dann mit gcc -c source.c kompiliert und mit objdump -d -M intel source.o dann die Ausgabe des GCC-Compilers in einer Datei gespeichert.

Danach habe ich im C-Source den Funktionsaufruf auskommentiert und die beiden letzten Schritte wiederholt (Ausgabe dieses mal in einer anderen Datei gespeichert).

Wenn ich diese beiden Ausgabe-Dateien jetzt vergleiche, versteh ich nur Bahnhhof:
00000000 <_fn>:
   0: 55                    push   ebp
   1: 89 e5                mov    ebp,esp
   3: 8b 45 0c              mov    eax,DWORD PTR [ebp+0xc]
   6: 8b 55 08              mov    edx,DWORD PTR [ebp+0x8]
   9: 01 d0                add    eax,edx
   b: 5d                    pop    ebp
   c: c3                    ret   

0000000d <_main>:
   d: 55                    push   ebp
   e: 89 e5                mov    ebp,esp
  10: 83 e4 f0              and    esp,0xfffffff0
+ 13: 83 ec 10              sub    esp,0x10
! 16: e8 00 00 00 00        call   1b <_main+0xe>
+ 1b: c7 44 24 04 05 00 00 mov    DWORD PTR [esp+0x4],0x5
+ 22: 00
+ 23: c7 04 24 03 00 00 00 mov    DWORD PTR [esp],0x3
+ 2a: e8 d1 ff ff ff        call   0 <_fn>
  2f: b8 00 00 00 00        mov    eax,0x0
  34: c9                    leave 
  35: c3                    ret   
  36: 90                    nop
+ 37: 90                    nop
(Zeilen mit einem + am Anfang sind neu; Zeile mit ! wurde verändert)
Die veränderte Zeile war vorher
  18:   e8 00 00 00 00          call   18 <_main+0xb>

Speziell bei diesem Beispiel wären meine Fragen jetzt:
  • Was macht das and esp, 0xfffffff0?
  • Warum sub esp, 0x10?
  • Was bringen die 2 Parameter bei den calls? Kenne das nur mit einem Label/Funktion (z.B. call print_string)
Ich vermute mal 1b und 23 verschieben meine Parameter (3 und 5) auf den Stack, aber warum jetzt an esp+4 und einmal an esp? (Frage geht einerher mit meiner 2. Frage)

Ich weiss, das sind eine Menge fragen und ich verlange gar nicht, dass die alle beantwortet werden! Es würde mir schon wahnsinnig helfen mir passende Links zu geben (das was ich bei Google gefunden habe, war das, was ich bereits gemacht und oben beschrieben habe) oder mir zumindest einen Schubs in die richtige Richtung zu geben! :wink:

Grüße
Meuchelfix77
« Letzte Änderung: 09. February 2013, 14:15 von Meuchelfix77 »

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #1 am: 05. February 2013, 23:09 »
Das Wiki hat einen Artikel zu Aufrufkonventionen.

Du hast recht, 1b und 23 packen die Parameter auf den Stack, und logischerweise nicht an dieselbe Stelle, sonst würden sie sich ja überschreiben. Auf den Stack kommen im PM 32-Bit-Werte (also 4 Bytes), deswegen ist esp+4 der richtige Platz für den zweiten Parameter. Das sub sorgt vorher dafür, dass Platz auf dem Stack geschaffen wird (ein push ist im Prinzip auch nur ein sub und mov in einem).

Ich muss zugeben, dass ich das and und call 1b nicht auf Anhieb verstehe. call hat aber nicht zwei Parameter, sondern nur einen, nämlich das Sprungziel. Das wird aber in zwei Darstellungen ausgegeben, einmal als absolute Adresse und einmal relativ zum nächsten Symbol, unter dem es steht.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Jidder

  • Administrator
  • Beiträge: 1 625
    • Profil anzeigen
Gespeichert
« Antwort #2 am: 05. February 2013, 23:15 »
Moin moin und Willkommen an Board!

Die Aufrufkonventionen stehen natürlich einmal in den entsprechenden Dokumentationen (ELF-Spezifikation, Microsoft Visual C++-Dokumentation, ...), aber das Wichtigste findest du auch in dem  Wikipedia-Artikel dazu.

zu 1.) Das rundet den Wert in ESP auf ein Vielfaches von 16 ab. Kurzes googeln ergibt, dass das ermöglichen soll, Parameter für SIMD-Instruktionen (SSE, etc.) auf den Stack legen zu können, weil diese auf 16-Bytes ausgerichtet sein müssen. Glaub ich jetzt einfach mal so.

zu 2.) Damit werden 16 Bytes (16 = 0x10) auf dem Stack reserviert. Die können für lokale und temporäre Variablen verwendet werden, oder wie in diesem Fall teilweise durch die Parameter. mov    DWORD PTR [esp+0x4],0x5 und mov    DWORD PTR [esp],0x3 schreiben die beiden Parameter 3 und 5 in diesen Bereich. Das ist wieder ein Optimierungsversuch des Compilers, der sich die push-Befehle sparen will. Die Code ist fast äquivalent zu:
push irgendwas ; damit genau 16 bytes gepusht werden
push irgendwas ; damit genau 16 bytes gepusht werden
push 5
push 3
call _fn

Du siehst, dass in der Compilerausgabe an die Adressen ESP+4 und ESP+0 geschrieben wird. Der folgende CALL-Opcode verringert ESP um 4 und schreibt die Rücksprungadresse auf den Stack. Die Parameter liegen also nun bei ESP+8 bzw. ESP+4. Die Funktion _fn legt nun EBP auf den Stack, was ESP wieder um 4 verringert. Dadurch kann auf die Parameter über ESP+12 bzw. ESP+8 zugegriffen werden. (Das macht der Code auch, aber er kopiert vorher den Wert von ESP nach EBP und greift über EBP darauf zu.)

zu 3.) Das in den Spitzenklammern in der Ausgabe des Disassemblers ist kein Teil des Assemblercodes mehr sondern Anmerkungen. Die stehen in der Regel hinter Zahlen, die Adressen angeben. Also 00000000 <_fn>: heißt, dass an der Adresse 0 die Funktion _fn liegt. Der Call-Opcode hat auch nur ein Argument und call   0 <_fn> heißt, dass der Code an Adresse 0 aufgerufen wird. Das <_fn> dahinter bedeutet, dass der Disassembler festgestellt hat, dass dort die Funktion _fn liegt. Lass dich nicht von den Adressen verwirren: Da du eine Objekt-Datei disassembliert hast, sind die Funktionen noch nicht an ihre endgültige Adresse gelinkt und deswegen beginnt alles bei 0. Der Disassembler schreibt übrigens nicht call _fn (oder call print_string), sondern call 0 <_fn>, weil im Maschinencode nur noch call 0 steht, und er das mit dem _fn selbst herausgefunden hat.

Der Aufruf call   1b <_main+0xe> hat übrigens irgendwas damit zu tun, wie Windowsprogramme aussehen müssen, und der Linker wird dort später eine korrekte Adresse einsetzen. Irgendeine Libraryfunktion muss da aufgerufen werden. Den Befehl kannst du ignorieren.
« Letzte Änderung: 05. February 2013, 23:18 von Jidder »
Dieser Text wird unter jedem Beitrag angezeigt.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #3 am: 06. February 2013, 09:49 »
Der Aufruf call   1b <_main+0xe> hat übrigens irgendwas damit zu tun, wie Windowsprogramme aussehen müssen, und der Linker wird dort später eine korrekte Adresse einsetzen. Irgendeine Libraryfunktion muss da aufgerufen werden. Den Befehl kannst du ignorieren.
Ah, logisch, das ist ja eine Objektdatei. :oops: Was auch immer irgendwelche unaufgeforderten Libraryfunktionen da zu suchen haben...

Statt ignorieren könntest du auch objdump -dr benutzen, das -r zeigt dann den richtigen Symbolnamen an.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Meuchelfix77

  • Beiträge: 4
    • Profil anzeigen
Gespeichert
« Antwort #4 am: 06. February 2013, 20:21 »
Erstmal danke für die ausführlichen Antworten! ;)

Den Aufrufkonventionen-Artikel hier habe ich schon gelesen, aber irgendwie die Funktion int test (int x) { return x; } außer acht gelassen. Nach euren Erklärungen erschliesst sich mir aber auch was da passiert.
+-------------------+------------------+------------------+
| Rücksprungadresse | 1. Parameter (3) | 2. Parameter (5) |
+-------------------+------------------+------------------+
Der Speicher sieht also so aus? (Angenommen die 5 Bereiche sind gleich groß (jeder 32bit)).

Wenn ich das in dem o.g. Wiki-Artikel richtig sehe, dann kann ich auch direkt auf ESP zugreifen, anstatt diesen zuerst in EBP zu verschieben?
Dann hätte ich mir doch ein paar Befehle gespart und die Rechnerei, wo denn nun meine Parameter liegen.

Wow! Ich fürchte bei so guten Erklärungen wird das nicht meine letzte Frage gewesen sein! (Achtung: Drohung) ;)

Grüße und noch einen schönen Abend! :)

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #5 am: 07. February 2013, 19:43 »
+-------------------+------------------+------------------+
| Rücksprungadresse | 1. Parameter (3) | 2. Parameter (5) |
+-------------------+------------------+------------------+
Der Speicher sieht also so aus? (Angenommen die 5 Bereiche sind gleich groß (jeder 32bit)).
Sieht richtig aus, wenn links die kleinere Adresse ist.

Zitat
Wenn ich das in dem o.g. Wiki-Artikel richtig sehe, dann kann ich auch direkt auf ESP zugreifen, anstatt diesen zuerst in EBP zu verschieben?
Ja, das geht und gcc hat auch eine Option, um den Code so zu generieren. Für einen Framepointer (also ebp so wie hier benutzt) bist du aber dankbar, wenn du mal debuggen musst - ansonsten wird es nämlich schwer, einen Backtrace zu bekommen oder mit einem Debugger lokale Variablen zu untersuchen.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Meuchelfix77

  • Beiträge: 4
    • Profil anzeigen
Gespeichert
« Antwort #6 am: 08. February 2013, 00:43 »
Und ein erneutes "Vielen Dank!" ;)
Ja, links ist in meiner "Tabelle" die kleinere Adresse.
Eine letzte Frage hätte ich noch:
Sieht mein Speicher genau genommen nicht sogar so aus?
+-------------------+------------------+------------------+------+------+
| Rücksprungadresse | 1. Parameter (3) | 2. Parameter (5) | leer | leer |
+-------------------+------------------+------------------+------+------+
(Wieder 32 Bit / Bereich.)

Weil ich ja 16 Bit für die Parameter reserviert habe (mit sub 0x10) und nochmal 4 Bit für die Rücksprungadresse (mit dem call).
Ist das nicht ineffizient oder gar Speicherverschwendung?
Oder ist das egal, weil ja mein ESP (ich befinde mich gedanklich direkt nach der pop-Anweisung) momentan auf mein erstes Element zeigt (Rücksprungadresse).
Ich vermute mal dieser wird mit einem Aufruf von ret vom Stack gelöscht und so steht mein ESP auf dem ersten Parameter (?)

Bei einem erneuten Funktionsaufruf (nachdem meine Funktion das erste mal durchlaufen wurde) würde ich ja nun erneut Speicher auf dem Stack reservieren (sub esp, 0x10) und diesen wieder mit Parametern füllen. Aber wie kommen dann die Parameter vom ersten Funktionsaufruf wieder aus meinem Speicher raus? Die blockieren ja quasi den Speicher.
Was übersehe ich?

Jidder

  • Administrator
  • Beiträge: 1 625
    • Profil anzeigen
Gespeichert
« Antwort #7 am: 08. February 2013, 00:51 »
Die 8 Bytes, die da ungenutzt bleiben sind kein Problem, wenn man ein paar Gigabyte Arbeitsspeicher hat. Und normalerweise sind Funktionen ja nicht besonders oft ineinander verschachtelt. Es wird also nicht viel Speicher verschwendet.

Ich vermute mal dieser wird mit einem Aufruf von ret vom Stack gelöscht und so steht mein ESP auf dem ersten Parameter (?)
So ist es.

Bei einem erneuten Funktionsaufruf (nachdem meine Funktion das erste mal durchlaufen wurde) würde ich ja nun erneut Speicher auf dem Stack reservieren (sub esp, 0x10) und diesen wieder mit Parametern füllen. Aber wie kommen dann die Parameter vom ersten Funktionsaufruf wieder aus meinem Speicher raus? Die blockieren ja quasi den Speicher.
Nachdem die Funktion _fn zurückgekehrt ist, werden die Parameter nicht mehr benötigt. Der Bereich auf dem Stack ist dann ungenutzt. Wenn die Funktion ein zweites mal aufgerufen wird, werden die selben 16 Bytes wieder für die Parameter verwendet. Am Ende der Funktion _main wird der Speicherbereich auf dem Stack durch den LEAVE-Befehl freigegeben. Der setzt ESP auf den alten Wert, der den vorher durch mov ebp,esp gesichert wurde.
« Letzte Änderung: 08. February 2013, 00:54 von Jidder »
Dieser Text wird unter jedem Beitrag angezeigt.

Meuchelfix77

  • Beiträge: 4
    • Profil anzeigen
Gespeichert
« Antwort #8 am: 09. February 2013, 14:13 »
Okay. Vielen Dank! ;)
Ich glaube jz habe ich es endlich auch begriffen! :)

BTW: Du hast momentan 1337 Forumsbeiträge! :P

 

Einloggen