Explizite Registervariablen
https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars)
Ich glaube, dass dies jetzt im Allgemeinen der empfohlene Ansatz für Registerbeschränkungen sein sollte, weil:
- es kann alle Register darstellen, einschließlich
r8
,r9
undr10
die für Systemaufrufargumente verwendet werden:Wie werden Registerbeschränkungen für die Intel x86_64-Register r8 bis r15 in der GCC-Inline-Assemblierung angegeben? - Es ist die einzige optimale Option für andere ISAs neben x86 wie ARM, die keine magischen Registereinschränkungsnamen haben:Wie kann ein einzelnes Register als Einschränkung in der ARM-GCC-Inline-Assemblierung angegeben werden? (neben der Verwendung eines temporären Registers + Clobbers + und einer zusätzlichen Mov-Anweisung)
- Ich werde argumentieren, dass diese Syntax besser lesbar ist als die Verwendung der Einzelbuchstaben-Mnemonik wie
S -> rsi
Registervariablen werden zum Beispiel in glibc 2.29 verwendet, siehe:sysdeps/unix/sysv/linux/x86_64/sysdep.h
.
main_reg.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
register int64_t rax __asm__ ("rax") = 1;
register int rdi __asm__ ("rdi") = fd;
register const void *rsi __asm__ ("rsi") = buf;
register size_t rdx __asm__ ("rdx") = size;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi), "r" (rsi), "r" (rdx)
: "rcx", "r11", "memory"
);
return rax;
}
void my_exit(int exit_status) {
register int64_t rax __asm__ ("rax") = 60;
register int rdi __asm__ ("rdi") = exit_status;
__asm__ __volatile__ (
"syscall"
: "+r" (rax)
: "r" (rdi)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub-Upstream.
Kompilieren und ausführen:
gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \
-pedantic -o main_reg.out main_reg.c
./main.out
echo $?
Ausgabe
hello world
0
Zum Vergleich das Folgende analog zu How to invoke a system call via syscall or sysenter in inline assembly? erzeugt eine äquivalente Assembly:
main_constraint.c
#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (1), "D" (fd), "S" (buf), "d" (size)
: "rcx", "r11", "memory"
);
return ret;
}
void my_exit(int exit_status) {
ssize_t ret;
__asm__ __volatile__ (
"syscall"
: "=a" (ret)
: "0" (60), "D" (exit_status)
: "rcx", "r11", "memory"
);
}
void _start(void) {
char msg[] = "hello world\n";
my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}
GitHub-Upstream.
Demontage von beiden mit:
objdump -d main_reg.out
ist fast identisch, hier ist der main_reg.c
eins:
Disassembly of section .text:
0000000000001000 <my_write>:
1000: b8 01 00 00 00 mov $0x1,%eax
1005: 0f 05 syscall
1007: c3 retq
1008: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
100f: 00
0000000000001010 <my_exit>:
1010: b8 3c 00 00 00 mov $0x3c,%eax
1015: 0f 05 syscall
1017: c3 retq
1018: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
101f: 00
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: bf 01 00 00 00 mov $0x1,%edi
102a: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: ba 0d 00 00 00 mov $0xd,%edx
1043: b8 01 00 00 00 mov $0x1,%eax
1048: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104f: 0a
1050: 0f 05 syscall
1052: 31 ff xor %edi,%edi
1054: 48 83 f8 0d cmp $0xd,%rax
1058: b8 3c 00 00 00 mov $0x3c,%eax
105d: 40 0f 95 c7 setne %dil
1061: 0f 05 syscall
1063: c3 retq
Wir sehen also, dass GCC diese winzigen Syscall-Funktionen wie gewünscht eingebaut hat.
my_write
und my_exit
sind für beide gleich, aber _start
in main_constraint.c
ist etwas anders:
0000000000001020 <_start>:
1020: c6 44 24 ff 00 movb $0x0,-0x1(%rsp)
1025: 48 8d 74 24 f3 lea -0xd(%rsp),%rsi
102a: ba 0d 00 00 00 mov $0xd,%edx
102f: 48 b8 68 65 6c 6c 6f movabs $0x6f77206f6c6c6568,%rax
1036: 20 77 6f
1039: 48 89 44 24 f3 mov %rax,-0xd(%rsp)
103e: b8 01 00 00 00 mov $0x1,%eax
1043: c7 44 24 fb 72 6c 64 movl $0xa646c72,-0x5(%rsp)
104a: 0a
104b: 89 c7 mov %eax,%edi
104d: 0f 05 syscall
104f: 31 ff xor %edi,%edi
1051: 48 83 f8 0d cmp $0xd,%rax
1055: b8 3c 00 00 00 mov $0x3c,%eax
105a: 40 0f 95 c7 setne %dil
105e: 0f 05 syscall
1060: c3 retq
Es ist interessant zu beobachten, dass GCC in diesem Fall eine etwas kürzere äquivalente Codierung gefunden hat, indem es Folgendes ausgewählt hat:
104b: 89 c7 mov %eax,%edi
um den fd
einzustellen bis 1
, was dem 1
entspricht von der Syscall-Nummer, anstatt direkter:
1025: bf 01 00 00 00 mov $0x1,%edi
Für eine eingehende Diskussion der Aufrufkonventionen siehe auch:Was sind die Aufrufkonventionen für UNIX- und Linux-Systemaufrufe (und Benutzerraumfunktionen) auf i386 und x86-64
Getestet in Ubuntu 18.10, GCC 8.2.0.
Zunächst einmal, Sie können GNU C Basic asm("");
nicht sicher verwenden Syntax dafür (ohne Input/Output/Clobber-Einschränkungen). Sie benötigen Extended asm, um den Compiler über von Ihnen geänderte Register zu informieren. Siehe das Inline-Asm im GNU C-Handbuch und das Inline-Assembly-Tag-Wiki für Links zu anderen Anleitungen für Details zu Dingen wie "D"(1)
bedeutet als Teil eines asm()
Aussage.
Sie benötigen außerdem asm volatile
denn das ist für Extended asm
nicht implizit Anweisungen mit 1 oder mehr Ausgabeoperanden.
Ich werde Ihnen zeigen, wie Sie Systemaufrufe ausführen, indem Sie ein Programm schreiben, das Hello World!
schreibt zur Standardausgabe mit write()
Systemaufruf. Hier ist die Quelle des Programms ohne Implementierung des eigentlichen Systemaufrufs:
#include <sys/types.h>
ssize_t my_write(int fd, const void *buf, size_t size);
int main(void)
{
const char hello[] = "Hello world!\n";
my_write(1, hello, sizeof(hello));
return 0;
}
Sie können sehen, dass ich meine benutzerdefinierte Systemaufruffunktion als my_write
benannt habe um Namenskonflikte mit dem "normalen" write
zu vermeiden , bereitgestellt von libc. Der Rest dieser Antwort enthält die Quelle von my_write
für i386 und amd64.
i386
Systemaufrufe in i386 Linux werden mit dem 128. Interrupt-Vektor implementiert, z. durch Aufruf von int 0x80
in Ihrem Assembler-Code, natürlich vorher entsprechend parametriert. Dasselbe ist über SYSENTER
möglich , aber die tatsächliche Ausführung dieser Anweisung wird durch das VDSO erreicht, das virtuell jedem laufenden Prozess zugeordnet ist. Seit SYSENTER
war nie als direkter Ersatz für int 0x80
gedacht API, es wird niemals direkt von Userland-Anwendungen ausgeführt – stattdessen ruft es, wenn eine Anwendung auf Kernel-Code zugreifen muss, die virtuell abgebildete Routine im VDSO auf (das ist, was der call *%gs:0x10
in Ihrem Code steht für), die den gesamten Code enthält, der SYSENTER
unterstützt Anweisung. Es gibt ziemlich viel davon, weil die Anleitung tatsächlich funktioniert.
Wenn Sie mehr darüber lesen möchten, schauen Sie sich diesen Link an. Es enthält einen ziemlich kurzen Überblick über die Techniken, die im Kernel und im VDSO angewendet werden. Siehe auch The Definitive Guide to (x86) Linux System Calls - einige Systemaufrufe wie getpid
und clock_gettime
sind so einfach, dass der Kernel Code und Daten exportieren kann, die im Benutzerbereich ausgeführt werden, sodass das VDSO nie in den Kernel eintreten muss, wodurch es sogar viel schneller ist als sysenter
könnte sein.
Es ist viel einfacher, das langsamere int $0x80
zu verwenden zum Aufrufen der 32-Bit-ABI.
// i386 Linux
#include <asm/unistd.h> // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"int $0x80"
: "=a" (ret)
: "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
: "memory" // the kernel dereferences pointer args
);
return ret;
}
Wie Sie sehen können, verwenden Sie den int 0x80
API ist relativ einfach. Die Nummer des Syscalls geht an die eax
registrieren, während alle Parameter, die für den Syscall benötigt werden, jeweils in ebx
gehen , ecx
, edx
, esi
, edi
, und ebp
. Systemrufnummern können durch Einlesen der Datei /usr/include/asm/unistd_32.h
ermittelt werden .
Prototypen und Beschreibungen der Funktionen finden Sie im 2. Teil des Handbuchs, in diesem Fall also write(2)
.
Der Kernel speichert/stellt alle Register (außer EAX) wieder her, sodass wir sie als Nur-Eingabe-Operanden für den Inline-Asm verwenden können. Siehe Was sind die Aufrufkonventionen für UNIX- und Linux-Systemaufrufe (und Benutzerbereichsfunktionen) auf i386 und x86-64
Beachten Sie, dass die Clobber-Liste auch den memory
enthält Parameter, was bedeutet, dass die in der Anweisungsliste aufgeführte Anweisung auf den Speicher verweist (über den buf
Parameter). (Eine Zeigereingabe in Inline-ASM bedeutet nicht, dass der Speicher, auf den gezeigt wird, auch eine Eingabe ist. Siehe Wie kann ich angeben, dass der Speicher, auf den ein Inline-ASM-Argument *zeigt*, verwendet werden darf?)
amd64
Anders sieht es bei der AMD64-Architektur aus, die eine neue Anweisung namens SYSCALL
enthält . Es unterscheidet sich stark vom ursprünglichen SYSENTER
Anleitung und definitiv viel einfacher von Userland-Anwendungen aus zu verwenden - es ähnelt wirklich einem normalen CALL
, eigentlich, und Anpassung des alten int 0x80
zum neuen SYSCALL
ist ziemlich trivial. (Außer, dass es RCX und R11 anstelle des Kernel-Stacks verwendet, um die Benutzerraum-RIP und RFLAGS zu speichern, damit der Kernel weiß, wohin er zurückkehren muss).
In diesem Fall wird die Nummer des Systemaufrufs noch im Register rax
übergeben , aber die zum Speichern der Argumente verwendeten Register stimmen jetzt fast mit der Funktionsaufrufkonvention überein:rdi
, rsi
, rdx
, r10
, r8
und r9
in dieser Reihenfolge. (syscall
selbst zerstört rcx
also r10
wird anstelle von rcx
verwendet , sodass libc-Wrapper-Funktionen einfach mov r10, rcx
verwenden / syscall
.)
// x86-64 Linux
#include <asm/unistd.h> // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
ssize_t ret;
asm volatile
(
"syscall"
: "=a" (ret)
// EDI RSI RDX
: "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
: "rcx", "r11", "memory"
);
return ret;
}
(Siehe es auf Godbolt kompilieren)
Beachten Sie, dass praktisch das einzige, was geändert werden musste, die Registernamen und die eigentliche Anweisung waren, die zum Tätigen des Anrufs verwendet wurde. Dies ist vor allem den Ein-/Ausgabelisten zu verdanken, die von der erweiterten Inline-Assembly-Syntax von gcc bereitgestellt werden, die automatisch geeignete Bewegungsanweisungen bereitstellt, die zum Ausführen der Anweisungsliste benötigt werden.
Der "0"(callnum)
Übereinstimmungsbeschränkung könnte als "a"
geschrieben werden weil Operand 0 (der "=a"(ret)
Ausgang) hat nur ein Register zur Auswahl; wir wissen, dass es EAX auswählen wird. Verwenden Sie, was immer Sie klarer finden.
Beachten Sie, dass Nicht-Linux-Betriebssysteme wie MacOS andere Rufnummern verwenden. Und sogar andere Konventionen für die Übergabe von Argumenten für 32-Bit.