[LINUX] Was ist ein Systemaufruf?

Warum sollten Sie sich um Systemaufrufe kümmern?

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.

Was wird in diesem Artikel erklärt

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.

Zweck und Rolle des Systemaufrufs

Gemäß Linux Kernel Development usw. wird ein Mechanismus namens Systemaufruf eingeführt. Es gibt zwei Vorteile:

  1. Bereitstellung einer einfachen Schnittstelle für den Betrieb von Hardware
  2. Anwendungen können Betriebssystemressourcen sicher und sicher verwenden

1. Bereitstellung einer einfachen Schnittstelle für den Betrieb von Hardware

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.

2. Anwendungen können Betriebssystemressourcen sicher und sicher verwenden

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.

Wie die Anwendung den Systemaufruf aufruft

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:

  1. Stellen Sie die Nummer ein, die den Systemaufruf angibt, der im Register ausgeführt werden soll (minimaler Speicher in der CPU).
  2. Legen Sie das Argument fest, das an den Systemaufruf im Register übergeben werden soll
  3. Führen Sie die Anweisung / Anweisung aus, die den Systemaufruf aufruft

Im Folgenden wird das obige Verfahren anhand des Codes, der Zeichen auf dem Bildschirm ausgibt (Standardausgabe), ausführlich erläutert.

Beispiel am Beispiel der Zeichenausgabe

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

Schritt 2

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

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

Erläuterung des Beispielcodes

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.

[Ergänzung] So stellen Sie Systemaufrufe bereit

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.

[Ergänzung] Wie die CPU die Maschinensprache ausführt

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.

  1. Lesen Sie eine Anweisung / Anweisung aus dem Speicher
  2. Führen Sie Anweisungen aus und aktualisieren Sie die Registerwerte usw.
  3. Aktualisieren Sie den Wert des Registers, in dem die Adresse des aktuell ausgeführten Befehls gespeichert ist, um das Lesen des nächsten Befehls vorzubereiten.

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.

Implementierungsmethode für Systemaufrufe und interne Implementierung

Der allgemeine Ablauf der Verarbeitung von Systemaufrufen ist wie folgt.

  1. Führen Sie eine Anweisung aus, um einen Systemaufruf aufzurufen und zum Code im Kernel zu springen
  2. Rufen Sie die Implementierung des Systemaufrufs auf, der durch die Systemaufrufnummer im Kernel angegeben wird
  3. Führen Sie eine Anweisung aus, um zum Aufrufer des Systemaufrufs im Kernel zurückzukehren und zum ursprünglichen Code zurückzukehren.

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.

  1. Führen Sie den Befehl syscall aus und lesen Sie den Wert des Registers mit der Adresse des Systemaufruf-Handlers in das Programmzählerregister.
  2. Der Systemaufruf-Handler akzeptiert die Verarbeitung
  3. Versand zur Implementierung jedes Systemaufrufs basierend auf der Systemaufrufnummer im Systemaufruf-Handler
  4. Der Befehl sysretq stellt den ursprünglichen Code im Benutzerprozess wieder her.
  5. Registrieren Sie die Adresse des Systemaufruf-Handlers beim Start im Register

In der Abbildung sieht es so aus.

syscall (4).png

Wir werden jedes Element von (1) bis (5) unten erklären.

(1) Ausführung des Syscall-Befehls

Wenn die CPU den Befehl "syscall" ausführt, wird ungefähr Folgendes ausgeführt.

  1. Speichern Sie den Wert des FLAGS-Registers, das den aktuellen Ausführungsmodus der CPU darstellt, im R11-Register.
  2. Maskieren Sie den Wert des "FLAGS" -Registers mit dem Wert des "IA32_FMASK MSR" -Registers und wechseln Sie in einen Modus, in dem die CPU den Kernelcode ausführen kann.
  3. Speichern Sie den Wert des Programmzählerregisters "RIP" im "RCX" -Register
  4. Lesen Sie die Adresse des Systemaufrufhandlers aus dem Register "IA32_LSTAR MSR" in das Programmzählerregister "RIP" und springen Sie zum Systemaufrufhandler.

** 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.

x86/syscall

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.

(2) Systemaufruf-Handler

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 , r8undr9` verwenden. Wenn Sie also ein Argument mit der folgenden Signatur an die Funktion "do_syscall_64" übergeben möchten,

linux/arch/x86/entry/common.c

__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.

(3) Versand an jeden Systemaufruf

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.

linux/arch/x86/entry/common.c

__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.

linux/arch/x86/entry/common.c

	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.

linux/fs/read_write.c

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/).

(4) Kehren Sie mit der Anweisung sysretq zurück

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;

(5) Lesen Sie beim Start die Adresse des Systemaufruf-Handlers in das Register

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).

[Ergänzung] Beziehung zwischen Systemaufruf und Interrupt

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 `.

Was bedeutet es, Interrupt / IRQ auszuschalten?

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.

Den nächsten Schritt machen

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.

Welche Arten von Systemaufrufen gibt es?

Informationen zu x86-64 und Assemblycode

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.

Über den Linux-Kernel

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.

Lesen des Kernel-Quellcodes

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

Über den Mechanismus und die Funktion des Betriebssystems

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

andere Referenzen

Recommended Posts

Was ist ein Systemaufruf?
Was ist __call__?
Was ist eine Distribution?
Was ist ein Terminal?
Was ist ein Hacker?
Was ist ein Zeiger?
Was ist ein Entscheidungsbaum?
Was ist ein Kontextwechsel?
Was ist ein Superuser?
[Definition] Was ist ein Framework?
Was ist eine Rückruffunktion?
[Python] Was ist eine Zip-Funktion?
[Python] Was ist eine with-Anweisung?
Was ist ein lexikalischer / dynamischer Bereich?
Was ist das Convolutional Neural Network?
Was ist das X Window System?
Was ist ein Namespace?
Was ist copy.copy ()
Was ist Django? .. ..
Was ist dotenv?
Was ist POSIX?
Was ist klass?
Was ist SALOME?
Was ist ein Hund? Django-Installationsvolumen
Was ist ein Hund? Python-Installationsvolumen
Was ist Linux?
Was ist Python?
Was ist Hyperopt?
Was ist Linux?
Was ist Pyvenv?
Was ist Linux?
Was ist Python?
Was ist ein Hund? Django - Erstellen Sie ein benutzerdefiniertes Benutzermodell
Was ist ein Hund? Fordern Sie die Django-Vorlage heraus! Volumen
Es ist ein Mac. Was ist der Linux-Befehl Linux?
Was ist ein Hund? Django - Erstellen Sie ein benutzerdefiniertes Benutzermodell 2
Sag mir, was eine gleichwinklige Abbildung ist, Python!
Was ist Piotroskis F-Score?
Was ist Raspberry Pi?
[Python] Was ist Pipeline ...
Was ist das Calmar-Verhältnis?
[PyTorch Tutorial ①] Was ist PyTorch?
Was ist Hyperparameter-Tuning?
Was ist JSON? .. [Hinweis]
Ein Hinweis zu __call__
Wofür ist Linux?
Was ist Ensemble-Lernen?
Was ist TCP / IP?
Was ist Pythons __init__.py?
Was ist ein Iterator?
Was ist UNIT-V Linux?
[Python] Was ist virtualenv?
Was ist maschinelles Lernen?
Was ist ein Hund? POST-Übertragungsvolumen mit Django--forms.py
Was ist ein Hund? Startvolumen der Django-App erstellen --startapp
Was ist ein Hund? Django App Creation Start Volume - Startprojekt
Was ist ein empfohlener Motor? Zusammenfassung der Typen
Was ist Gott? Erstelle einen einfachen Chatbot mit Python
Für mich als Django-Anfänger (2) - Was ist MTV?