blog · git · desktop · images · contact & privacy · gopher


Assembler: Calling Conventions und Syscalls bei 32 und 64 Bit

Ein Funktionsaufruf in C sieht nicht spektakulär aus:

retval = foo(arg, bla, blubb);

Auf Assembler-Ebene gibt es zwar das Instruktionspaar „call“ und „ret“, mit denen man komfortabel in eine andere Stelle im Code und zurück springen kann. Aber einen festen Mechanismus, wie man dabei Parameter übergeben kann, gibt es nicht. Deswegen muss man sich auf einen sinnvollen Weg einigen – und das regeln die Calling Conventions.

Diese Konvention unterscheidet sich nicht nur von Betriebssystem zu Betriebssystem, sondern bei GNU/Linux auch zwischen 32 und 64 Bit. Zusätzlich muss man unterscheiden, ob es sich um einen Syscall handelt, ich also direkt mit dem Kernel spreche, oder ob es einfach nur ein Codesprung im Userland ist. Für ersteres gibt der Kernel ein Interface vor und ein Syscall ist auch etwas grundsätzlich anderes als ein Funktionsaufruf – auch wenn im highlevel C-Code beide Dinge gleich wirken mögen (Fußnote: Es sieht nur an der Oberfläche gleich aus, weil die libc alles kapselt). Im Userland könnte ich aber in meinem eigenen Code theoretisch machen, was ich möchte. Spätestens dann, wenn mein Code mit dem anderer Leute interagieren soll (prominentes Beispiel libc), muss ich mich aber auch hier an die Konvention halten.

Im Folgenden jeweils für 32 und 64 Bit die Calling Conventions für das Userland und der Syscall-Mechanismus. Andere Systeme als GNU/Linux werden nicht betrachtet. Gedacht unter anderem als Merkzettel für mich.

Alle folgenden Beispiele habe ich in diesem Archiv zusammengefasst.

32 Bit

Userland

Ich habe dazu früher schonmal etwas geschrieben. Der Übersichtlichkeit zuliebe hier eine kurze Wiederholung.

Die Argumente für die aufzurufende Funktion werden in „umgekehrter“ Reihenfolge auf dem Stack abgelegt. Der Rückgabewert steht im Register EAX, eventuell wird noch EDX zuhilfe genommen, wenn EAX nicht ausreicht. In EDX steht dann der obere Teil.

Bild des Stacks für die Parameterübergabe:

:         :                        :
:         :                        :
|  Arg 2  |  <--   16(%ebp)        |   vorheriger
|  Arg 1  |  <--   12(%ebp)        |     Frame
|  Arg 0  |  <--    8(%ebp)        /
+---------+
| Rücksp. |                        \
| EBP alt |  <-- EBP               |
|  Var 0  |  <--   -4(%ebp)        |
|  Var 1  |  <--   -8(%ebp)        |   aktueller
|  Var 2  |  <--  -12(%ebp)        |     Frame
|  Var 3  |  <--  -16(%ebp)        |
:         :                        |
:         :                        |
| letzte  |  <-- ESP               /
+---------+

Künstlich verkomplizierter Beispielcode „cc-32.s“, damit die relevanten Stellen deutlich sichtbar werden:

.section .data

fmt:
    .string "Rückgabe: %d\n\0"

.section .text
.global main

main:
    pushl %ebp                     /* Frame Pointer aufbauen */
    movl %esp, %ebp

    subl $16, %esp                 /* Platz auf dem Stack schaffen */

    /* ---------------- Aufruf der eigenen Funktion ----------------- */

                                   /*  :           :                  */
                                   /*  :           :                  */
                                   /*  +-----------+                  */
                                   /*  | Rücksp.   |                  */
                                   /*  | EBP alt 1 |  <--     (%ebp)  */
                                   /*  +-----------+                  */
    movl $4,  -4(%ebp)             /*  |  4        |  <--   -4(%ebp)  */
    movl $3,  -8(%ebp)             /*  |  3        |  <--   -8(%ebp)  */
    movl $2, -12(%ebp)             /*  |  2        |  <--  -12(%ebp)  */
    movl $1, -16(%ebp)             /*  |  1        |  <--  -16(%ebp)  */
    call foo                       /* =#===========#================= */
                                   /*  :           :                  */
                                   /*  :           :                  */

                                   /* Rückgabewert --> %eax */

    /* ----------------- Ausgabe des Rückgabewerts ------------------ */

                                   /*  :           :                  */
                                   /*  :           :                  */
                                   /*  +-----------+                  */
                                   /*  | Rücksp.   |                  */
                                   /*  | EBP alt 1 |  <--     (%ebp)  */
                                   /*  +-----------+                  */
                                   /*  |  4        |  <--   -4(%ebp)  */
                                   /*  |  3        |  <--   -8(%ebp)  */
    movl %eax, -12(%ebp)           /*  |  -2       |  <--  -12(%ebp)  */
    movl $fmt, -16(%ebp)           /*  |  $fmt     |  <--  -16(%ebp)  */
    call printf                    /* =#===========#================= */
                                   /*  :           :                  */
                                   /*  :           :                  */

    /* ------------------ Ende des Hauptprogramms ------------------- */

    movl $0, %eax                  /* return 0 */
    leave                          /* Framepointer abbauen */
    ret                            /* Rücksprung aus main() */

foo:
    pushl %ebp
    movl %esp, %ebp

    /* Berechne hier:

        a = 1 - 2;
        b = 3 - 4;
        return a + b;

    */

    subl $8, %esp

                                   /*  :           :                  */
                                   /*  :           :                  */
                                   /*  +-----------+                  */
                                   /*  | Rücksp.   |                  */
    /* a = 1 - 2 */                /*  | EBP alt 1 |                  */
    movl  8(%ebp), %ecx            /*  +-----------+                  */
    subl 12(%ebp), %ecx            /*  |  4        |  <-- 20(%ebp)    */
    movl %ecx, -4(%ebp)            /*  |  3        |  <-- 16(%ebp)    */
                                   /*  |  2        |  <-- 12(%ebp)    */
    /* b = 3 - 4 */                /*  |  1        |  <--  8(%ebp)    */
    movl 16(%ebp), %ecx            /* =#===========#================= */
    subl 20(%ebp), %ecx            /*  | Rücksp.   |                  */
    movl %ecx, -8(%ebp)            /*  | EBP alt 2 |  <--   (%ebp)    */
                                   /*  +-----------+                  */
    /* a + b */                    /*  |  a        |  <-- -4(%ebp)    */
    movl -4(%ebp), %ecx            /*  |  b        |  <-- -8(%ebp)    */
    addl -8(%ebp), %ecx            /* =#===========#================= */
                                   /*  :           :                  */
                                   /*  :           :                  */

    movl %ecx, %eax                /* Rückgabewert --> %eax */

    leave
    ret

Bei Arch Linux muss „multilib-devel“ installiert sein, damit 32 Bit-Code auf einer 64 Bit-Maschine übersetzt werden kann. Zusätzlich muss dann „-m32“ hier angegeben werden:

gcc -m32 -Wall -Wextra -g -o cc-32 cc-32.s

Syscalls

Für Syscalls gibt es zwei Möglichkeiten: Interrupt 80 und SYSENTER/SYSEXIT. Die zweite Möglichkeit ist einen eigenen Blogpost wert, daher hier „nur“ Interrupt 80, was in der Funktion nicht begrenzt, aber langsamer ist (nicht, dass man das bei einfachem Code merken würde).

In EAX wird die Nummer des Syscalls geschrieben. In die Register EBX, ECX, EDX, ESI, EDI kommen die Argumente. Sollten diese Register nicht ausreichen (wie beispielsweise bei „mmap()“), dann wird in EBX ein Pointer auf ein Struct übergeben.

Die Argumente erfährt man ganz gewöhnlich über die Manpages (zum Beispiel „man 2 write“) oder über diese nützliche Seite. Die Nummer des Syscalls steht dort auch oder, falls man mal kein Internet hat, in „/usr/include/asm/unistd_32.h“.

„Hallo Welt!“-Beispiel „sc-32.s“ ohne Frame Pointer:

.section .data

msg:
    .string "Hallo, Welt!\n"

.section .text
.global _start

_start:
    movl $4, %eax         /* 4 = write() */
    movl $1, %ebx         /* 1 für stdout */
    movl $msg, %ecx       /* Pointer auf den String */
    movl $13, %edx        /* Länge des Strings */
    int $0x80             /* Interrupt auslösen */

    movl $1, %eax         /* 1 = _exit() */
    movl $0, %ebx         /* Rückgabewert 0 */
    int $0x80

Übersetzen mit „as“ und „ld“ oder hiermit:

gcc -nostdlib -m32 -Wall -Wextra -g -o sc-32 sc-32.s

64 Bit

Userland

Auf x86_64 wird mehr Gebrauch von den üppiger vorhandenen Registern gemacht. Argument 0 in RDI, Argument 1 in RSI, dann RDX, RCX, R8, R9. Gibt es noch mehr Argumente, dann werden diese zusätzlich wie bei 32 Bit über den Stack übergeben. Den Rückgabewert gibt es in RAX und falls nötig noch RDX.

.section .data

msg:
    .string "Hallo, Welt!\0"

fmt:
    .string "Formatiert: 0x%X, %c, '%s', %d, %d; %d, %d, %d\n\0"

fmtret:
    .string "Rückgabe: %d\n\0"

.section .text
.global main

main:
    pushq %rbp              /* Frame Pointer aufbauen */
    movq %rsp, %rbp

    subq $32, %rsp          /* Eigentlich 24, aber 16 Byte Alignment! */

    movq $fmt, %rdi         /* arg0 = %rdi                     */
    movl $3735928559, %esi  /* arg1 = %rsi      (hier nur int) */
    movl $65, %edx          /* arg2 = %rdx      (hier nur int) */
    movq $msg, %rcx         /* arg3 = %rcx                     */
    movl $5, %r8d           /* arg4 = %r8       (hier nur int) */
    movl $6, %r9d           /* arg5 = %r9       (hier nur int) */
    movl $7, -32(%rbp)      /* arg6+ auf dem Stack wie 32 Bit, */
    movl $8, -24(%rbp)      /* auch "umgedrehte" Reihenfolge   */
    movl $9, -16(%rbp)
    call printf             /* Rückgabewert --> %rax */

    movq $fmtret, %rdi      /* Jetzt wieder Ausgabe des Rückgabewerts */
    movl %eax, %esi         /* printf gibt nur int zurück */
    call printf

    movl $0, %eax           /* return 0 */
    leave                   /* Framepointer abbauen */
    ret                     /* Rücksprung aus main() */

Übersetzung wie gewohnt:

gcc -Wall -Wextra -g -o cc-64 cc-64.s

Syscalls

Syscalls sehen auf x86_64 sehr ähnlich wie normale Aufrufe aus. Die Syscallnummer kommt in RAX. Argument 0 in RDI, Argument 1 in RSI, dann RDX, R10, R8, R9. Den Rückgabewert gibt es in RAX und falls nötig noch RDX. Den Syscall löst man dann mit der Instruktion „syscall“ aus.

Gegenüber Aufrufen im Userland verwendet man also R10 statt RCX.

Man muss beachten, dass die 32 Bit-Variante mit „int $0x80“ ebenfalls auf den ersten Blick zu funktionieren scheint. Sie ist allerdings für 32 Bit-Code gedacht! Aus einem 64 Bit-Programm heraus sollte das so nicht aufgerufen werden.

Die Syscall-Nummern unterscheiden sich ebenfalls von denen bei 32 Bit. Man kann sie aber analog in „/usr/include/asm/unistd_64.h“ ablesen.

.section .data

msg:
    .string "Hallo, Welt!\n"

.section .text
.global _start

_start:
    movq $1, %rax         /* 1 = write() */
    movq $1, %rdi         /* 1 für stdout */
    movq $msg, %rsi       /* Pointer auf den String */
    movq $13, %rdx        /* Länge des Strings */
    syscall

    movq $60, %rax        /* 60 = _exit() */
    movq $0, %rdi         /* Rückgabewert 0 */
    syscall

Schlussendlich die Übersetzung dessen:

gcc -nostdlib -Wall -Wextra -g -o sc-64 sc-64.s