blog · git · desktop · images · contact
2013-04-05
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.
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
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
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 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