Gedanken zu copy_{to, from}_user() im Linux-Kernel

Gedanken zu copy_{to, from}_user() im Linux-Kernel

1. Was ist copy_{to,from}_user()

Es ist eine Brücke für die Kommunikation zwischen Kernel- und Benutzerbereich. Alle Dateninteraktionen sollten eine solche Schnittstelle verwenden. Aber was genau ist seine Rolle? Wir stellen folgende Fragen:

  • Warum brauchen wir copy_{to,from}_user() und was tut es für uns hinter den Kulissen?
  • Was ist der Unterschied zwischen copy_{to,from}_user() und memcpy()? Kann ich memcpy() direkt verwenden?
  • Wird es Probleme geben, wenn memcpy() copy_{to,from}_user() ersetzt?

Herzliche Erinnerung: Die Codeanalyse in diesem Artikel basiert auf Linux-4.18.0 und einige architekturbezogene Codes werden durch ARM64 dargestellt.

1. copy_{to,from}_user() vs. memcpy()

  • Im Vergleich zu memcpy() verfügt copy_{to,from}_user() über eine zusätzliche Prüfung der Gültigkeit der eingehenden Adresse. Beispielsweise, ob es zum Userspace-Adressbereich gehört. Theoretisch kann der Kernelbereich Zeiger, die aus dem Benutzerbereich übergeben werden, direkt verwenden. Selbst wenn Daten kopiert werden müssen, kann memcpy() direkt verwendet werden. Tatsächlich verwendet die endgültige Implementierung von copy_{to,from}_user() auf einer Architektur ohne MMU memcpy(). Bei den meisten Plattformen mit MMU ist die Situation jedoch etwas anders: Der vom Benutzerbereich übergebene Zeiger befindet sich im virtuellen Adressraum, und der virtuelle Adressraum, auf den er verweist, wurde möglicherweise nicht der tatsächlichen physischen Seite zugeordnet. Aber was können wir dagegen tun? Die durch den Seitenfehler verursachte Ausnahme wird vom Kernel transparent repariert (eine neue physische Seite wird an den Adressraum des Seitenfehlers gesendet) und der Befehl, der auf den Seitenfehler zugegriffen hat, wird weiter ausgeführt, als ob nichts passiert wäre. Dies ist jedoch nur das Verhalten der Seitenfehlerausnahme im Benutzerbereich. Im Kernelbereich muss diese Seitenfehlerausnahme explizit repariert werden, was durch das Entwurfsmuster der vom Kernel bereitgestellten Funktion zur Behandlung von Seitenfehlerausnahmen bestimmt wird. Die Idee dahinter ist: Wenn ein Programm im Kernelmodus versucht, auf eine Benutzerbereichsadresse zuzugreifen, die noch nicht einer physischen Seite zugewiesen wurde, muss der Kernel diesbezüglich wachsam sein und darf nicht unwissend sein wie der Benutzerbereich.
  • Wenn wir die Richtigkeit des im Benutzermodus übergebenen Zeigers sicherstellen, können wir copy_{to,from}_user() vollständig durch die Funktion memcpy() ersetzen. Nach einigen experimentellen Tests stellte ich fest, dass es kein Problem gab, das Programm mit memcpy() auszuführen. Daher können die beiden ersetzt werden, während die Sicherheit der Benutzermoduszeiger gewährleistet bleibt.

In verschiedenen Blogs konzentrieren sich die Meinungen hauptsächlich auf den ersten Punkt. Es scheint, dass der erste Punkt allgemein anerkannt ist. Wer jedoch auf die Praxis setzt, gelangt zu einer zweiten Ansicht, denn Übung macht den Meister. Liegt die Wahrheit in den Händen einiger weniger Menschen? Oder sind die Augen der Menschen schärfer? Selbstverständlich bestreite ich keine der oben genannten Ansichten. Auch können wir Ihnen nicht garantieren, welche Ansicht richtig ist. Denn ich bin davon überzeugt, dass selbst eine einstmals einwandfreie Theorie im Laufe der Zeit oder aufgrund veränderter Umstände ihre Richtigkeit verlieren kann. Beispielsweise Newtons Theorie der klassischen Mechanik (das scheint etwas weit hergeholt). Wenn ich es in menschlichen Worten ausdrücken müsste, wäre es Folgendes: Die Linux-Codebasis ändert sich im Laufe der Zeit ständig. Vielleicht war die obige Ansicht einmal richtig. Natürlich kann es auch heute noch richtig sein. Die folgende Analyse stellt meine Meinung dar. Ebenso müssen wir skeptisch bleiben.

2. Funktionsdefinition

Sehen wir uns zunächst die Funktionsdefinitionen von memcpy() und copy_{to,from}_user() an. Die Parameter sind fast gleich, sie enthalten alle die Zieladresse, die Quelladresse und die Größe der zu kopierenden Bytes.

statisch __always_inline unsigned long __must_check 
kopiere_nach_Benutzer(void __user *nach, const void *von, unsigned long n); 
statisch __always_inline unsigned long __must_check 
kopiere_von_Benutzer(void *nach, const void __user *von, unsigned long n); 
void *memcpy(void *dest, const void *src, size_t len);


Eines wissen wir jedoch mit Sicherheit. Das heißt, memcpy() überprüft nicht die Legitimität der übergebenen Adresse. Und copy_{to,from}_user() führt eine Gültigkeitsprüfung ähnlich der folgenden an der eingehenden Adresse durch (um es einfacher auszudrücken: Weitere Einzelheiten zur Überprüfung finden Sie im Code).

  • Wenn Daten vom Benutzerbereich in den Kernelbereich kopiert werden, müssen die Benutzerbereichsadressen bis und bis plus die Länge n der kopierten Bytes im Benutzerbereichsadressraum liegen.
  • Wenn Sie Daten vom Kernelspeicher in den Benutzerspeicher kopieren, müssen Sie auch die Legitimität der Adresse überprüfen. Beispielsweise ob es sich um einen Out-of-Bounds-Zugriff handelt oder ob es sich um Daten im Codeabschnitt handelt, etc. Kurz gesagt: Sämtliche illegalen Aktivitäten müssen sofort gestoppt werden.

Schauen wir uns nach diesem kurzen Vergleich weitere Unterschiede an und diskutieren wir die beiden oben genannten Punkte. Beginnen wir mit dem zweiten Punkt. Wenn es ums Üben geht, glaube ich immer noch, dass Übung den Meister macht. Aus den Ergebnissen meines Tests lassen sich zwei Implementierungsergebnisse ableiten.

Das Ergebnis des ersten Falls ist: Beim Testen mit memcpy() gibt es kein Problem und der Code wird normal ausgeführt. Der Testcode lautet wie folgt (es wird nur die Leseschnittstellenfunktion angezeigt, die den Dateioperationen unter dem Proc-Dateisystem entspricht):

statische ssize_t test_read(Struktur Datei *Datei, char __user *buf, 
                         size_t Länge, loff_t *Offset) 
{ 
        memcpy(buf, "test\n", 5); /* Kopiere_in_Benutzer(buf, "test\n", 5) */ 
        Rückgabe 5; 
}

Wir verwenden den Befehl cat, um den Dateiinhalt zu lesen. cat ruft test_read über den Systemaufruf read auf und die übergebene Puffergröße beträgt 4 KB. Der Test verlief reibungslos und die Ergebnisse waren vielversprechend. Die Zeichenfolge „Test“ wurde erfolgreich gelesen. Es scheint, dass der zweite Punkt richtig ist. Wir müssen jedoch weiterhin Überprüfungen und Untersuchungen durchführen. Denn der erste erwähnte Punkt: „Diese Seitenfehlerausnahme muss explizit im Kernelbereich repariert werden.“ Daher müssen wir auch die folgende Situation überprüfen: Wenn buf virtueller Adressraum im Benutzerraum zugewiesen wurde, aber keine spezifische Zuordnungsbeziehung mit dem physischen Speicher hergestellt wurde, tritt in diesem Fall ein Seitenfehler im Kernelmodus auf. Wir müssen zuerst diese Bedingung erstellen, den passenden Puffer finden und ihn dann testen. Getestet habe ich das natürlich nicht. Weil es Testschlussfolgerungen gibt (hauptsächlich, weil ich faul bin und es mühsam finde, diese Bedingung zu konstruieren). Dieser Test wurde von einem Freund von mir durchgeführt, der auch als „Assistenzlehrer“ von Teacher Song, Ackerman, bekannt ist. Er hat dieses Experiment einmal durchgeführt und ist zu folgendem Schluss gekommen: Auch wenn keine spezielle Zuordnungsbeziehung zwischen Puffer und physischem Speicher besteht, kann der Code normal ausgeführt werden. Ein Seitenfehler tritt im Kernelzustand auf und wird von diesem repariert (Zuweisen eines bestimmten physischen Speichers, Füllen der Seitentabelle und Herstellen einer Zuordnungsbeziehung). Gleichzeitig habe ich es aus der Perspektive des Codes analysiert und bin zu dem gleichen Schluss gekommen.

Nach der obigen Analyse scheint es, dass memcpy () auch normal verwendet werden kann. Aus Sicherheitsgründen wird empfohlen, Schnittstellen wie copy_{to,from}_user () zu verwenden.

Das Ergebnis des zweiten Falls ist, dass der obige Testcode nicht ordnungsgemäß ausgeführt wird und einen Kernel-Oops auslöst. Natürlich unterscheiden sich die Kernelkonfigurationsoptionen für diesen Test von denen für den letzten Test. Dieses Konfigurationselement ist CONFIG_ARM64_SW_TTBR0_PAN oder CONFIG_ARM64_PAN (für ARM64-Plattform). Die Funktion beider Konfigurationsoptionen besteht darin, den direkten Zugriff des Kernelmodus auf den Benutzeradressraum zu verhindern. Der einzige Unterschied besteht darin, dass CONFIG_ARM64_SW_TTBR0_PAN diese Funktion durch Softwaresimulation implementiert, während CONFIG_ARM64_PAN diese Funktion durch Hardware implementiert (erweiterte Funktion von ARMv8.1). Wir verwenden CONFIG_ARM64_SW_TTBR0_PAN als Analyseobjekt (nur die Softwaresimulation verfügt über Code zur Analyse). Übrigens, wenn die Hardware es nicht unterstützt, ist es nutzlos, selbst wenn CONFIG_ARM64_PAN konfiguriert ist, und Sie können nur die Methode der Softwareemulation verwenden. Wenn Sie auf die Benutzerbereichsadresse zugreifen müssen, müssen Sie eine Schnittstelle wie copy_{to,from}_user() verwenden, da es sonst zu Kernel-Oops kommt.

Nachdem Sie CONFIG_ARM64_SW_TTBR0_PAN aktiviert haben, führt das Testen des obigen Codes zu Kernel-Oops. Der Grund liegt darin, dass der Kernelstatus direkt auf die Benutzerbereichsadresse zugreift. Daher können wir in diesem Fall memcpy() nicht verwenden. Wir haben keine andere Wahl, als copy_{to,from}_user() zu verwenden.

Warum brauchen wir die PAN-Funktion (Privileged Access Never)? Der Grund dafür kann sein, dass die Dateninteraktion zwischen Benutzerbereich und Kernelbereich leicht zu Sicherheitsproblemen führen kann. Daher erlauben wir dem Kernelbereich nicht, einfach auf den Benutzerbereich zuzugreifen. Wenn dies erforderlich ist, müssen wir PAN über eine bestimmte Schnittstelle schließen. Andererseits kann die PAN-Funktion die Verwendung von Schnittstellen für die Dateninteraktion im Kernelmodus und Benutzermodus weiter standardisieren. Wenn die PAN-Funktion aktiviert ist, können die Kernel- oder Treiberentwickler gezwungen werden, Sicherheitsschnittstellen wie copy_{to,from}_user() zu verwenden, um die Sicherheit des Systems zu verbessern. Bei nicht standardmäßigen Vorgängen wie memcpy() gibt der Kernel ein Hoppla aus.

Durch unsachgemäße Programmierung entstehen Sicherheitslücken. Beispiel: Die Linux-Kernel-Sicherheitslücke CVE-2017-5123 kann zu einer Privilegienerhöhung führen. Der Grund für die Einführung dieser Sicherheitsanfälligkeit ist das Fehlen von access_ok(), um die Legitimität der vom Benutzer übergebenen Adresse zu überprüfen. Um Sicherheitsprobleme zu vermeiden, die durch unseren eigenen Code verursacht werden, müssen wir daher bei der Interaktion zwischen Daten im Kernelbereich und im Benutzerbereich besonders vorsichtig sein.

2. CONFIG_ARM64_SW_TTBR0_PAN-Prinzip

CONFIG_ARM64_SW_TTBR0_PAN Das Prinzip hinter dem Design. Aufgrund des speziellen Hardwaredesigns von ARM64 verwenden wir zwei Seitentabellen-Basisadressregister ttbr0_el1 und ttbr1_el1. Der Prozessor bestimmt anhand der oberen 16 Bits der 64-Bit-Adresse, ob die aufgerufene Adresse zum Benutzerbereich oder zum Kernelbereich gehört. Wenn es sich um eine Benutzerbereichsadresse handelt, verwenden Sie ttbr0_el1, andernfalls verwenden Sie ttbr1_el1. Daher müssen Sie beim Wechseln des ARM64-Prozesses nur den Wert von ttbr0_el1 ändern. ttbr1_el1 muss sich möglicherweise nicht ändern, da alle Prozesse die gleiche Kernelspace-Adresse gemeinsam nutzen.

Wie können wir verhindern, dass der Kernelzustand auf den Adressraum des Benutzerzustands zugreift, wenn ein Prozess in den Kernelzustand wechselt (Unterbrechung, Ausnahme, Systemaufruf usw.)? Tatsächlich ist es nicht schwer herauszufinden, dass wir nur den Wert von ttbr0_el1 ändern müssen, damit er auf eine ungültige Zuordnung verweist. Daher bereiten wir zu diesem Zweck eine spezielle Seitentabelle vor. Die Seitentabellengröße beträgt 4 KB Speicher und ihre Werte sind alle 0. Wenn der Prozess in den Kernelmodus wechselt, kann das Ändern des Werts von ttbr0_el1 in die Adresse der Seitentabelle sicherstellen, dass der Zugriff auf die Benutzerbereichsadresse unzulässig ist. Weil der Wert der Seitentabelle unzulässig ist. Dieser spezielle Seitentabellenspeicher wird vom Linker-Skript zugewiesen.

#define RESERVED_TTBR0_SIZE (SEITENGRÖSSE) 
ABSCHNITTE 
{ 
        reserviert_ttbr0 = .; 
        . += RESERVIERT_TTBR0_SIZE; 
        swapper_pg_dir = .; 
        . += SWAPPER_DIR_SIZE; 
        swapper_pg_end = .; 
}

Diese spezielle Seitentabelle befindet sich zusammen mit der Kernel-Seitentabelle. Der Größenunterschied zu swapper_pg_dir beträgt nur 4 KB. Der Inhalt des 4K-Speicherplatzes ab der Adresse reserved_ttbr0 wird gelöscht.

Wenn wir in den Kernelstatus wechseln, schalten wir ttbr0_el1 über __uaccess_ttbr0_disable um, um den Benutzerbereich-Adresszugriff zu deaktivieren, und aktivieren den Benutzerbereich-Adresszugriff über _uaccess_ttbr0_enable, wenn Zugriff erforderlich ist. Die beiden Makrodefinitionen sind nicht kompliziert. Nehmen wir _uaccess_ttbr0_disable als Beispiel, um das Prinzip zu verdeutlichen. Die Definition lautet wie folgt:

Makro __uaccess_ttbr0_disable, tmp1 
    mrs \tmp1, ttbr1_el1 // swapper_pg_dir (1) 
    bic \tmp1, \tmp1, #TTBR_ASID_MASK 
    sub \tmp1, \tmp1, #RESERVED_TTBR0_SIZE // reserved_ttbr0 kurz vor 
                                                // swapper_pg_dir (2) 
    msr ttbr0_el1, \tmp1 // setze reserviertes TTBR0_EL1 (3) 
    isb 
    füge \tmp1, \tmp1, #RESERVED_TTBR0_SIZE hinzu 
    msr ttbr1_el1, \tmp1 // reservierte ASID festlegen 
    isb 
.endm
  • ttbr1_el1 speichert die Basisadresse der Kernel-Seitentabelle, sein Wert ist also swapper_pg_dir.
  • swapper_pg_dir minus RESERVED_TTBR0_SIZE ist die oben beschriebene spezielle Seitentabelle.
  • Durch die Änderung von ttbr0_el1, sodass es auf diese spezielle Basisadresse der Seitentabelle verweist, können Sie natürlich sicherstellen, dass nachfolgende Zugriffe auf Benutzeradressen unzulässig sind.

Die C-Sprachimplementierung, die __uaccess_ttbr0_disable entspricht, finden Sie hier. Wie kann dem Kernelmodus der Zugriff auf Benutzerbereichsadressen gestattet werden? Es ist auch sehr einfach, nämlich die umgekehrte Operation von __uaccess_ttbr0_disable, wodurch ttbr0_el1 eine gültige Basisadresse für die Seitentabelle erhält. Es ist nicht nötig, es hier zu wiederholen. Was wir jetzt wissen müssen, ist, dass, wenn CONFIG_ARM64_SW_TTBR0_PAN konfiguriert ist, die Schnittstelle copy_{to,from}_user() dem Kernelmodus den Zugriff auf den Benutzerbereich vor dem Kopieren ermöglicht und die Möglichkeit des Kernelmodus, auf den Benutzerbereich zuzugreifen, nachdem der Kopiervorgang abgeschlossen ist, deaktiviert. Daher ist die Verwendung von copy_{to,from}_user() der konventionelle Ansatz. Dies spiegelt sich hauptsächlich in Sicherheitsüberprüfungen und der Sicherheitszugriffsverarbeitung wider. Dies ist die erste Funktion, die es gegenüber memcpy() hat, und eine weitere wichtige Funktion wird später eingeführt.

Wir können nun die Fragen beantworten, die im vorigen Abschnitt noch offen geblieben sind. Wie kann ich memcpy() weiterhin verwenden? Jetzt ist es ganz einfach. Erlauben Sie dem Kernelmodus vor dem Aufruf von memcpy() den Zugriff auf die Benutzerbereichsadresse über uaccess_enable_not_uao(), rufen Sie memcpy() auf und deaktivieren Sie schließlich die Möglichkeit des Kernelmodus, über uaccess_disable_not_uao() auf den Benutzerbereich zuzugreifen.

3. Testen

Die oben genannten Testfälle basieren alle auf dem Test der Weitergabe legaler Adressen im Benutzerbereich. Was ist eine legale Benutzerbereichsadresse? Der im virtuellen Adressraum enthaltene Adressbereich, der vom Benutzerraum über den Systemaufruf angefordert wurde, ist eine gültige Adresse (unabhängig davon, ob physische Seiten zugewiesen werden, um eine Zuordnungsbeziehung herzustellen). Da wir ein Schnittstellenprogramm schreiben, müssen wir auch die Robustheit des Programms berücksichtigen. Wir können nicht davon ausgehen, dass alle von Benutzern übergebenen Parameter zulässig sind. Wir sollten das Auftreten illegaler Übertragungen von Teilnehmern vorhersehen und uns im Voraus darauf vorbereiten, das heißt, uns auf schlechte Zeiten vorbereiten.

Wir verwenden zuerst den Testfall von memcpy() und übergeben eine zufällige ungültige Adresse. Beim Testen stellte sich heraus, dass es Kernel-Oops auslösen würde. Verwenden Sie weiterhin copy_{to,from}_user() anstelle des memcpy()-Tests. Der Test ergab, dass read() lediglich einen Fehler zurückgibt, aber keinen Kernel-Oops auslöst. Dies ist das gewünschte Ergebnis. Schließlich sollte eine Anwendung nicht in der Lage sein, einen Kernel-Oops auszulösen. Was ist das Umsetzungsprinzip dieses Mechanismus?

Nehmen wir copy_to_user() als Beispiel. Der Funktionsaufrufablauf ist:

copy_to_user()->_copy_to_user()->raw_copy_to_user()->__arch_copy_to_user()

_arch_copy_to_user() ist auf der ARM64-Plattform in Assemblercode implementiert und dieser Teil des Codes ist kritisch.

Ende .req x5 
EINTRAG(__arch_copy_to_user) 
        uaccess_enable_not_uao x3, x4, x5 
        Ende hinzufügen, x0, x2 
#include "Kopiervorlage.S" 
        uaccess_disable_not_uao x3, x4 
        mov x0, #0 
        zurück 
ENDPROC(__arch_copy_to_user) 
        .Abschnitt .Fixup,"ax" 
        .ausrichten 2 
9998: sub x0, end, dst // Bytes nicht kopiert 
        zurück 
        .vorherige
  • Uaccess_enable_not_uao und uaccess_disable_not_uao sind die Schalter für den Kernelmodus, um oben erwähnten Benutzerbereich zuzugreifen.
  • Die Datei copy_template.S ist die Assemblerimplementierung der Funktion memcpy(). Sie werden es deutlich verstehen, wenn Sie sich später den Implementierungscode von memcpy() ansehen.
  • .section.fixup,“ax” definiert einen Abschnitt namens „.fixup“ mit den Berechtigungen ax (‚a‘ verschiebbares Segment, ‚x‘ ausführbares Segment). Der Befehl auf der Bezeichnung 9998 dient der Nachbearbeitung. Erinnern Sie sich an die Bedeutung des Rückgabewerts von copy_{to,from}_user()? Gibt 0 zurück, wenn das Kopieren erfolgreich war, andernfalls die Anzahl der noch zu kopierenden Bytes. Diese Codezeile berechnet die Anzahl der verbleibenden Bytes, die nicht kopiert wurden. Wenn wir auf eine ungültige Benutzerbereichsadresse zugreifen, wird definitiv ein Seitenfehler ausgelöst. In diesem Fall wurde der im Kernelzustand aufgetretene Seitenfehler bei der Rückkehr nicht behoben, sodass es definitiv unmöglich ist, zur Adresse zurückzukehren, an der die Ausnahme aufgetreten ist, und mit der Ausführung fortzufahren. Daher hat das System zwei Möglichkeiten: Die erste Möglichkeit ist ein Kernel-Oops, bei dem ein SIGSEGV-Signal an den aktuellen Prozess gesendet wird. Die zweite Möglichkeit besteht darin, nicht zu der Adresse zurückzukehren, bei der die Ausnahme aufgetreten ist, sondern eine reparierte Adresse für die Rückkehr auszuwählen. Wenn Sie memcpy() verwenden, haben Sie nur die erste Option. Aber copy_{to,from}_user() kann eine zweite Option haben. Das .fixup-Segment wird verwendet, um diese Reparaturfunktion zu implementieren. Wenn während des Kopiervorgangs auf eine ungültige Benutzerbereichsadresse zugegriffen wird, wird die von do_page_fault() zurückgegebene Adresse zur Nummer 9998. Zu diesem Zeitpunkt kann die Länge der verbleibenden, nicht kopierten Bytes berechnet werden und das Programm kann mit der Ausführung fortfahren.

Verglichen mit den Ergebnissen der vorherigen Analyse kann _arch_copy_to_user() tatsächlich ungefähr der folgenden Beziehung entsprechen.

uaccess_enable_not_uao(); 
memcpy(ubuf, kbuf, Größe); == __arch_copy_to_user(ubuf, kbuf, Größe); 
uaccess_disable_not_uao();

Lassen Sie mich zunächst eine Nachricht einfügen, um zu erklären, warum copy_template.S memcpy() ist. memcpy() wird durch Assemblercode auf der ARM64-Plattform implementiert. Es ist in der Datei arch/arm64/lib/memcpy.S definiert.

.schwaches memcpy 
EINTRAG(__memcpy) 
EINTRAG(memcpy) 
#include "Kopiervorlage.S" 
        zurück 
ENDPIPROC(memcpy) 
ENDPROC(__memcpy)

Daher sind die Funktionsdefinitionen von memcpy() und __memcpy() offensichtlich identisch. Und die memcpy()-Funktion wird als schwach deklariert, sodass die memcpy()-Funktion neu geschrieben werden kann (etwas weit hergeholt). Lassen Sie mich noch etwas weiter gehen. Warum Assembly verwenden? Warum nicht die Funktion memcpy() in der Datei lib/string.c verwenden? Dies dient natürlich dazu, die Ausführungsgeschwindigkeit von memcpy() zu optimieren. Die Funktion memcpy() in der Datei lib/string.c kopiert Bytes (selbst die beste Hardware kann durch schlechten Code ruiniert werden). Die meisten Prozessoren heutzutage sind jedoch 32- oder 64-Bit-Prozessoren, es ist also möglich, 4 Bytes, 8 Bytes oder sogar 16 Bytes zu kopieren (unter Berücksichtigung der Adressausrichtung). Kann die Ausführungsgeschwindigkeit erheblich verbessern. Daher verwendet die ARM64-Plattform eine Assemblerimplementierung. Weitere Informationen hierzu finden Sie in diesem Blog „Memcpy-Optimierung und Implementierung von ARM64“.

Kommen wir zum Punkt zurück und wiederholen: Wenn der Kernelstatus auf eine Benutzerbereichsadresse zugreift und ein Seitenfehler ausgelöst wird, behebt der Kernelstatus die Ausnahme, als wäre nichts passiert (er weist physischen Speicher zu und stellt eine Seitentabellenzuordnungsbeziehung her), solange die Benutzerbereichsadresse zulässig ist. Wenn Sie jedoch auf eine illegale Benutzerbereichsadresse zugreifen, wählen Sie Pfad 2 und versuchen Sie, sich zu rehabilitieren. Diese Möglichkeit besteht darin, die Abschnitte .fixup und __ex_table zu verwenden. Wenn es keine Möglichkeit gibt, die Situation zu retten, können Sie nur ein SIGSEGV-Signal an den aktuellen Prozess senden. Darüber hinaus kann der Fehler ein Kernel-Oops oder eine Kernel-Panik sein (abhängig von der Kernel-Konfigurationsoption CONFIG_PANIC_ON_OOPS). Wenn im Kernelmodus auf eine ungültige Benutzerbereichsadresse zugegriffen wird ,do_page_fault() schließlich beim Label no_context zu do_kernel_fault().

statisches void __do_kernel_fault(unsignierte lange Adresse, unsignierte int esr, 
                              Struktur pt_regs *regs) 
{ 
        /* 
         * Sind wir darauf vorbereitet, diesen Kernelfehler zu beheben? 
         * Wir sind mit ziemlicher Sicherheit nicht darauf vorbereitet, Anweisungsfehler zu beheben. 
         */ 
        wenn (!is_el1_instruction_abort(esr) && fixup_exception(regs)) 
                zurückkehren; 
        /* … */ 
}

fixup_exception() ruft anschließend search_exception_tables() auf, das nach dem Abschnitt _extable sucht. Das Segment __extable speichert die Ausnahmetabelle und jeder Eintrag speichert die Ausnahmeadresse und die entsprechende Reparaturadresse. Beispielsweise wird die Adresse der oben erwähnten Anweisung 9998:subx0,end,dst gefunden und die Rücksprungadresse der Funktion do_page_fault() geändert, um die Sprungreparaturfunktion zu erreichen. Tatsächlich besteht der Suchvorgang darin, basierend auf der Adressadresse des Problems herauszufinden, ob im Segment _extable (Ausnahmetabelle) ein entsprechender Ausnahmetabelleneintrag vorhanden ist. Wenn dies der Fall ist, bedeutet dies, dass es repariert werden kann. Da die Implementierungsmethoden von 32-Bit-Prozessoren und 64-Bit-Prozessoren unterschiedlich sind, beginnen wir zunächst mit dem Implementierungsprinzip der Ausnahmetabelle für 32-Bit-Prozessoren.

Die erste und letzte Adresse des _extable-Segments sind __start___ex_table und __stop___ex_table (definiert in include/asm-generic/vmlinux.lds.h). Dieses Speichersegment kann als Array betrachtet werden, dessen jedes Element vom Typ struct exception_table_entry ist, das die Adresse aufzeichnet, an der die Ausnahme aufgetreten ist, und die entsprechende Reparaturadresse.

                        Ausnahmetabellen 
__start___ex_table --> +---------------+ 
                       | Eintrag | 
                       +-----------------+ 
                       | Eintrag | 
                       +-----------------+ 
                       | ... | 
                       +-----------------+ 
                       | Eintrag | 
                       +-----------------+ 
                       | Eintrag | 
__stop___ex_table --> +---------------+

Auf einem 32-Bit-Prozessor wird die Struktur exception_table_entry wie folgt definiert:

Struktur Ausnahmetabelleneintrag { 
        unsignierte lange Inserate, Fixup; 
};

Eines muss klargestellt werden: Auf einem 32-Bit-Prozessor beträgt „unsigned long“ 4 Bytes. insn und fixup speichern die Adresse des Ausnahmeauftretens bzw. die entsprechende Fixup-Adresse. Suche nach der entsprechenden Reparaturadresse gemäß der Ausnahmeadresse ex_addr (bei Nichtgefunden wird 0 zurückgegeben). Der schematische Code lautet wie folgt:

unsigned long search_fixup_addr32(unsigned long ex_addr) 
{ 
        const struct Ausnahmetabelleneintrag *e; 
        für (e = __start___ex_table; e < __stop___ex_table; e++) 
                wenn (ex_addr == e->insn) 
                        Rückgabewert e->fixup; 
        gebe 0 zurück; 
}


Auf 32-Bit-Prozessoren ist das Erstellen eines Ausnahmetabelleneintrags relativ einfach. Für jeden Befehl, der auf die Benutzerbereichsadresse im Assemblercode copy{to,from}user() zugreift, wird ein Eintrag erstellt. Insn speichert die dem aktuellen Befehl entsprechende Adresse und Fixup speichert die dem Reparaturbefehl entsprechende Adresse.

Wenn wir mit der Entwicklung von 64-Bit-Prozessoren fortfahren und diese Methode weiterhin verwenden, werden wir zwangsläufig doppelt so viel Speicher wie bei 32-Bit-Prozessoren benötigen, um die Ausnahmetabelle zu speichern (da zum Speichern einer Adresse 8 Bytes erforderlich sind). Daher verwendet der Kernel eine andere Methode zur Implementierung. Auf 64-Prozessoren wird struct exception_table_e wie folgt definiert:

Struktur Ausnahmetabelleneintrag { 
        int insn, fixup; 
};

Der von jedem Ausnahmetabelleneintrag belegte Speicher entspricht dem eines 32-Bit-Prozessors, die Speichernutzung bleibt also unverändert. Allerdings hat sich die Bedeutung von „insn“ und „fixup“ geändert. insn und fixup speichern jeweils die Adresse, an der die Ausnahme aufgetreten ist, und den Offset der Reparaturadresse relativ zur aktuellen Adresse des Strukturmitglieds (etwas verwirrend). Beispielsweise wird gemäß der Ausnahmeadresse ex_addr die entsprechende Reparaturadresse gesucht (wenn sie nicht gefunden wird, wird 0 zurückgegeben), und der schematische Code lautet wie folgt:

unsigned long search_fixup_addr64(unsigned long ex_addr) 
{ 
        const struct Ausnahmetabelleneintrag *e; 
        für (e = __start___ex_table; e < __stop___ex_table; e++) 
                wenn (ex_addr == (unsigned long)&e->insn + e->insn) 
                        Rückgabewert (vorzeichenloser Long-Wert) und e->fixup + e->fixup; 
        gebe 0 zurück; 
}


Daher konzentrieren wir uns auf die Konstruktion von exception_table_entry. Wir müssen für jeden Speicherzugriff auf eine Benutzerbereichsadresse einen Ausnahmetabelleneintrag erstellen und ihn in das _extable-Segment einfügen. Beispielsweise die folgenden Montageanweisungen (die den Montageanweisungen entsprechenden Adressen sind willkürlich geschrieben, machen Sie sich also keine Gedanken darüber, ob sie richtig oder falsch sind. Das Verständnis der Prinzipien ist der Schlüssel).

0xffff000000000000: ldr x1, [x0] 
0xffff000000000004: füge x1, x1, #0x10 hinzu 
0xffff000000000008: ldr x2, [x0, #0x10] 
/* … */ 
0xffff000040000000: mov x0, #0xffffffffffffffff2 // -14 
0xffff000040000004: ret

Angenommen, das x0-Register enthält die Benutzerbereichsadresse, sodass wir einen Ausnahmetabelleneintrag für den Assemblerbefehl an der Adresse 0xffff000000000000 erstellen müssen. Wir erwarten, dass, wenn x0 eine ungültige Benutzerbereichsadresse ist, die vom Sprung zurückgegebene Reparaturadresse 0xffff000040000000 ist. Der Einfachheit halber nehmen wir an, dass dies die Erstellung des ersten Eintrags ist und der Wert von __start___ex_table 0xffff000080000000 ist. Dann lauten die Werte der Insn- und Fixup-Mitglieder des ersten Ausnahmetabelleneintrags: 0x80000000 und 0xbffffffc (beide Werte sind negativ). Daher wird für jede Anweisung zum Zugriff auf eine Userspace-Adresse im Assemblercode copy{to,from}user() ein Eintrag erstellt. Daher muss die Assembleranweisung an der Adresse 0xffff000000000008 auch einen Ausnahmetabelleneintrag erstellen.

Was genau passiert also, wenn der Kernelmodus auf eine ungültige Benutzerbereichsadresse zugreift? Der obige Analyseprozess kann wie folgt zusammengefasst werden:

  • 0xffff000000000000:ldr x1,[x0]
  • MMU löst eine Ausnahme aus
  • Die CPU ruft do_page_fault() auf
  • do_page_fault() ruft search_exception_table() auf (regs->pc == 0xffff000000000000)
  • Suchen Sie im Segment _extable nach 0xffff000000000000 und geben Sie die Reparaturadresse 0xffff000040000000 zurück.
  • do_page_fault() ändert die Rücksprungadresse der Funktion (regs->pc = 0xffff000040000000) und gibt zurück
  • Das Programm wird weiter ausgeführt und behandelt den Fehler
  • Ändern Sie den Funktionsrückgabewert x0 = -EFAULT (-14) und geben Sie zurück (ARM64 übergibt den Funktionsrückgabewert über x0).

IV. Fazit

Jetzt ist es Zeit zur Überprüfung und Zusammenfassung, und die Überlegungen zu copy_{to,from}_user() enden hier. Lassen Sie uns diesen Artikel mit einer Zusammenfassung beenden.

Unabhängig davon, ob im Kernelmodus oder im Benutzermodus auf eine legitime Benutzerbereichsadresse zugegriffen wird und die virtuelle Adresse keine Zuordnungsbeziehung zur physischen Adresse herstellt, ist der Seitenfehlerprozess nahezu derselbe. Er hilft uns dabei, den physischen Speicher zu beantragen und eine Zuordnungsbeziehung herzustellen. In diesem Fall sind memcpy() und copy_{to,from}_user() ähnlich.

Wenn der Kernelstatus auf eine ungültige Benutzerbereichsadresse zugreift, wird die Reparaturadresse basierend auf der Ausnahmeadresse gefunden. Diese Methode zum Beheben der Ausnahme stellt keine Adresszuordnungsbeziehung her, ändert jedoch die Rücksprungadresse von do_page_fault(). memcpy() kann dies nicht.

Wenn CONFIG_ARM64_SW_TTBR0_PAN oder CONFIG_ARM64_PAN aktiviert ist (nur gültig, wenn die Hardware dies unterstützt), können wir nur die Schnittstelle copy_{to,from}_user() verwenden. Die direkte Verwendung von memcpy() ist nicht möglich.

Abschließend möchte ich sagen, dass memcpy() in einigen Fällen sogar einwandfrei funktionieren kann. Dies wird jedoch ebenfalls nicht empfohlen und stellt keine gute Programmierpraxis dar. Bei der Dateninteraktion im Benutzerbereich und im Kernelbereich müssen wir eine Schnittstelle verwenden, die copy_{to,from}_user() ähnelt. Warum sind sie ähnlich? Denn es gibt zwar noch andere Schnittstellen für die Dateninteraktion im Kernel- und Benutzerbereich, diese sind aber nicht so bekannt wie copy_{to,from}_user(). Beispiel: {get,put}_user().

Dies ist das Ende dieses Artikels über copy_{to, from}_user(). Weitere relevante Kopier- und Benutzerinhalte finden Sie in den vorherigen Artikeln von 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird!

Das könnte Sie auch interessieren:
  • Einführung in die Containerfunktion of() in der Linux-Kernel-Programmierung
  • Detaillierte Analyse des Linux-Kernel-Makros container_of
  • Detaillierte Erklärung der container_of-Funktion im Linux-Kernel
  • VMware Workstation-Installation (Linux-Kernel) Kylin-Grafik-Tutorial
  • Detaillierte Erklärung des Linux-Kernel-Makros Container_Of

<<:  Beispielcode zur Implementierung des wellenförmigen Wasserballeffekts mit CSS

>>:  Detaillierte Erklärung der vier Arten von MySQL-Verbindungen und Multi-Table-Abfragen

Artikel empfehlen

Implementierung der Ausführung von SQL Server mit Docker

Jetzt ist .net Core plattformübergreifend und jed...

So richten Sie Windows Server 2019 ein (mit Bildern und Text)

1. Installation von Windows Server 2019 Installie...

Vue führt weltweit SCSS (Mixin) ein

Inhaltsverzeichnis 1. mixin.scss 2. Einzeldateinu...

Fallstudie zum Löschen und Neuinstallieren eines Mac-Knotens

Mac-Knoten löschen und neu installieren löschen K...

Verwenden Sie Docker, um ein Git-Image mithilfe des Klon-Repositorys zu erstellen

Überblick Ich verwende Docker seit über einem Jah...

JavaScript Canvas realisiert farbenfrohen Sonnenhalo-Effekt

In diesem Artikelbeispiel wird der spezifische Co...

Den praktischen Wert der CSS-Eigenschaft *-gradient erkunden

Lassen Sie mich zunächst eine interessante Eigens...

So installieren Sie Postgres 12 + pgadmin im lokalen Docker (unterstützt Apple M1)

Inhaltsverzeichnis einführen Unterstützt Intel-CP...

Untersuchung der Eingabetastenfunktion vom Typ „Datei“

<br />Beim Hochladen auf manchen Websites wi...

Häufige Browserkompatibilitätsprobleme (Zusammenfassung)

Browserkompatibilität ist nichts anderes als Stil...

So legen Sie die Tabellenbreite in IE8 und Chrome fest

Wenn die oben genannten Einstellungen in IE8 und C...

Beispielcode für CSS-Stacking und Z-Index

Kaskadierung und kaskadierende Ebenen HTML-Elemen...

So verwenden Sie Nginx als Load Balancer für MySQL

Hinweis: Die Nginx-Version muss 1.9 oder höher se...

Mysql teilt Zeichenfolge durch gespeicherte Prozedur in Array auf

Um einen String in ein Array aufzuteilen, müssen ...

Verwenden von nginx + fastcgi zum Implementieren eines Bilderkennungsservers

Hintergrund Ein spezielles Gerät wird verwendet, ...