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

Intel x86 vs. x64-Systemaufruf

Allgemeiner Teil

BEARBEITEN:Linux-relevante Teile entfernt

Auch wenn es nicht ganz falsch ist, beschränken Sie sich auf int 0x80 und syscall vereinfacht die Frage wie bei sysenter es gibt mindestens eine dritte Möglichkeit.

Die Verwendung von 0x80 und eax für die Syscall-Nummer, ebx, ecx, edx, esi, edi und ebp zum Übergeben von Parametern ist nur eine von vielen möglichen anderen Optionen zum Implementieren eines Systemaufrufs, aber diese Register sind diejenigen, die das 32-Bit-Linux-ABI gewählt hat .

Bevor wir uns die beteiligten Techniken genauer ansehen, sollte gesagt werden, dass sie alle um das Problem kreisen, aus dem privilegierten Gefängnis zu entkommen, in dem jeder Prozess läuft.

Eine andere Möglichkeit als die hier vorgestellten, die die x86-Architektur bietet, wäre die Verwendung eines Anrufgates gewesen (siehe:http://en.wikipedia.org/wiki/Call_gate)

Die einzige andere Möglichkeit, die auf allen i386-Maschinen vorhanden ist, ist die Verwendung eines Software-Interrupts, der es der ISR (Interrupt Service Routine oder einfach ein Interrupt-Handler ), um mit einer anderen Berechtigungsstufe als zuvor ausgeführt zu werden.

(Unterhaltsame Tatsache:Einige i386-Betriebssysteme haben eine Ausnahme wegen ungültiger Anweisungen verwendet, um den Kernel für Systemaufrufe aufzurufen, weil das tatsächlich schneller war als ein int Anweisung auf 386 CPUs. Siehe OsDev syscall/sysret und sysenter/sysexit Anweisungen aktivieren für eine Zusammenfassung möglicher Systemaufrufmechanismen.)

Software-Unterbrechung

Was genau passiert, wenn ein Interrupt ausgelöst wird, hängt davon ab, ob das Umschalten auf die ISR eine Privilegienänderung erfordert oder nicht:

(Intel® 64 and IA-32 Architectures Software Developer’s Manual)

6.4.1 Aufruf- und Rückgabeoperation für Unterbrechungs- oder Ausnahmebehandlungsverfahren

...

Wenn das Codesegment für die Handler-Prozedur die gleiche Berechtigungsstufe wie das aktuell ausgeführte Programm oder die aktuell ausgeführte Aufgabe hat, verwendet die Handler-Prozedur den aktuellen Stapel; Wenn der Handler auf einer privilegierteren Ebene ausgeführt wird, wechselt der Prozessor zum Stack für die Berechtigungsebene des Handlers.

....

Wenn ein Stapelwechsel auftritt, führt der Prozessor Folgendes aus:

  1. Speichert temporär (intern) den aktuellen Inhalt der SS-, ESP-, EFLAGS-, CS- und> EIP-Register.

  2. Lädt den Segmentselektor und den Stapelzeiger für den neuen Stapel (d. h. den Stapel für die aufgerufene Privilegebene) aus dem TSS in die SS- und ESP-Register und schaltet auf den neuen Stapel um.

  3. Schiebt die temporär gespeicherten SS-, ESP-, EFLAGS-, CS- und EIP-Werte für den Stack der unterbrochenen Prozedur auf den neuen Stack.

  4. Schiebt einen Fehlercode auf den neuen Stack (falls zutreffend).

  5. Lädt den Segmentselektor für das neue Codesegment und den neuen Befehlszeiger (vom Interrupt-Gatter oder Trap-Gate) in die CS- bzw. EIP-Register.

  6. Wenn der Aufruf durch ein Interrupt-Gatter erfolgt, wird das IF-Flag im EFLAGS-Register gelöscht.

  7. Beginnt mit der Ausführung der Handler-Prozedur auf der neuen Berechtigungsebene.

... seufz, das scheint viel zu tun zu sein und selbst wenn wir fertig sind, wird es nicht viel besser:

(Auszug aus derselben Quelle wie oben erwähnt:Intel® 64 and IA-32 Architectures Software Developer’s Manual)

Beim Ausführen einer Rückkehr von einem Interrupt- oder Exception-Handler von einer anderen Berechtigungsstufe als der unterbrochenen Prozedur führt der Prozessor diese Aktionen aus:

  1. Führt eine Berechtigungsprüfung durch.

  2. Stellt die CS- und EIP-Register auf ihre Werte vor dem Interrupt oder der Ausnahme wieder her.

  3. Stellt das EFLAGS-Register wieder her.

  4. Stellt die SS- und ESP-Register auf ihre Werte vor dem Interrupt oder der Ausnahme wieder her, was zu einem Stack-Switch zurück zum Stack der unterbrochenen Prozedur führt.

  5. Setzt die Ausführung der unterbrochenen Prozedur fort.

Sysenter

Eine weitere Option auf der 32-Bit-Plattform, die in Ihrer Frage überhaupt nicht erwähnt wird, aber dennoch vom Linux-Kernel verwendet wird, ist sysenter Anleitung.

(Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B &2C):Instruction Set Reference, A-Z)

Beschreibung Führt einen schnellen Aufruf einer Systemprozedur oder -routine der Ebene 0 aus. SYSENTER ist eine Begleitanweisung zu SYSEXIT. Die Anweisung ist optimiert, um die maximale Leistung für Systemaufrufe von Benutzercode, der auf Berechtigungsebene 3 ausgeführt wird, zu Betriebssystem- oder Ausführungsprozeduren bereitzustellen, die auf Berechtigungsebene 0 ausgeführt werden.

Ein Nachteil bei der Verwendung dieser Lösung ist, dass sie nicht auf allen 32-Bit-Rechnern vorhanden ist, also der int 0x80 Methode muss noch bereitgestellt werden, falls die CPU nichts davon weiß.

Die SYSENTER- und SYSEXIT-Anweisungen wurden in die IA-32-Architektur im Pentium II-Prozessor eingeführt. Die Verfügbarkeit dieser Befehle auf einem Prozessor wird durch das Merkmalsflag SYSENTER/SYSEXITpresent (SEP) angezeigt, das von dem CPUID-Befehl an das EDX-Register zurückgegeben wird. Ein Betriebssystem, das das SEP-Flag qualifiziert, muss auch die Prozessorfamilie und das Modell qualifizieren, um sicherzustellen, dass die SYSENTER/SYSEXIT-Anweisungen tatsächlich vorhanden sind

Systemaufruf

Die letzte Möglichkeit, die syscall Anweisung ermöglicht so ziemlich die gleiche Funktionalität wie sysenter Anweisung. Die Existenz von beiden liegt daran, dass man (systenter ) wurde von Intel eingeführt, während die andere (syscall ) wurde von AMD eingeführt.

Linux-spezifisch

Im Linux-Kernel kann jede der drei oben genannten Möglichkeiten gewählt werden, um einen Systemaufruf zu realisieren.

Siehe auch The Definitive Guide to Linux System Calls .

Wie bereits oben erwähnt, ist die int 0x80 Methode ist die einzige der 3 ausgewählten Implementierungen, die auf jeder i386-CPU laufen kann, also ist dies die einzige, die immer für 32-Bit-Benutzerraum verfügbar ist.

(syscall ist die einzige, die immer für 64-Bit-Benutzerraum verfügbar ist, und die einzige, die Sie jemals in 64-Bit-Code verwenden sollten; x86-64-Kernel können ohne CONFIG_IA32_EMULATION erstellt werden , und int 0x80 ruft immer noch die 32-Bit-ABI auf, die Zeiger auf 32-Bit abschneidet.)

Um zwischen allen 3 Wahlmöglichkeiten wechseln zu können, erhält jeder Prozesslauf Zugriff auf ein spezielles gemeinsam genutztes Objekt, das Zugriff auf die für das laufende System ausgewählte Systemaufrufimplementierung gewährt. Das ist das seltsam aussehende linux-gate.so.1 Möglicherweise sind Sie bei der Verwendung von ldd bereits auf eine nicht aufgelöste Bibliothek gestoßen oder ähnliches.

(arch/x86/vdso/vdso32-setup.c)

 if (vdso32_syscall()) {                                                                               
        vsyscall = &vdso32_syscall_start;                                                                 
        vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start;                                       
    } else if (vdso32_sysenter()){                                                                        
        vsyscall = &vdso32_sysenter_start;                                                                
        vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start;                                     
    } else {                                                                                              
        vsyscall = &vdso32_int80_start;                                                                   
        vsyscall_len = &vdso32_int80_end - &vdso32_int80_start;                                           
    }   

Um es zu nutzen, müssen Sie nur alle Ihre Register Systemrufnummer in eax laden, Parameter in ebx, ecx, edx, esi, edi wie bei int 0x80 Implementierung von Systemaufrufen und call die Hauptroutine.

Leider ist es nicht ganz so einfach; B. um das Sicherheitsrisiko zu minimieren, eine fest vorgegebene Adresse, der Ort, an dem die vdso (virtuelles dynamisches gemeinsames Objekt ) wird in einem zufälligen Prozess sichtbar sein, also müssen Sie zuerst die richtige Position herausfinden.

Diese Adresse ist für jeden Prozess individuell und wird an den Prozess weitergegeben, sobald er gestartet wird.

Falls Sie es nicht wussten, bekommt jeder Prozess beim Start unter Linux Zeiger auf die übergebenen Parameter, sobald er gestartet wurde, und Zeiger auf eine Beschreibung der Umgebungsvariablen, unter denen er ausgeführt wird, die auf seinem Stack weitergegeben werden - jede von ihnen endet mit NULL.

Zusätzlich zu diesen wird nach den vorgenannten noch ein dritter Block sogenannter Elf-Auxiliary-Vektoren übergeben. Der richtige Standort ist in einem davon mit der Typkennung AT_SYSINFO verschlüsselt .

Das Stack-Layout sieht also so aus (Adressen wachsen nach unten):

  • parameter-0
  • ...
  • parameter-m
  • NULL
  • Umgebung-0
  • ...
  • Umgebung-n
  • NULL
  • ...
  • Hilfselbenvektor:AT_SYSINFO
  • ...
  • Hilfselbenvektor:AT_NULL

Anwendungsbeispiel

Um die richtige Adresse zu finden, müssen Sie zuerst alle Argumente und alle Umgebungszeiger überspringen und dann nach AT_SYSINFO suchen wie im folgenden Beispiel gezeigt:

#include <stdio.h>
#include <elf.h>

void putc_1 (char c) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "int $0x80"
           :: "c" (&c)
           : "eax", "ebx", "edx");
}

void putc_2 (char c, void *addr) {
  __asm__ ("movl $0x04, %%eax\n"
           "movl $0x01, %%ebx\n"
           "movl $0x01, %%edx\n"
           "call *%%esi"
           :: "c" (&c), "S" (addr)
           : "eax", "ebx", "edx");
}


int main (int argc, char *argv[]) {

  /* using int 0x80 */
  putc_1 ('1');


  /* rather nasty search for jump address */
  argv += argc + 1;     /* skip args */
  while (*argv != NULL) /* skip env */
    ++argv;            

  Elf32_auxv_t *aux = (Elf32_auxv_t*) ++argv; /* aux vector start */

  while (aux->a_type != AT_SYSINFO) {
    if (aux->a_type == AT_NULL)
      return 1;
    ++aux;
  }

  putc_2 ('2', (void*) aux->a_un.a_val);

  return 0;
}

Wie Sie sehen werden, wenn Sie sich das folgende Snippet von /usr/include/asm/unistd_32.h ansehen auf meinem System:

#define __NR_restart_syscall 0
#define __NR_exit            1
#define __NR_fork            2
#define __NR_read            3
#define __NR_write           4
#define __NR_open            5
#define __NR_close           6

Der Syscall, den ich verwendet habe, ist der mit 4 (Schreiben) nummerierte, wie er im eax-Register übergeben wurde. Er nimmt den Dateideskriptor (ebx =1), den Datenzeiger (ecx =&c) und die Größe (edx =1) als Argumente, die jeweils in übergeben werden entsprechendes Register.

Um es kurz zu machen

Vergleich eines angeblich langsam laufenden int 0x80 Systemaufruf für beliebig Intel-CPU mit einer (hoffentlich) viel schnelleren Implementierung unter Verwendung des (eigentlich von AMD erfundenen) syscall Der Unterricht vergleicht Äpfel mit Birnen.

IMHO:Höchstwahrscheinlich der sysenter Anweisung statt int 0x80 sollte hier auf die Probe gestellt werden.


Es gibt drei Dinge, die passieren müssen, wenn Sie den Kernel aufrufen (einen Systemaufruf machen):

  1. Das System wechselt vom "Benutzermodus" in den "Kernelmodus" (Ring 0).
  2. Der Stack wechselt vom "Benutzermodus" in den "Kernelmodus".
  3. Es wird zu einem geeigneten Teil des Kernels gesprungen.

Offensichtlich muss der Kernel-Code, sobald er sich im Kernel befindet, wissen, was der Kernel tatsächlich tun soll, und daher etwas in EAX und oft mehr Dinge in andere Register einfügen, da es Dinge wie "Name der Datei, die Sie öffnen möchten " oder "Puffer zum Einlesen von Daten aus einer Datei in" usw. usw.

Unterschiedliche Prozessoren haben unterschiedliche Wege, um die obigen drei Schritte zu erreichen. In x86 gibt es mehrere Möglichkeiten, aber die beiden beliebtesten für handgeschriebene asm sind int 0xnn (32-Bit-Modus) oder syscall (64-Bit-Modus). (Es gibt auch den 32-Bit-Modus sysenter , eingeführt von Intel aus demselben Grund, aus dem AMD die 32-Bit-Modus-Version von syscall eingeführt hat :als schnellere Alternative zum langsamen int 0x80 . Die 32-Bit-glibc verwendet einen beliebigen effizienten Systemaufrufmechanismus, der verfügbar ist, und verwendet nur den langsamen int 0x80 wenn nichts besseres verfügbar ist.)

Die 64-Bit-Version von syscall Instruktionen wurden mit der x86-64-Architektur eingeführt, um einen Systemaufruf schneller eingeben zu können. Es verfügt über eine Reihe von Registern (unter Verwendung der x86-MSR-Mechanismen), die den Adress-RIP enthalten, zu dem wir springen möchten, welche Selektorwerte in CS und SS geladen werden sollen und für den Übergang von Ring3 zu Ring0. Es speichert auch die Absenderadresse in ECX/RCX. [Bitte lesen Sie das Handbuch des Befehlssatzes für alle Details dieser Anweisung - es ist nicht ganz trivial!]. Da der Prozessor weiß, dass dies zu Ring0 wechseln wird, kann er direkt das Richtige tun.

Einer der wichtigsten Punkte ist syscall manipuliert nur Register; es tut keine Lasten oder speichert. (Deshalb wird RCX mit dem gespeicherten RIP und R11 mit den gespeicherten RFLAGS überschrieben). Der Speicherzugriff hängt von Seitentabellen ab, und Seitentabelleneinträge haben ein Bit, das sie nur für den Kernel gültig machen kann, nicht für den Benutzerbereich, also wird der Speicherzugriff während ausgeführt Das Ändern der Berechtigungsebene muss möglicherweise warten, anstatt nur Register zu schreiben. Im Kernel-Modus verwendet der Kernel normalerweise swapgs oder eine andere Möglichkeit, den Kernel-Stack zu finden. (syscall tut nicht RSP modifizieren; es zeigt immer noch auf den Benutzer-Stack beim Eintritt in den Kernel.)

Bei der Rückkehr mit der SYSRET-Anweisung werden die Werte aus vorgegebenen Werten in Registern wiederhergestellt, also geht es wieder schnell, weil der Prozessor nur ein paar Register einrichten muss. Der Prozessor weiß, dass er von Ring0 zu Ring3 wechseln wird, und kann daher schnell die richtigen Dinge tun.

(AMD-CPUs unterstützen den syscall Anweisung aus dem 32-Bit-Benutzerraum; Intel-CPUs nicht. x86-64 war ursprünglich AMD64; deshalb haben wir syscall im 64-Bit-Modus. AMD hat die Kernelseite von syscall neu gestaltet für den 64-Bit-Modus, also die 64-Bit-syscall Kernel-Einstiegspunkt unterscheidet sich erheblich vom 32-Bit-syscall Einstiegspunkt in 64-Bit-Kernels.)

Der int 0x80 Die im 32-Bit-Modus verwendete Variante entscheidet anhand des Werts in der Interrupt-Deskriptortabelle, was zu tun ist, was bedeutet, dass aus dem Speicher gelesen wird. Dort findet es die neuen CS- und EIP/RIP-Werte. Das neue CS-Register bestimmt den neuen "Ring"-Pegel - in diesem Fall Ring0. Es wird dann den neuen CS-Wert verwenden, um in das Task-Zustandssegment (basierend auf dem TR-Register) zu schauen, um herauszufinden, welcher Stapelzeiger (ESP/RSP und SS) ist, und springt dann schließlich zu der neuen Adresse. Da dies eine weniger direkte und allgemeinere Lösung ist, ist sie auch langsamer. Das alte EIP/RIP und CS wird zusammen mit den alten Werten von SS und ESP/RSP auf dem neuen Stack gespeichert.

Bei der Rückkehr liest der Prozessor unter Verwendung des IRET-Befehls die Rückkehradresse und die Stapelzeigerwerte aus dem Stapel, wobei er auch die neuen Stapelsegment- und Codesegmentwerte aus dem Stapel lädt. Auch hier ist der Prozess generisch und erfordert einige Speicherlesevorgänge. Da es generisch ist, muss der Prozessor auch prüfen "ändern wir den Modus von Ring0 auf Ring3, wenn ja, ändern Sie diese Dinge".

Zusammenfassend ist es also schneller, weil es so funktionieren sollte.

Für 32-Bit-Code können Sie definitiv den langsamen und kompatiblen int 0x80 verwenden wenn Sie möchten.

Für 64-Bit-Code int 0x80 ist langsamer als syscall und wird Ihre Zeiger auf 32-Bit kürzen, also verwenden Sie es nicht. Siehe Was passiert, wenn Sie die 32-Bit-int 0x80-Linux-ABI in 64-Bit-Code verwenden? Außerdem int 0x80 ist nicht auf allen Kerneln im 64-Bit-Modus verfügbar, daher ist es nicht einmal für sys_exit sicher die keine Zeigerargumente akzeptiert:CONFIG_IA32_EMULATION kann deaktiviert werden, und vor allem ist auf dem Windows-Subsystem für Linux deaktiviert.


Linux
  1. Wie kann man herausfinden, ob das System Intel Amt unterstützt?

  2. Aufruf an Betriebssystem zum Öffnen der URL?

  3. Was macht der Systemaufruf brk()?

  4. x86_64 Assembly Linux-Systemaufruf-Verwirrung

  5. Wo finde ich den Quellcode für Systemaufrufe?

So machen Sie ein Linux-System schneller auf Intel-CPUs

Was ist der Unterschied zwischen Systemaufruf und Bibliotheksaufruf?

Prüfen, ob errno !=EINTR:was bedeutet das?

Schnellster Linux-Systemaufruf

Wie übergebe ich Parameter an den Linux-Systemaufruf?

Warum unterscheiden sich Linux-Systemrufnummern in x86 und x86_64?