2016-07-29 10 views
2

Ich versuche, mit dem Autor meines Buches zu folgen, und er gibt uns die Beispielfunktion mit einem Prolog und Epilog (keine lokalen Variablen in der Funktion)Funktion Epilog in der Montage

1: push ebp 
2: mov ebp, esp 
3: ... 
4: movsx eax, word ptr [ebp+8] 
5: movsx eax, word ptr [ebp+0Ch] 
6: add eax, ecx 
7: ... 
8: mov esp, ebp 
9: pop ebp 
10: retn 

die aufgerufen wird, durch

push eax  ; param 2 
push ecx  ; param 1 
call addme 
add esp, 8 ; cleanup stack 

In diesem Beispiel ist die Leitung 8 kein redundantes Anweisung? Ich meine, ist EBP in diesem Zusammenhang nicht schon gleich ESP? Nichts war PUSH oder POP im Stapel seit.

Meine Annahme ist, dass diese Zeile nur notwendig wäre, wenn wir lokale Variablen hätten, die auf den Stack geschoben würden, und dies wäre eine Methode, um den Stapel dieser lokalen Variablen zu löschen?

Ich möchte nur klarstellen, dass dies der Fall ist

+3

Ja, es ist redundant. Aber das ist nicht das Einzige. Der gesamte Aufbau des Stack-Frames ist in diesem Fall nicht notwendig. – Jester

+0

@Jester Das stimmt. Ich nahm an, dass er einen grundlegenden Überblick darüber gab, wie wir einen Stapelrahmen aufbauen würden. Ich habe mir gerade die Haare ausgerissen, um herauszufinden, warum wir die Anweisung von Zeile 8 haben, wenn die Beispielfunktion ohne lokale Variablen eingerichtet ist. Vielen Dank! – X33

+0

das entspricht dem Anfangs- und Endblock in C, obwohl es keine lokalen Variablen gibt, ist es immer noch da. Der Stapel muss in der richtigen Reihenfolge gehalten werden, nachdem eine Funktion ihn benutzt hat. – rcd

Antwort

3

Sie richtig sind, ist es überflüssig wenn Sie wissen, dass esp bereits an der Stelle zeigt, wo Sie Ihre Anrufer ebp geschoben.


Wenn gcc eine Funktion mit -fno-omit-frame-pointer kompiliert, ist es in der Tat die Optimierung Sie vorschlagen, nur ebp knallen, wenn es weiß, dass esp bereits an der richtigen Stelle zeigt.

Dies ist sehr häufig in Funktionen, die Aufrufe beibehalten Register (wie ebx10), die auch gespeichert/wiederhergestellt werden müssen wie ebp. Compiler führen normalerweise alle Sicherungen/Wiederherstellungen im Prolog/Epilog durch, bevor irgendetwas Platz für ein C99-Array variabler Größe reserviert. So wird pop ebx immer esp auf den richtigen Platz für pop ebp zeigen lassen.

z.B. 3.8 der Ausgang (mit -O3 -m32) für diese Funktion, auf der Godbolt compiler explorer. Wie üblich ist, Compiler nicht ganz optimalen Code machen:

void extint(int); // a function that can't inline because the compiler can't see the definition. 
int save_reg_framepointer(int a){ 
    extint(a); 
    return a; 
} 

    # clang3.8 
    push ebp 
    mov  ebp, esp      # stack-frame boilerplate 
    push esi       # save a call-preserved reg 
    push eax       # align the stack to 16B 
    mov  esi, dword ptr [ebp + 8]  # load `a` into a register that will survive the function call. 
    mov  dword ptr [esp], esi   # store the arg for extint. Doing this with an ebp-relative address would have been slightly more efficient, but just push esi here instead of push eax earlier would make even more sense 
    call extint 
    mov  eax, esi      # return value 
    add  esp, 4      # pop the arg 
    pop  esi       # restore esi 
    pop  ebp       # restore ebp. Notice the lack of a mov esp, ebp here, or even a lea esp, [ebp-4] before the first pop. 
    ret 

Natürlich ein Mensch (ein Trick von gcc zu leihen)

# hand-written based on tricks from gcc and clang, and avoiding their suckage 
call_non_inline_and_return_arg: 
    push ebp 
    mov  ebp, esp      # stack-frame boilerplate if we have to. 
    push esi       # save a call-preserved reg 
    mov  esi, dword [ebp + 8]   # load `a` into a register that will survive the function call 
    push esi       # replacing push eax/mov 
    call extint 
    mov  eax, esi      # return value. Could mov eax, [ebp+8] 
    mov  esi, [ebp-4]     # restore esi without a pop, since we know where we put it, and esp isn't pointing there. 
    leave        # same as mov esp, ebp/pop ebp. 3 uops on recent Intel CPUs 
    ret 

Da der Stapel von 16, bevor ein ausgerichtet werden muss, call (nach den Regeln des SystemV i386 ABI, siehe Links in der Tag Wiki), können wir auch eine extra reg, statt nur push [ebp+8] und dann (nach dem Anruf) mov eax, [ebp+8] speichern/wiederherstellen. Compiler bevorzugen das Speichern/Wiederherstellen von rufkonservierten Registern, um lokale Daten mehrfach neu zu laden.

Wenn nicht für die Stack-Ausrichtungsregeln in der aktuellen Version des ABIS, könnte ich schreiben:

# hand-written: esp alignment not preserved on the call 
call_no_stack_align: 
    push ebp 
    mov  ebp, esp      # stack-frame boilerplate if we have to. 
    push dword [ebp + 8]    # function arg. 2 uops for push with a memory operand 
    call extint      # esp is offset by 12 from before the `call` that called us: return address, ebp, and function arg. 
    mov  eax, [ebp+8]     # return value, which extint won't have modified because it only takes one arg 
    leave        # same as mov esp, ebp/pop ebp. 3 uops on recent Intel CPUs 
    ret 

gcc leave statt mov/pop, tatsächlich in Fällen verwenden, wo es ändern muss, um esp vor dem Knallen ebx10. Zum Beispiel flip Godbolt to gcc (instead of clang), and take out -m32, also kompilieren wir für x86-64 (wo Argumente in Registern übergeben werden). Das bedeutet, dass Sie nach einem Anruf keine Argumente mehr aus dem Stapel löschen müssen, sodass rsp richtig eingestellt ist, um nur zwei Regs zu öffnen. (Push/pop Einsatz 8 Bytes des Stapels, aber rsp noch 16B ausgerichtet werden, bevor ein call im SysV AMD64 ABI, so gcc hat tatsächlich eine sub rsp, 8 und entsprechende add um die call.)

Eine weitere Optimierung verpasst: mit gcc -m32 verwendet die Variable-Array-Array-Funktion eine add esp, 16/leave nach dem Aufruf. Die add ist völlig nutzlos. (Fügen Sie -m32 zu den gcc-Argumenten auf godbolt hinzu).

3

Sie wissen nicht, was auf den Linien 3 und 7 Also gehe ich davon aus Leitung 8 im allgemeinen Fall nicht redundant ist. Normalerweise sollte es ohne diese Zeile 8 funktionieren, da der Wert von ESP am Ende Ihrer Funktion normalerweise derselbe wie am Anfang Ihrer Funktion ist. Aber ich kann mir einige schmutzige Szenarien vorstellen, in denen Zeile 8 benutzt wird, um etwas zu bereinigen, wie zum Beispiel wenn Sie eine Sequenz mit Push-Call aufrufen und die letzte ADD ESP Zeile weglassen. Dann können Sie einfach MOV ESP, EBP verwenden, um das ESP am Ende Ihrer Funktion zu reparieren. Schmutzig aber funktioniert.