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 ebx
10), 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 x86 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 ebx
10. 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).
Ja, es ist redundant. Aber das ist nicht das Einzige. Der gesamte Aufbau des Stack-Frames ist in diesem Fall nicht notwendig. – Jester
@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
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