Assembler: Fehlerhafter Stackframe

Pascal, Basic und andere nicht aufgelistete
Benutzeravatar
Architekt
Beiträge: 172
Registriert: Sa Mai 24, 2014 12:04 pm

Assembler: Fehlerhafter Stackframe

Beitrag von Architekt » Fr Jun 13, 2014 3:43 pm

Hallo zusammen.
Nach Abschluss meiner BA wollte ich mich nun der direkten Kompilierung einer Sprache in Assembler widmen.
Jedoch erhalte ich einen Fehler und kann ihn mir bei partout nicht erklären, ihr vllt?

Dieser Code (2 Variablen) funktioniert:

Code: Alles auswählen

.text
.globl _prog

_prog:

subl $4, %esp
movl $4, %eax
movl %eax, -4(%ebp)

subl $4, %esp
movl $8, %eax
movl %eax, -8(%ebp)

addl $8, %esp
ret
Dieser jedoch (3 Variablen) nicht. Es kompiliert fehlerfrei, jedoch crasht es zur Laufzeit:

Code: Alles auswählen

.text
.globl _prog

_prog:

subl $4, %esp
movl $4, %eax
movl %eax, -4(%ebp)

subl $4, %esp
movl $8, %eax
movl %eax, -8(%ebp)

subl $4, %esp
movl $12, %eax
movl %eax, -12(%ebp)

addl $12, %esp
ret

Wie man sehen kann, fordere ich Speicher an und move die Variable genau wie die anderen beiden vorherigen. Woran kann der Fehler liegen? Habe ich zuwenig Speicher auf dem Stack? Und wenn ja, wieso?
Danke im voraus.

edit:
Das "runtime" C Program sieht im übrigen so aus:

Code: Alles auswählen

#include <stdio.h>

void prog();

int main() {
	prog();
}

void print_int(int n) {
	printf("%d\n", n);
}
Und kompiliert wird ganz normal per

Code: Alles auswählen

 gcc -o out.exe out.S rt.c

EDIT dani93: Titel war "Fehlerhaftes Assembler"

Benutzeravatar
Architekt
Beiträge: 172
Registriert: Sa Mai 24, 2014 12:04 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von Architekt » Fr Jun 13, 2014 6:07 pm

Ich bin der Meinung, dass das Problem mit ​

Code: Alles auswählen

addl $12, %esp
zusammenhängt. Ohne diesen Teil wird das Programm zwar nie beendet (läuft scheinbar ewig) crasht jedoch nicht.
Addiere ich natürlich mehr oder weniger als 12 Byte (also das, was ich auch angefordert habe) crasht es (natürlich) auch.
Ich bin leider echt zu wenig mit Assembler vertraut, als das ich wüsste, woran es liegt. :/

nufan
Wiki-Moderator
Beiträge: 2557
Registriert: Sa Jul 05, 2008 3:21 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von nufan » Fr Jun 13, 2014 9:01 pm

Dein Problem ist, dass du den Stackframe deiner Funktion falsch erstellst bzw. zerstörst.

Jede Funktion hat eine Startadresse, an der ihr Speicher beginnt. Wenn du eine Funktion aufrufst, erwartet der Aufrufer, dass du diese Startadresse unverändert lässt. Willst du sie also ändern, musst du sie innerhalb deiner Funktion speichern und am Ende wieder auf den ursprünglichen Wert zurücksetzen. Diese Basis-Adresse befindet sich immer im Register %ebp (bzw. %rbp auf 64 bit Systemen).
Du schreibst allerdings ohne jede Vorbereitung auf folgende Adressen:

Code: Alles auswählen

movl %eax, -4(%ebp)
movl %eax, -8(%ebp)
movl %eax, -12(%ebp)
hast aber dein %ebp davor nicht für deine Funktion initialisiert. Du schreibst also praktisch in den Speicherbereich deines Aufrufers, also main!

So, nun ist klar, dass du dein %ebp richtig setzen musst. Nur welchen Wert soll %ebp bekommen? Du schreibst alle deine Werte auf den Stack, also sollte auch deine Startadresse dort hin zeigen. Deine Funktion besteht grob gesagt aus den folgenden Schritten:
* 1: %ebp des Aufrufers speichern
* 2: %ebp auf den Speicherbereich deiner Funktion setzen. Die Obergrenze deines Stacks befindet sich in %esp, also können wir das einfach kopieren.
* 3: %esp um den benötigten Speicherbereich vermindern.
* 4: blabla (Die eigentliche Funktionalität deiner Funktion)
* 5: %esp um den benötigten Speicherbereich erhöhen.
* 6: %ebp des Aufrufers wiederherstellen.
* 7: Kontrolle an den Aufrufer zurückliefern.

In deinen Code gegossen schaut dies so aus:

Code: Alles auswählen

.text
.globl prog

prog:
    push %ebp /* 1 */
    mov %esp, %ebp /* 2 */
    sub $12, %esp /* 3 */

    /* 4 */
    mov $4, %eax
    mov %eax, 0(%esp)

    mov $8, %eax
    mov %eax, 4(%esp)

    mov $12, %eax
    mov %eax, 8(%esp)

    add $12, %esp /* 5 */
    pop %ebp /* 6 */
    ret /* 7 */

Ich hab dein Programm noch etwas erweitert, um die Funktionalität des Stackframes zu testen. Ich rufe dazu eine (C-)Funktion auf, die drei ints als Parameter bekommt und auf die Kommandozeile schreibt, sowie eine andere Funktion innerhalb von main um den korrekten Abbau des Stackframes zu prüfen:

rt.c:

Code: Alles auswählen

#include <stdio.h>

extern void prog();
void print_int(int n);
void print_3_ints(int a, int b, int c);

int main() {
    prog();
    print_int(42);
}

void print_int(int n) {
    printf("%d\n", n);
}

void print_3_ints(int a, int b, int c) {
    printf("3 ints: %d %d %d\n", a, b, c);
}
out.S:

Code: Alles auswählen

.text
.extern print_3_ints   /* Es gibt "irgendwo" diese Funktion, sie wird erst vom Linker genau spezifziert. */
.globl prog

prog:
    push %ebp
    mov %esp, %ebp
    sub $12, %esp

    mov $4, %eax
    mov %eax, 0(%esp)

    mov $8, %eax
    mov %eax, 4(%esp)

    mov $12, %eax
    mov %eax, 8(%esp)

    call print_3_ints    /* Aufruf an die neue C-Funktion */

    add $12, %esp
    pop %ebp
    ret
Ausgabe:

Code: Alles auswählen

3 ints: 4 8 12
42
Man sieht hier auch sehr schön, dass man die Parameter in umgekehrter Reihenfolge auf den Stack legen muss. Das oberste Element wird logischerweise als erstes von der aufgerufene Funktion vom Stack genommen.

Für jemanden der das ganze selbst bauen und ausführen will:
Ich arbeite hier auf einem 64bit Linux-System. 64bit Register haben ein 'r'-Präfix anstatt 'e'. Das heißt statt eax, esp, ebp etc. verwendet man rax, rsp und rbp. Die kleineren Register existieren nur mehr als genauere Adressierung der größeren (rax hat ein eax, eax hat ein ax, ax hat ein al und ah usw). Um Probleme mit dem gegebenen Code zu umgehen, sage ich dem Assembler/Compiler/Linker explizit mir 32bit Binarys zu erstellen.
* Assembler-Code assemblieren:

Code: Alles auswählen

as -32 -o out.o out.S
* C-Code compilieren:

Code: Alles auswählen

gcc -m32 -c rt.c
* Executable linken (du hast oben .exe stehen, das verwirrte mich etwas?!):

Code: Alles auswählen

gcc -m32 -o out out.o rt.o
* Ausführen:

Code: Alles auswählen

./out
Das wars, noch Fragen? :)

Ich hab mir auch erlaubt den Titel des Threads ein wenig ausführlicher zu gestalten.

Benutzeravatar
Architekt
Beiträge: 172
Registriert: Sa Mai 24, 2014 12:04 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von Architekt » Fr Jun 13, 2014 9:20 pm

Wow, das nenne ich ausführliche und hilfreiche Antwort. :)
Danke schön, ich denke damit kann ich doch mal etwas anfangen. :D
Ich werde das mal ausprobieren und mich dann zurückmelden.

Benutzeravatar
Architekt
Beiträge: 172
Registriert: Sa Mai 24, 2014 12:04 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von Architekt » Fr Jun 13, 2014 11:06 pm

Ich hätte da noch eine Frage:
Wenn wir mit 'ret' zum Aufrufer zurückkehren, welche Funktion hat dann 'leave'?

nufan
Wiki-Moderator
Beiträge: 2557
Registriert: Sa Jul 05, 2008 3:21 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von nufan » Fr Jun 13, 2014 11:20 pm

Architekt hat geschrieben:Wenn wir mit 'ret' zum Aufrufer zurückkehren, welche Funktion hat dann 'leave'?
leave zerstört deinen Stackframe. Also genau das was du hier "händisch" machst:

Code: Alles auswählen

    add $12, %esp /* 5 */
    pop %ebp /* 6 */
kannst du auch einfach als leave schreiben:

Code: Alles auswählen

.text
.extern print_3_ints
.globl prog

prog:
    push %ebp
    mov %esp, %ebp
    sub $12, %esp

    mov $4, %eax
    mov %eax, 0(%esp)

    mov $8, %eax
    mov %eax, 4(%esp)

    mov $12, %eax
    mov %eax, 8(%esp)

    call print_3_ints

    leave /* 5, 6 */
    ret
Die Funktionalität ist äquivalent zur vorigen Version von mir.

Benutzeravatar
Architekt
Beiträge: 172
Registriert: Sa Mai 24, 2014 12:04 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von Architekt » So Jun 15, 2014 12:19 pm

Du hats mir schon enorm weitergeholfen, herzlichen Dank dafür.

Könntest du nochmal über diesen Code schauen? Er funktioniert tadellos, allerdings würde ich gerne wissen, ob ich etwas übersehen habe oder ob irgendwas redundant ist.

Code: Alles auswählen

.globl _prog

_prog:
push	%ebp
movl	%esp, %ebp

subl	$8, %esp # reserve

movl	$4, %eax # 4
movl	%eax, (%esp) # a = 4

movl	(%esp), %eax # param : a(4)
push	%eax
call	_print_int # print param : a(4)
addl	$4, %esp

movl	(%esp), %eax # param : a(4)
push	%eax
movl	$2, %eax # 2
imull	4(%esp), %eax # * 2
addl	$4, %esp
movl	%eax, 4(%esp) # b = a * 2 = 8

movl	4(%esp), %eax # param : b(8)
push	%eax
call	_print_int # print param : b(8)
addl	$4, %esp

addl	$8, %esp # release

pop 	%ebp
ret
Was mich speziell interessieren würde (was ich bisher einfach hingenommen habe): warum muss nach Operationen wie oder

Code: Alles auswählen

imull
ein

Code: Alles auswählen

addl	$4, %esp
?
Und ist es richtig, das ich nur 8 Byte auf dem Stack reserviere (

Code: Alles auswählen

subl	$8, %esp # reserve
), nämlich nur für die Variablen? Oder sollte man auch für jeden Push (die prints) Speicher reservieren?

Danke!

Der Code der "Sprache" sieht wie folgt aus:

Code: Alles auswählen

print 1 + 2 * 3 # 7
print 3 * 4 / 2 # 6

print (1 + 2) * 3 # 9
print 3 * (4 / 2) # 6

var a
a = 4

print a # 4

var b = a * 2 # Comment test

print b # 8

nufan
Wiki-Moderator
Beiträge: 2557
Registriert: Sa Jul 05, 2008 3:21 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von nufan » So Jun 15, 2014 2:05 pm

Architekt hat geschrieben:Könntest du nochmal über diesen Code schauen? Er funktioniert tadellos, allerdings würde ich gerne wissen, ob ich etwas übersehen habe oder ob irgendwas redundant ist.
Hm du hast da einige Redundanzen beim pushen der Funktions-Parameter. Siehe unten.
Architekt hat geschrieben:Was mich speziell interessieren würde (was ich bisher einfach hingenommen habe): warum muss nach Operationen wie oder

Code: Alles auswählen

imull
ein

Code: Alles auswählen

addl	$4, %esp
?
Damit "löscht" du deinen mit mov kopierten Parameter wieder vom Stack. Du könntest genauso gut ein pop in ein Register machen und den Wert verwerfen, kommt aufs Gleiche.

Wenn du dir den Assembler-Code deiner C-Funktion ansiehst, wirst du sehen, dass die Parameter nicht vom Stack gepopt werden, sondern lediglich relativ zu %esp addressiert werden:

Code: Alles auswählen

(gdb) disas _print_int
Dump of assembler code for function _print_int:
   0x08048460 <+0>:     push   %ebp
   0x08048461 <+1>:     mov    %esp,%ebp
   0x08048463 <+3>:     sub    $0x18,%esp
   0x08048466 <+6>:     mov    0x8(%ebp),%eax
   0x08048469 <+9>:     mov    %eax,0x4(%esp)
   0x0804846d <+13>:    movl   $0x8048540,(%esp)
   0x08048474 <+20>:    call   0x80482f0 <printf@plt>
   0x08048479 <+25>:    leave  
   0x0804847a <+26>:    ret
Irgendwie musst du die also wieder vom Stack runterbekommen.
Architekt hat geschrieben:Und ist es richtig, das ich nur 8 Byte auf dem Stack reserviere (

Code: Alles auswählen

subl   $8, %esp # reserve
), nämlich nur für die Variablen? Oder sollte man auch für jeden Push (die prints) Speicher reservieren?
Nein, das stimmt schon so. Push korrigiert dein esp automatisch, du brauchst dich also nicht darum kümmern. Allerdings sind deine push-Aufrufe redundant.
Das ist effektiv ein push von eax, wenn du zuvor Speicher reserviert hast:

Code: Alles auswählen

movl   %eax, (%esp)
Das heißt heißt mit:

Code: Alles auswählen

    movl   %eax, (%esp) # a = 4
    movl   (%esp), %eax # param : a(4)
    push   %eax
legst du 2 mal den Wert 4 auf den Stack.
Wenn du auf dein esp-Register aufpasst, kannst du beide Methoden mischen. Push hat den Vorteil, dass du die größe deines Speichers im Voraus nicht kennen musst. Das ist vor allem praktisch, weil du reine Übergabeparameter nicht in die Speichergröße mit einrechnen musst. Allerdings ist es mit der mov-Variante leichter lokale Variablen zu verwenden, wenn du sie immer mit dem Offset kennzeichnest.
Ich bin mir nicht ganz sicher was von deinem Code hier Stack-Spielereien zum Testen sind und was unbeabsichtigt so ist. Wenn du deine Ergebnisse a und b innerhalb der Funktion am Stack haben willst, würde ich das ganze so schreiben:

Code: Alles auswählen

.globl _prog

_prog:
    push %ebp
    movl %esp, %ebp

    # Platz für die Variablen a und b reservieren
    subl $8, %esp

    # a = 4 auf den Stack legen
    movl $4, (%esp)

    # a für die Ausgabe nochmal auf den Stack legen
    push (%esp)
    # a ausgeben
    call _print_int
    # a wieder vom Stack löschen
    addl $4, %esp

    # b = a * 2
    movl $2, %eax
    imull (%esp), %eax
    # b auf den Stack legen
    movl %eax, 4(%esp)

    # b für die Ausgabe nochmal auf den Stack legen
    push 4(%esp)
    # b ausgeben
    call _print_int
    # b wieder vom Stack nehmen
    addl $4, %esp
    
    # a und b liegen noch immer jeweils 1 mal am Stack!

    addl $8, %esp
    pop %ebp
    ret
Willst du effizienten Assembler-Code, würde ich das ganze so schreiben:

Code: Alles auswählen

.globl _prog

_prog:
    push %ebp
    movl %esp, %ebp
    subl $4, %esp

    # a = 4 auf den Stack legen und ausgeben
    movl $4, (%esp)
    call _print_int

    # b = 2 * 4 auf den Stack legen und ausgeben
    movl $4, %eax
    movl $2, %ebx
    imull %ebx, %eax
    movl %eax, (%esp)
    call _print_int

    addl $4, %esp
    pop %ebp
    ret
Und am effizientesten ist es, wenn du _print_int auch in Assembler implementierst und auf 64bit wechselst, da du dann einige Annahmen treffen kannst und den Stack gar nicht mehr benötigst:

Code: Alles auswählen

.globl _prog, _print_int
_prog:
    mov $4, %rsi
    call _print_int

    mov $4, %rsi
    mov $2, %rbx
    imul %rbx, %rsi
    call _print_int

    ret


_print_int:
    mov $format, %rdi
    call printf
    ret


format:
    .asciz  "%d\n"
Das ist übrigens sowieso offensichtlich redundant:

Code: Alles auswählen

movl   %eax, (%esp) # a = 4
movl   (%esp), %eax # param : a(4)
^^

Ich freu mich schon auf meine BA, dann hab ich mal Zeit mich mit spannenden Dingen zu beschäftigen :D

Benutzeravatar
Architekt
Beiträge: 172
Registriert: Sa Mai 24, 2014 12:04 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von Architekt » So Jun 15, 2014 6:29 pm

Okay, das ist heftiger Input auf einmal. :D
Ich werde mal sehen was ich verstehen und umsetzen kann, Danke.

Eine Frage vorweg:
Heißt das also, dass

Code: Alles auswählen

addl	$4, %esp
und

Code: Alles auswählen

pop	%eax
äquivalent sind oder das letzteres sogar besser/angebrachter ist?

nufan
Wiki-Moderator
Beiträge: 2557
Registriert: Sa Jul 05, 2008 3:21 pm

Re: Assembler: Fehlerhafter Stackframe

Beitrag von nufan » So Jun 15, 2014 6:38 pm

Architekt hat geschrieben:Okay, das ist heftiger Input auf einmal. :D
Irgendwas davon nicht klar?
Architekt hat geschrieben:Eine Frage vorweg:
Heißt das also, dass

Code: Alles auswählen

addl	$4, %esp
und

Code: Alles auswählen

pop	%eax
äquivalent sind oder das letzteres sogar besser/angebrachter ist?
Das kommt auf den Anwendungsfall drauf an:
* Mit addl verwirfst du den Wert am Stack, du müllst dir also nicht unnötig ein Register zu.
* Pop speichert deinen Wert im Register und korrigiert esp automatisch.
Also prinzipiell: Wenn du den Wert am Stack in einem Register haben willst pop, ansonsten addl.

Antworten