Ein Systemaufruf ist ein Mechanismus, mit dem eine Anwendung die vom Betriebssystem bereitgestellten Funktionen verwendet. Es ist jedoch wichtig, die Systemaufrufe zu kennen, um die Funktionsweise der Anwendung zu verstehen.
Dies liegt daran, dass fast alle wichtigen Dinge im Betrieb der Anwendung mithilfe von Systemaufrufen realisiert werden. Beispielsweise werden Netzwerkkommunikation, Dateieingabe / -ausgabe, Erstellung neuer Prozesse, Kommunikation zwischen Prozessen, Containererstellung usw. mithilfe von Systemaufrufen realisiert. Im Gegenteil, das einzige, was eine Anwendung ohne Systemaufrufe tun kann, ist die Berechnung auf der CPU und die Eingabe / Ausgabe in / aus dem Speicher.
Der in diesem Artikel behandelte Inhalt befasst sich mit der allgemeinen Natur und Mechanik von Systemaufrufen. Zunächst werde ich erklären, was ein Systemaufruf ist und wie er realisiert wird. Darüber hinaus zeigen wir Ihnen, wie Sie Systemaufrufe direkt verwenden, ohne eine Bibliothek zu durchlaufen, oder wie Sie tief in den Kernel eindringen, um die Implementierung von Systemaufrufen zu untersuchen.
Der Inhalt ist wie folgt.
Gemäß Linux Kernel Development usw. wird ein Mechanismus namens Systemaufruf eingeführt. Es gibt zwei Vorteile:
Systemaufrufe bieten Anwendungen eine einfache, abstrahierte Schnittstelle zum Bearbeiten von Hardware. Dadurch muss der Anwendungscode die zugrunde liegenden Hardwaredetails nicht mehr kennen.
Beispielsweise ist der Systemaufruf write
eine übliche Schnittstelle zum Schreiben einer Byte-Zeichenfolge in etwas.
Es gibt eine Vielzahl von Dingen, die als Schreibziele angegeben werden können, dh als Dateien behandelt werden können, z. B. verschiedene Ausgabegeräte wie Pipes für die Kommunikation zwischen Prozessen, Sockets für die Netzwerkkommunikation, Monitore usw. sowie verschiedene Arten von Dateisystemen. Und so weiter.
Die Grundphilosophie von Linux und Unix ist alles ist eine Datei
, aber verschiedene Typen, die für die Eingabe und Ausgabe verwendet werden, einschließlich write
. Es kann gesagt werden, dass der Systemaufruf von die Verkörperung davon ist.
Systemaufrufe vermitteln zwischen der Anwendung und den vom Betriebssystem verwalteten Ressourcen und haben die Aufgabe, zu verhindern, dass die Anwendung die Ressource falsch oder auf eine Weise verwendet, die ein Sicherheitsproblem darstellt. Auf diese Weise können Anwendungen Ressourcen sicher und sicher verwenden.
Als Beispiel für die sichere Verwendung von Ressourcen, z. B. Zuweisung des Speicherbereichs nach Prozessen (unter Verwendung von mmap
) Es wird____geben. Eine Hardwareressource namens Speicher wird mit anderen Prozessen und Betriebssystemen gemeinsam genutzt. Bei falscher Verwendung besteht die Gefahr, dass andere Prozesse und Betriebssysteme zerstört werden. Durch diesen Systemaufruf kann das Betriebssystem jedem Prozess den Speicherbereich auf sichere Weise zuweisen, wenn ein Prozess einen Speicherbereich zuweisen möchte.
Ein Beispiel für die sichere Verwendung von Ressourcen wäre die Zugriffssteuerung auf Dateien basierend auf Berechtigungsinformationen.
Was macht die Anwendung beim Aufrufen eines Systemaufrufs? Was ist der Unterschied zu einem normalen Funktionsaufruf? Hier erklären wir, wie die Anwendung den Systemaufruf aufruft.
Es gibt drei Schritte, die eine Anwendung beim Aufrufen eines Systemaufrufs ausführt:
Im Folgenden wird das obige Verfahren anhand des Codes, der Zeichen auf dem Bildschirm ausgibt (Standardausgabe), ausführlich erläutert.
Der Systemaufruf write
wird für die Standardausgabe und das Schreiben in eine Datei verwendet, aber ein Beispiel, das diesen Systemaufruf aufruft Schauen wir uns die Schritte an, in denen eine Anwendung einen Systemaufruf mit Code aufruft.
Starten Sie zunächst gcc image, mit dem der Beispielcode ausgeführt wird, als Container.
$ docker container run -it --rm gcc /bin/bash
Erstellen Sie eine Datei hi.s
mit dem folgenden Assemblycode im gestarteten Container. Ich werde den Code später erklären.
# cat <<EOF > hi.s
.intel_syntax noprefix
.global main
main:
push rbp
mov rbp, rsp
push 0xA216948
mov rax, 1
mov rdi, 1
mov rsi, rsp
mov rdx, 4
syscall
mov rsp, rbp
pop rbp
ret
EOF
Wenn Sie es kompilieren und ausführen, werden die Zeichen wie unten gezeigt auf dem Bildschirm ausgegeben.
# gcc -o hi.o hi.s; ./hi.o
Hi!
Die Methode zum Aufrufen von Systemaufrufen unterscheidet sich je nach CPU-Spezifikation / Architektur geringfügig. Der obige Code ist jedoch der beliebteste x86-64. Dies ist ein Beispiel für den Aufruf des Systemaufrufs "write" in der Architektur.
Bevor wir den oben eingeführten Beispielcode erläutern, erklären wir zunächst, wie ein Systemaufruf auf x86-64 aufgerufen wird. Was Sie mit anderen Architekturen machen, ändert sich nicht so sehr.
Das Aufrufen eines Systemaufrufs auf x86-64 erfolgt in drei Schritten: 1. Stellen Sie die Systemrufnummer im Register "rax" ein. 2. Legen Sie das Argument (falls vorhanden) fest, das Sie an den Systemaufruf im Register übergeben möchten. 3. Rufen Sie die Anweisung / Anweisung von "syscall" auf. Schritt 1 Zuerst Schritt 1 Hier speichert jedoch ein Register mit dem Namen "rax" (= ein in die CPU eingebauter Speicher von etwa 16 bis 64 Bit, auf den von der CPU aus mit extrem hoher Geschwindigkeit zugegriffen werden kann) eine Nummer, die als Systemaufrufnummer bezeichnet wird und den aufzurufenden Systemaufruf angibt. Machen. Mit dieser Nummer können Sie festlegen, welchen Systemaufruf der Kernel ausführen soll.
Der der Systemrufnummer entsprechende Systemaufruf wird beispielsweise wie folgt angegeben.
system call number | Name des Systemaufrufs | Inhalt |
---|---|---|
0 | read | Lesen |
1 | write | Export |
2 | open | Datei öffnen |
57 | fork | Starten Sie einen neuen Prozess |
Eine umfassende Tabelle mit mehr als den oben genannten Beispielen finden Sie unter hier.
Im Beispielcode sind die Teile, die diesem Schritt entsprechen ,:
mov rax, 1
Als nächstes speichern wir in Schritt 2 die Argumente, die an den Systemaufruf übergeben werden sollen, im Register. Sie können bis zu 6 Argumente übergeben, und die Register "rdi", "rsi", "rdx", "r10", "r9" und "r8" sind in der Reihenfolge vom ersten Argument an zu verwenden.
Für den Systemaufruf "Schreiben" werden die zu übergebenden Argumente und Register wie folgt angegeben:
Registername | rdi | rsi | rdx |
---|---|---|---|
Streit | Dateideskriptor | Startadresse der zu schreibenden Byte-Zeichenfolge | Zu exportierende Byte-String-Größe |
Hier können Sie überprüfen, welche Argumente Sie für andere Systemaufrufe verwenden sollten [https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/].
Übrigens ist ein Dateideskriptor (der im Register "rdi" festgelegt ist) wie eine Portnummer, die jedem Prozess zugeordnet ist, der die Eingabe / Ausgabe des Prozesses steuert. Standardmäßig kann der Dateideskriptor drei Werte von 0 bis 2 verwenden und entspricht 0: Standardeingabe, 1: Standardausgabe bzw. 2: Standardfehler. Wenn Sie eine Datei oder einen Socket öffnen, werden nacheinander neue Dateideskriptoren 3, 4, 5 usw. zugewiesen, und Systemaufrufe wie Schreiben, Schreiben und Lesen, Lesen werden für sie ausgeführt (Systemaufruf). Kann als Argument an) übergeben werden.
Im Beispielcode sind die Teile, die diesem Schritt entsprechen ,:
mov rdi, 1
mov rsi, rsp
mov rdx, 4
Schritt 3 gibt eine Anweisung / Anweisung mit dem Namen "syscall" aus.
Dies führt dazu, dass die CPU die Ausführung des Anwendungscodes unterbricht, in einen Modus wechselt, in dem der Kernelcode ausgeführt wird, und dann zu dem Code springt, der den Systemaufruf im Kernel ausführt (Systemaufrufhandler).
Wenn Sie den Befehl syscall
verlassen, wird der Rückgabewert im Register rax
eingestellt und kann bei Bedarf referenziert werden.
Dieser Teil wird später in diesem Artikel unter [Implementieren von Systemaufrufen und interner Implementierung] ausführlich erläutert.
Im Beispielcode sind die Teile, die diesem Schritt entsprechen ,:
syscall
Ich habe einen Kommentar hinzugefügt, damit ich die Bedeutung des Beispielcodes verstehen kann. Der Punkt ist die Linie mit der Bezeichnung "syscall" und kurz davor.
.intel_syntax noprefix #Code-Format
.global main #Bezeichnung des Ausführungsstartpunktes
main:
#Vorverarbeitung
push rbp
mov rbp, rsp
#String'Hi!'Auf dem Stapel
push 0xA216948
#Schritt 1 mit Systemrufnummer'1'(=Unterstützt das Schreiben)Konkretisieren
mov rax, 1
#Schritt 2 Legen Sie die Argumente fest, die an den Schreibsystemaufruf im Register übergeben werden sollen
mov rdi, 1 #1 im zu exportierenden Dateideskriptor(Standardausgabe)einstellen
mov rsi, rsp #Startadresse des Stapels(Brief`H`Ist enthalten)einstellen
mov rdx, 4 #Legen Sie die Größe der Ausgabezeichenfolge fest
#Schritt 3 Führen Sie einen Systemaufruf aus
syscall
#Nachbearbeitung
mov rsp, rbp
pop rbp
ret
Übrigens, wenn Sie ein wenig über den Teil des obigen Codes hinzufügen, der in Schritt 2 die Startadresse der Zeichenfolge übergibt.
mov rsi, rsp #Startadresse des Stapels(Brief`H`Ist enthalten)einstellen
Die Zeichenfolge wird auf dem Stapel gespeichert, aber das Register "rsp" ist ein spezielles Register, das auf die Startadresse des Stapels zeigt. Wenn Sie den Wert von "rsp" auf "rsi" setzen, bedeutet dies, dass die Startadresse der Zeichenfolge an den Systemaufruf übergeben wird.
Systemaufrufe werden normalerweise als C-Sprachbibliothek bereitgestellt. Wenn Sie Systemaufrufe verwenden, können Sie diese Wrapper-Bibliothek grundsätzlich verwenden, und Sie müssen den Assembly-Code nicht direkt schreiben.
Beispielsweise wird "write (2)" als C-Funktion mit der folgenden Signatur definiert: ](Http://man7.org/linux/man-pages/man2/write.2.html)
ssize_t write(int fd, const void *buf, size_t count);
Wenn Sie andererseits die C-Bibliothek nicht verwenden möchten, müssen Sie Ihre eigenen Systemaufrufaufrufe als Assemblycode implementieren, wie Sie es im obigen Beispiel getan haben. Die Go-Sprache verfolgt beispielsweise einen solchen Ansatz.
golang/sys/unix/asm_linux_amd64.s
TEXT ·SyscallNoError(SB),NOSPLIT,$0-48
CALL runtime·entersyscall(SB)
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
MOVQ AX, r1+32(FP)
MOVQ DX, r2+40(FP)
CALL runtime·exitsyscall(SB)
RET
Um den obigen Code zu ergänzen, ist ein unbekanntes Register wie "AX" ein Alias, der auf den 16-Bit-Teil "wie das" Rax "-Register verweist.
Wenn ein Systemaufruf ausgegeben wird, springt die CPU vom aktuell ausgeführten Code zum Code im Kernel. Wie wird eine solche Funktion zum Springen des in der Mitte auszuführenden Codes auf der CPU realisiert? Um dies zu verstehen, müssen wir zunächst verstehen, wie die CPU den Code (Maschinensprache) ausführt.
Die Ausführung der Maschinensprache auf der CPU erfolgt durch Wiederholen der folgenden Schritte.
Anweisungen sind Bytes, die die CPU als einzelne Anweisung interpretieren kann und eine Eins-zu-Eins-Entsprechung mit einer einzelnen Zeile des Assembler-Codes aufweist. Beispielsweise ist in dem oben angegebenen Assembler-Code die Entsprechung zwischen Maschinensprache und Anweisungen wie folgt.
# objdump -d -M intel ./hi.o | grep syscall -B 4
66c: 48 c7 c0 01 00 00 00 mov rax,0x1
673: 48 c7 c7 01 00 00 00 mov rdi,0x1
67a: 48 89 e6 mov rsi,rsp
67d: 48 c7 c2 20 00 00 00 mov rdx,0x20
684: 0f 05 syscall
Außerdem wird das Register, in dem die Adresse des aktuell ausgeführten Befehls gespeichert ist, als Programmzählerregister oder Befehlszeigerregister bezeichnet, und wenn ein Befehl ausgeführt wird, wird er unmittelbar danach hinzugefügt und auf den Wert der Adresse des Befehls aktualisiert. Dadurch wird der Code nacheinander ausgeführt.
Es ist eine Funktion, den von der CPU in der Mitte ausgeführten Code zu überspringen, aber dies gibt eine Anweisung aus (zum Beispiel jmp
), die das Programmzählerregister direkt neu schreibt. Sie können es tun, indem Sie tun. Was Sie damit tun können, umfasst nicht nur das Springen von Code in den Kernel mit Systemaufrufen, sondern allgemeiner bedingte Verzweigung, Schleifenverarbeitung, Funktionsaufrufe und so weiter.
Der allgemeine Ablauf der Verarbeitung von Systemaufrufen ist wie folgt.
Die Schritte 1 und 3 springen in den Kernel und kehren von ihm zurück. Dies wird mit speziellen Anweisungen für x86-64, syscall
und sysretq
erreicht.
Mit anderen Worten, dieser Teil wird in Hardware implementiert, dh durch eine logische Schaltung in der CPU, die die Spezifikationen von x86-64 erfüllt.
In Schritt 2 erfolgt dies im Code, der als Systemaufruf-Handler im Kernel bezeichnet wird. Innerhalb des Systemaufruf-Handlers erfolgt ein Versand zur Implementierung jedes Systemaufrufs basierend auf der Systemaufrufnummer.
Etwas detaillierter kann die Verarbeitung von Systemaufrufen in folgende Bereiche unterteilt werden.
syscall
aus und lesen Sie den Wert des Registers mit der Adresse des Systemaufruf-Handlers in das Programmzählerregister.sysretq
stellt den ursprünglichen Code im Benutzerprozess wieder her.In der Abbildung sieht es so aus.
Wir werden jedes Element von (1) bis (5) unten erklären.
Wenn die CPU den Befehl "syscall" ausführt, wird ungefähr Folgendes ausgeführt.
** 1. ** FLAGS-Register ist ein Register, das den aktuellen CPU-Status / Ausführungsmodus der CPU darstellt. Die CPU ist beispielsweise Schutz. //ja.wikipedia.org/wiki/%E3%83%AA%E3%83%B3%E3%82%B0%E3%83%97%E3%83%AD%E3%83%86%E3% 82% AF% E3% 82% B7% E3% 83% A7% E3% 83% B3) Zeigt an, wo Sie sich befinden. Speichern Sie den aktuellen Wert im R11-Register, da wir den ursprünglichen Status wiederherstellen möchten, wenn Sie vom Systemaufruf zum Benutzerprozess zurückkehren.
** 2. ** Durch Maskieren des Werts des "FLAGS" -Registers mit dem Wert des "IA32_FMASK MSR" -Registers wird der CPU-Modus umgeschaltet und wechselt auf Berechtigungsstufe 0, dh den Modus, in dem der Kernelcode ausgeführt werden kann.
Die Berechtigungsstufe ist dargestellt durch 12 ~ 13 Bit des FLAGS-Registers, aber um diese "00" (Berechtigungsstufe 0) zu machen, ist es wie folgt. Operationen werden ausgeführt, wenn "syscall" aufgerufen wird.
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
Der Wertesatz im Register "IA32_FMASK", der als Maske fungiert (beachten Sie, dass "NOT" erforderlich ist), wird im Kernel mit dem folgenden Code ausgeführt.
linux/arch/x86/kernel/cpu/common.c
/* Flags to clear on syscall */
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
Das obige X86_EFLAGS_IOPL
ist eine Maske für die Rolle des Änderns von 12 \ ~ 13bit in 00
, aber Sie können sehen, dass es tatsächlich als Wert definiert ist, so dass nur 12 \ ~ 13bit 1 wird.
linux/arch/x86/include/uapi/asm/processor-flags.h
#define X86_EFLAGS_IOPL_BIT 12 /* I/O Privilege Level (2 bits) */
#define X86_EFLAGS_IOPL (_AC(3,UL) << X86_EFLAGS_IOPL_BIT)
** 3. ** Speichern Sie den Wert des Programmzählerregisters RIP
im RCX
Register. Wenn Sie dies nicht tun, kennen Sie den Speicherort der ursprünglichen Anweisung (= den Wert des Programmzählers) nicht, wenn Sie vom Systemaufruf-Handler zum Benutzerprozess zurückkehren.
** 4. ** Liest die in IA32_LSTAR MSR
eingestellte Adresse des Systemaufrufhandlers in das Programmzählerregister RIP
und springt zum Systemaufrufhandler. Wie die Handleradresse in "IA32_LSTAR MSR" eingelesen wird, wird in (5) vorgestellt.
Weitere detaillierte Informationen zum CPU-Verhalten beim Aufruf von "syscall" finden Sie hier.
Der Systemaufruf-Handler ist der Code im Kernel, der ausgeführt wird, nachdem der Befehl "syscall" aufgerufen wurde, in dem der Systemaufruf verarbeitet wird. Hier stellen wir den vorverarbeitenden Teil des Systemaufruf-Handlers vor, den Teil, der den an das Register übergebenen Wert als Argument in die Struktur packt und an die nachfolgende Verarbeitung übergibt.
Zunächst sieht der Einstiegs- / Einstiegspunkt für den Systemaufruf-Handler folgendermaßen aus:
linux/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/*
* Interrupts are off on entry.
In diesem entry_SYSCALL_64
werden verschiedene Prozesse ausgeführt, und einer von ihnen erstellt eine Struktur auf dem Stapel, bei der das Wertefeld wie unten gezeigt als Argument an das Register übergeben wird. Ich werde.
linux/arch/x86/entry/entry_64.S
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
pushq %rax /* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
Im obigen Code werden die Werte der Register "rcx" und "r11" (die zum Speichern beim Aufrufen des Befehls "syscall" verwendet wurden) und des Registers "rax" der Struktur "pt_regs" auf dem Stapel zugewiesen. Die Zuweisung von Werten wie "rdi" und "rsi", die als Argumente für Systemaufrufe an die Struktur verwendet werden, ist das letzte Makro "PUSH_AND_CLEAR_REGS". bfeffd155283772bbe78c6a05dec7c0128ee500c / arch / x86 / entry / calling.h # L100-L145).
Die oben erstellte Struktur und die Systemaufrufnummer werden an die Funktion "do_syscall_64" übergeben, die den folgenden Systemaufruf verarbeitet.
linux/arch/x86/entry/entry_64.S#L173-L175
movq %rax, %rdi
movq %rsp, %rsi
call do_syscall_64 /* returns with IRQs disabled */
Es ist zu beachten, dass die Methode zum Übergeben des Arguments an "do_syscall_64" unter Verwendung eines Registers erfolgt. In der Regel (https://wiki.osdev.org/System_V_ABI) beim Übergeben von Argumenten an eine Funktion mit x86-64 in der Reihenfolge vom ersten Argument "rdi", "rsi", "rdx", "rcx" Sie sollten die Register ,
r8und
r9` verwenden.
Wenn Sie also ein Argument mit der folgenden Signatur an die Funktion "do_syscall_64" übergeben möchten,
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
Sie können dies tun, indem Sie die Werte festlegen, die Sie an die beiden Register "rdi" und "rsi" übergeben möchten.
Der Implementierungsaufruf für jeden Systemaufruf erfolgt innerhalb von "do_syscall_64". Insbesondere wird durch Angabe eines Elements mit einer Systemaufrufnummer für das Array "sys_call_table", das die Funktion enthält, die jeden Systemaufruf implementiert, der Versand an die Implementierung des entsprechenden Systemaufrufs durchgeführt.
Außerdem wird die Implementierung jedes Systemaufrufs im Makro SYSCALL_DEFINE *
definiert, das Sie als Orientierungspunkt finden können.
Werfen wir einen Blick auf den entsprechenden Kernel-Code unten.
Als Argument der Funktion "do_syscall_64" wird die Struktur, die aus der Systemaufrufnummer im ersten Argument "nr" und dem Registerwert zum Zeitpunkt des Systemaufrufs besteht, im zweiten Argument "regs" übergeben.
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
struct thread_info *ti;
enter_from_user_mode();
local_irq_enable();
sys_call_table
implementiert die Verarbeitung jedes Systemaufrufs wie in [hier] definiert (https://github.com/torvalds/linux/blob/7e9890a3500d95c01511a4c45b7e7192dfa47ae2/arch/x86/entry/syscall_64.c#L18) Es ist ein Array von Funktionen, aber durch Angabe der Elemente dieses Arrays mit der Systemaufrufnummer "nr" und Übergabe der Struktur "regs" wird es an die Verarbeitung jedes Systemaufrufs gesendet.
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
}
regs-> ax = sys_call_table [nr](regs);
ist der Teil, der versendet. Auch der Rückgabewert der Funktion wird in das Feld der Struktur gesetzt, die dem "AX" -Register (= Rax-Register) entspricht, aber nachdem dieser Wert den "Syscall" -Befehl beendet hat, ist er tatsächlich "Rax" als Rückgabewert. Es wird im Register eingetragen.
Es ist eine Funktion, die tatsächlich Systemaufrufe verarbeitet, die ein Element des Arrays "sys_call_table" ist. Beispielsweise wird die Verarbeitung von "write" unten implementiert.
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count);
}
Im Allgemeinen wird die Implementierung jedes Systemaufrufs in [SYSCALL_DEFINE *
Makro] definiert (https://github.com/torvalds/linux/blob/dd5001e21a991b731d659857cd07acc7a13e6789/include/linux/syscalls.h#L215-L2). Sie können dies also als Leitfaden verwenden, um jede Implementierung zu finden.
Weitere Informationen zu den Makros sys_call_table
und SYSCALL_DEFINE *
finden Sie in diesem Artikel (https://lwn.net/Articles/604287/).
Die Rückkehr vom Systemaufruf-Handler zum ursprünglichen Code im Benutzerprozess erfolgt durch die Anweisung sysretq
. Die von der Anweisung "sysetq" durchgeführte Verarbeitung ist fast die Umkehrung der Anweisung "syscall".
--Lesen Sie für das RFLAGS-Register den Wert des R11-Registers (das den ursprünglichen Wert von RFLAGS gespeichert hat), um ihn auf den ursprünglichen Wert zurückzusetzen, sowie den Modus (Berechtigungsstufe) der CPU, die den Benutzerprozess ausführt. Zurück zu 3) --Lesen Sie für das Programmzählerregister "RIP" den Wert des "RCX" -Registers (das den ursprünglichen Wert von "RIP" gespeichert hat), um ihn auf den ursprünglichen Wert zurückzusetzen, und den ursprünglichen Befehl, der "syscall" aufgerufen hat. Kehren Sie zum Punkt zurück
Es werden verschiedene andere Prozesse ausgeführt. Weitere Informationen finden Sie unter hier.
Der Code, der "sysretq" im Kernel aufruft, aber der Code, der zuletzt im Systemaufruf-Handler eintrifft, ist unten aufgeführt.
linux/arch/x86/entry/entry_64.S
popq %rdi
popq %rsp
USERGS_SYSRET64
END(entry_SYSCALL_64)
SYSretq
wird dabei von USERGS_SYSRET64
aufgerufen.
linux/arch/x86/include/asm/irqflags.h
#define USERGS_SYSRET64 \
swapgs; \
sysretq;
Beim Aufruf des Befehls "syscall" wurde die Adresse des Systemaufrufhandlers aus dem Register "IA32_LSTAR MSR" für das Programmzählerregister "RIP" gelesen und ein Sprung zum Systemaufrufhandler ausgeführt. Woher kennt das Register "IA32_LSTAR MSR" die Adresse des Systemaufruf-Handlers? Die Adresse des Systemaufrufhandlers wird während der CPU-Initialisierung in das Register "IA32_LSTAR MSR" eingelesen. Im Folgenden stellen wir den Kernel-Code vor, der diesen Initialisierungsprozess unterstützt.
Die CPU-Initialisierung erfolgt unten.
linux/arch/x86/kernel/cpu/common.c
/*
* cpu_init() initializes state that is per-CPU. Some data is already
* initialized (naturally) in the bootstrap process, such as the GDT
* and IDT. We reload them nevertheless, this function acts as a
* 'CPU state barrier', nothing should get across.
*/
#ifdef CONFIG_X86_64
void cpu_init(void)
{
Legen Sie die Adresse mit syscall_init ();
fest Getan werden.
linux/arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
Im obigen Code
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
Hat die Adresse des Systemaufruf-Handlers in das Register "IA32_LSTAR MSR" geschrieben.
Die Konstanten "MSR_LSTAR" und "entry_SYSCALL_64", aber die Konstante "MSR_LSTAR" geben das Register "IA32_LSTAR MSR" als Schreibzielregister an.
linux/arch/x86/include/asm/msr-index.h
#define MSR_LSTAR 0xc0000082 /* long mode SYSCALL target */
entry_SYSCALL_64
ist der Wert, der in [hier] als Prototyp deklariert wurde (https://github.com/torvalds/linux/blob/6f0d349d922ba44e4348a17a78ea51b7135965b1/arch/x86/include/asm/proto.h#L12). Rufen Sie den Einstiegspunkt für den Handler auf](https://github.com/torvalds/linux/blob/cd6c84d8f0cdc911df435bb075ba22ce3c605b07/arch/x86/entry/entry_64.S#L145-L147).
Wenn Sie sich Systemaufrufe ansehen, werden Sie häufig auf das Konzept von Interrupts stoßen. Im Kernel heißt der Kommentar im folgenden Code beispielsweise "Interrupts sind ausgeschaltet", aber was genau ist "Interrupts"? Was hat das mit Systemaufrufen zu tun?
linux/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/*
* Interrupts are off on entry.
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
Erstens können Systemaufrufe in Bezug auf die Beziehung zwischen Interrupts und Systemaufrufen als eine Art Interrupt angesehen werden. Wenn ein Interrupt auftritt, unterbricht die CPU den aktuell ausgeführten Code (ob im Benutzerprozess oder im Kernel), speichert den Ausführungsstatus, damit er später neu gestartet werden kann, und dann spezifischen Code im Kernel (= inter). Zum Rapto-Handler springen). Es ist so ziemlich das gleiche wie die Systemaufrufe, die wir bisher gesehen haben. Es gibt zwei Möglichkeiten, einen Interrupt zu generieren: Die durch Software verursachte wird als Software-Interrupt und die durch Hardware verursachte als Hardware-Interrupt bezeichnet.
Beispiel für einen Software-Interrupt
Beispiel für einen Hardware-Interrupt
Dank des Interrupt-Mechanismus ist es möglich, eine Funktion zu implementieren, die "wenn etwas passiert, die CPU zu einem bestimmten Code springt, ohne Fragen zu stellen und ihn zu verarbeiten", die beispielsweise die Eingabe von Hardware ermöglicht. Sie können schnell reagieren.
Darüber hinaus verfügt x86-64 über dedizierte Anweisungen ("syscall" und "sysretq") für Systemaufrufe. In x86-32 und anderen Architekturen vor einer Generation befinden sich Systemaufrufe jedoch im Interrupt-Mechanismus. Es ist realisiert in. Um beispielsweise einen Systemaufruf auf x86-32 zu implementieren, geben Sie den Vektor "0x80" in [Anweisung "int" (https://www.felixcloutier.com/x86/intn:into:int3:int1) "int 0x80" an Dies erfolgte durch Aufrufen der Anweisung `.
Einige der Systemaufruf-Handler werden mit ausgeschalteten Interrupts ausgeführt, dh "irq" aus. "irq" ist eine Interrupt-Anforderung, und wenn die CPU sie empfängt, wird ein Interrupt generiert. Im Allgemeinen können einige oder alle "irq" deaktiviert werden, wenn der Interrupt-Handler den Interrupt verarbeitet. Dies verhindert die Situation, in der während der Verarbeitung eines Interrupts ein Interrupt auftritt. Wenn "irq" deaktiviert ist, kann es keine Interrupts akzeptieren, so dass es beispielsweise nicht auf Tastatureingaben reagieren kann. Code, der mit "irq" ausgeschaltet ist, sollte also kurz genug sein, damit die Verarbeitung nicht lange dauert.
Wenn Sie mehr über Interrupts erfahren möchten, können Sie auf diese Seite verweisen.
Einführung von Materialien und Schlüsselwörtern zur weiteren Untersuchung von Systemaufrufen und Kerneln. Es dient auch als Einführung in die Literatur, die beim Schreiben dieses Artikels als Referenz verwendet wurde.
Ich habe auf hier verwiesen, als ich die Anweisungen nachgeschlagen habe. Für eine Einführung in den x86-64-Assemblycode empfehlen wir außerdem Einführung in das Erstellen eines C-Compilers für diejenigen, die niedrigere Ebenen kennenlernen möchten.
Linux Kernel Development ist hoch bewertet und befindet sich im Kernel. Wahrscheinlich das beste Implementierungskommentarbuch. Inhaltlich ist es nicht einfach (zumindest für mich), aber ich empfehle es, weil die Erzählung manchmal interessant und die Erklärung sehr höflich ist. Auch wenn Sie nicht am Kernel interessiert sind, kann Kapitel 10 hilfreich sein, in dem die Technik der parallelen Programmierung im Kernel erläutert wird. Wir empfehlen auch diesen Hinweis, in dem der Inhalt von LKD zusammengefasst ist.
Eigentlich habe ich den Kernel-Code zum ersten Mal richtig gelesen, als ich diesen Artikel geschrieben habe.
Der Eindruck ist natürlich, dass ich nicht alles verstehen kann, was im Code geschrieben ist, aber es ist nicht so schwierig, dem allgemeinen Ablauf der Verarbeitung zu folgen. Es gibt einige Teile, in denen die Kommentare sorgfältig geschrieben wurden.
Suchen Sie zum Lesen nach Schlüsselwörtern (drücken Sie die Taste /
) unter github oder hier. Ich habe mit der Definitionsquellensprungfunktion von (last / source) gelesen.
Ich bin auch ein Amateur, wenn es um Kernel geht, also gibt es vielleicht einen besseren Weg, es zu lesen, aber ...
Ich denke, es ist in Ordnung, Prozesse, virtuellen Speicher und Multitasking als grundsätzlich wichtige Konzepte im Zusammenhang mit dem Betriebssystem zu unterdrücken. Die empfohlenen Schlüsselwörter lauten wie folgt.
--Prozess --Programm zähler
--Virtueller Speicher --Virtuelle Adresse --Physikalische Adresse
--Multitasking
Recommended Posts