Content

Call und Ret

Am Tag 2 haben wir einige Assembler Befehle kennen und verstehen gelernt. Ein paar Dinge müssen wir aber noch genauer anschauen, um sie wirklich zu verstehen:

Zweites Beispiel

unsigned a = 6;

void
f()
{
    a = a + a;
}

void
g()
{
    f();
    f();
}

Mit

$shell> gcc-4.8 -S -fno-asynchronous-unwind-tables second.c                    

erzeugen wir den Assembler Code

                .globl _a
                .data
                .align 2
_a:
                .long           6
                .text
                .globl _f
_f:
                pushq           %rbp
                movq            %rsp, %rbp
                movl            _a(%rip), %edx
                movl            _a(%rip), %eax
                addl            %edx, %eax
                movl            %eax_a(%rip)
                popq            %rbp
                ret
                .globl _g
_g:
                pushq           %rbp
                movq            %rsp, %rbp
                movl            $0, %eax
                call            _f
                movl            $0, %eax
                call            _f
                popq            %rbp
                ret
                .subsections_via_symbols

Was tut Call und Ret?

Beide Assembler Befehle ändern das %rip Register. Damit wird als nicht der im Text-Segment folgende Maschinenbefehl ausgeführt sondern es wird gesprungen. Stellt sich also die Frage wieso man nicht einfach den jmp Befehl benutzt.

Den call Befehl könnte man tatsächlich mit einer jmp Anweisung ersetzten. Die Adresse des ersten Befehls einer Funktion kennt man ja nach dem Linken. Wenn die Funktion abgearbeitet ist muss man aber zurückspringen und zwar zu dem Maschinenbefehl der dem Funktionsaufruf folgt. Da die Funktion mehrfach aufgerufen werden kann ist diese Adresse aber nicht fix. Für jeden Funktionsaufruf muss man also eine Rücksprungadresse speichern.

Stellt sich die Frage wo man die Rücksprungadresse speichern soll. Man kann einen festen Platz im Speicher ausmachen. Das geht gut solange nur eine Funktion aufgerufen wird. Ein zweiter Funktionsaufruf dürfte nur erfolgen, wenn man mit dem ersten Aufruf “fertig” ist, d.h. die Funktion abgearbeitet ist und zurückgesprungen wurde.

Für jeden Funktionsaufruf muss man also einen neuen Platz für die Rücksprung Adresse festlegen. Wenn viele Funktionsaufrufe stattfinden, dann stapelt sich da was auf. Und da sind wir schon bei der Lösung. Die Rücksprungadressen werden einfach auf einem Stack gespeichert. Mit jedem Funktionsaufruf wird der Stack größer. Ganz oben auf dem Stack ist dann die Rücksprung Adresse für den letzten Funktionsaufruf. Darunter die für den vorletzten usw.