GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Wie rufe ich einen Systemaufruf über Syscall oder Sysenter in der Inline-Assembly auf?

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 und r10 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.


Linux
  1. So konfigurieren Sie die Virtualisierung unter Redhat Linux

  2. So ändern Sie den Hostnamen unter Linux

  3. Wie kann man den Stack für den Systemaufruf clone() unter Linux zuordnen?

  4. x86_64 Assembly Linux-Systemaufruf-Verwirrung

  5. Rufen Sie Syscalls von Java auf

So installieren Sie Cockpit unter Debian 10

So aktualisieren Sie Ubuntu 18.04 auf Ubuntu 20.04

Wie man Cron-Jobs für ein Linux-System über PHP ausführt und auflistet

So installieren Sie Nginx unter CentOS 8

So nehmen Sie Audio in Ubuntu 20.04 auf

So mounten Sie NFS unter Debian 11