Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

System Calls

Mithilfe von System Calls können Anwendungen Kernel-Funktionen nutzen.
© dotnetpro
W ie ein neuer virtueller Adressraum für Anwendungsprogramme erzeugt werden kann und wie Anwendungsprogramme von der FAT12-Partition geladen und ausgeführt werden, haben Sie in der zurückliegenden Folge [1] gesehen. Jetzt geht es darum, wie Anwendungsprogramme implementiert werden und wie diese mithilfe sogenannter System Calls mit dem Kernel zusammenarbeiten.Wie bereits in einer der vorangegangen Folgen erwähnt, bietet eine x64-CPU unterschiedliche Ringe an, in denen CPU-Instruktionen ausgeführt werden können. Klassischerweise bleibt hierbei der Ring 0 alleine für den Kernel reserviert und der Ring 3 für alle Anwendungsprogramme. Die Ringe 1 und 2 werden von modernen Betriebssystemen nicht benutzt. Bild 1 veranschaulicht diese Unterteilung.
Die vier Ringe: In Ring 0 läuft der Kernel, in Ring 3 die Anwendungen (Bild 1) © Autor
Anwendungsprogramme, die im Ring 3 ausgeführt werden, können allerdings wichtige CPU-Befehle (etwa in, out) nicht ausführen, da deren Ausführung nur im Ring 0 erlaubt ist. Auch der direkte Zugriff auf den Hauptspeicher ist tabu für Code, der im Ring 3 ausgeführt wird, da dieses Recht für Ring-0-Code reserviert ist. Daraus folgt, dass ein Anwendungsprogramm eine Kernel-Funktion wie etwa printf() nicht direkt aufrufen kann. Die CPU verbietet dies und löst bei einem Versuch eine Exception aus. Listing 1 zeigt eine einfache Anwendung dazu. Darin wird die Funktion printf() in einer Endlosschleife aufgerufen, welche die übergebene Zeichenkette auf dem Bildschirm ausgeben soll. Es handelt sich dabei jedoch nicht um die Kernel-Funktion printf(), die bereits in einer der ersten Folgen dieser Serie implementiert wurde. Das hat Folgen: Einerseits hat das Anwendungsprogramm überhaupt keinen Zugriff auf die Kernel-Funktion, da sich die Implementierung in einer anderen Binärdatei befindet. Und auf der anderen Seite hat die Anwendung im User Mode keinen Zugriff auf das Video-Memory und kann zudem die Assembly-Befehle in/out, durch die der Cursor auf dem Bildschirm bewegt wird, nicht ausführen.
Listing 1: Eine einfache Anwendung
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">ProgramMain</span><span class="hljs-params">()</span></span><br/>{<br/>   <span class="hljs-keyword">while</span> (<span class="hljs-number">1</span> == <span class="hljs-number">1</span>)<br/>   {<br/>      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Hello World from User Mode </span><br/><span class="hljs-string">         Program #1...\n"</span>);<br/>    }<br/>} 
Es stellt sich also die Frage, wie es ein Anwendungsprogramm schafft, überhaupt eine sinnvolle Aufgabe zu erledigen, wenn es nicht auf die Kernel-Funktionalitäten zugreifen kann. Die Antwort darauf sind die sogenannten System Calls.Ein Betriebssystem-Kernel stellt Anwendungen die benötigten Funktionalitäten in Form einer standardisierten Schnittstelle zur Verfügung.Die printf()-Funktion aus Listing 1 ist nichts anderes als ein Wrapper über eine System-Call-Funktion, die einen übergebenen String auf dem Bildschirm ausgibt. Der System Call wird vom Kernel entgegengenommen, und dieser führt dann intern die Kernel-Funktion printf() aus. Beim Aufruf eines System Calls werden die folgenden Schritte durchgeführt:
  • Wechsel vom User Mode in Ring 3 in den Kernel Mode (Ring 0).
  • Aufruf der entsprechenden Funktion im Kernel.
  • Wechsel vom Kernel Mode (Ring 0) zurück in den User Mode (Ring 3).
Damit ein Anwendungsprogramm System Calls nutzen kann, muss im Kernel ein System-Call-Handler registriert werden, also eine Funktion, die im Rahmen eines System Calls aufgerufen wird.Ich habe mich bei meinem Betriebssystem dazu entschieden, dass System Calls über den Software-Interrupt 0x80 (dezimal 128) aufgerufen werden können. Daher wird innerhalb der Funktion InitIdt() der 128. Eintrag der Interrupt Descriptor Table mit einem Verweis auf den System-Call-Handler SysCallHandlerAsm initialisiert.Damit der Interrupt 0x80 von einem Anwendungsprogramm, das im Ring 3 läuft, ausgelöst werden kann, muss der DPL (Descriptor Privilege Level) auf Ring 3 gesetzt werden. Beim DPL handelt es sich um den CPU-Ring, der als Minimum benötigt wird, um den Interrupt auslösen zu können. Weitere Informationen hierzu finden Sie unter [2]. Listing 2 zeigt diese Erweiterung innerhalb der Funktion InitIdt().
Listing 3: System Calls verarbeiten
typedef struct <span class="hljs-type">SysCallRegisters</span><br/>{<br/>   // <span class="hljs-type">Parameter</span> values<br/>   unsigned long <span class="hljs-type">RDI</span>;<br/>   unsigned long <span class="hljs-type">RSI</span>;<br/>   unsigned long <span class="hljs-type">RDX</span>;<br/>   unsigned long <span class="hljs-type">RCX</span>;<br/>   unsigned long <span class="hljs-type">R8</span>;<br/>   unsigned long <span class="hljs-type">R9</span>;<br/>} <span class="hljs-type">SysCallRegisters</span>;<br/>long <span class="hljs-type">SysCallHandlerC</span>(<br/>      <span class="hljs-type">SysCallRegisters</span> *<span class="hljs-type">Registers</span>)<br/>{<br/>   // <span class="hljs-type">The</span> <span class="hljs-type">SysCall</span> <span class="hljs-type">Number</span> is stored in the<br/>   // register <span class="hljs-type">RDI</span><br/>   int sysCallNumber = <span class="hljs-type">Registers</span>-><span class="hljs-type">RDI</span>;<br/>   // printf<br/>   if (sysCallNumber == <span class="hljs-type">SYSCALL_PRINTF</span>)<br/>   {<br/>      printf((char *)<span class="hljs-type">Registers</span>-><span class="hljs-type">RSI</span>);<br/>      return 0;<br/>   }<br/>   // <span class="hljs-type">GetPID</span><br/>   else if (sysCallNumber == <span class="hljs-type">SYSCALL_GETPID</span>)<br/>   {<br/>      <span class="hljs-type">Task</span> *state = (<span class="hljs-type">Task</span> *)<span class="hljs-type">GetTaskState</span>();<br/>      return state-><span class="hljs-type">PID</span>;<br/>   }<br/>   // <span class="hljs-type">TerminateProcess</span><br/>   else if (sysCallNumber == <br/>         <span class="hljs-type">SYSCALL_TERMINATE_PROCESS</span>)<br/>   {<br/>      <span class="hljs-type">Task</span> *state = (<span class="hljs-type">Task</span> *)<span class="hljs-type">GetTaskState</span>();<br/>      <span class="hljs-type">TerminateTask</span>(state-><span class="hljs-type">PID</span>);<br/>      return 0;<br/>   }<br/>   return 0;<br/>} 
Bei der Funktion SysCallHandlerAsm handelt es sich um eine Assembler-Funktion, die alle aktuellen Registerinhalte auf dem Stack zwischenspeichert, danach die C-Funktion SysCallHandlerC() aufruft und anschließend die Registerinhalte vom Stack wiederherstellt. In Listing 3 sehen Sie die Implementierung dieser C-Funktion, welche die verschiedenen System Calls abarbeitet.
Listing 2: IDT-Erweiterung für System Calls
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">InitIdt</span><span class="hljs-params">()</span></span><br/>{<br/>   <span class="hljs-comment">// ...</span><br/><span class="hljs-comment">   // The INT 0x80 can be raised from Ring 3</span><br/><span class="hljs-comment">   IdtSetGate(128, (unsigned long)SysCallHandlerAsm, </span><br/><span class="hljs-comment">      0xE);</span><br/><span class="hljs-comment">   idtEntries[128].DPL = 3;</span><br/><span class="hljs-comment">   // Loads the IDT table into the processor </span><br/><span class="hljs-comment">   // register (Assembler function)</span><br/><span class="hljs-comment">   IdtFlush((unsigned long)&idtPointer);</span><br/><span class="hljs-comment">}</span> 
Wie Sie im Listing erkennen können, wird der Funktion SysCallHandlerC eine Referenz auf die Struktur SysCallRegisters übergeben, welche die sechs möglichen Parameter eines System Calls beinhaltet. Mehr als sechs Parameter werden aktuell nicht unterstützt. Hierbei wird die x64-Calling-Convention von Linux verwendet, welche die ersten sechs Parameter in den Registern RDI, RSI, RDX, RCX, R8 und R9 übergibt. Weitere Informationen zu dieser Calling Conven­tion finden Sie unter [3].Die Nummer des System Calls wird immer als erster Parameter im Register RDI übergeben, auf dessen Basis anschließend die entsprechende Funktion im Kernel aufgerufen wird. Der Kernel-Funktion werden zusätzlich noch die erforderlichen Parameter aus den passenden Registern übergeben. Im Fall der Funktion printf() ist der darzustellende String als Zeiger im Register RSI enthalten.

System Calls: User-Mode-Implementierung

Nachdem Sie gesehen haben, wie der Kernel in der Lage ist System Calls abzuarbeiten, wird es nun darum gehen, wie ­eine Anwendung einen System Call durchführen kann.Entwickeln Sie Anwendungsprogramme für Betriebssysteme wie Windows oder Linux, benötigen Sie immer eine Referenz auf die sogenannte C Runtime Library. Diese Laufzeitbibliothek bietet eine Vielzahl unterschiedlicher, standardisierter Funktionen an, mit denen Sie schlussendlich Ihre Anwendungen implementieren können.Im Hintergrund verwendet die C Runtime Library immer System Calls, um ihre Funktionalitäten bereitstellen zu können. Klassische Beispiele dafür sind open(), read(), write(), malloc(), printf() und so weiter.Anschließend werden nun die ersten Funktionen implementiert, die System Calls im Kernel durchführen. Die Runtime Library wird statisch mit den Anwendungsprogrammen verlinkt, das heißt, dass jede Binärdatei eines Anwendungsprogramms den kompletten Code der Runtime Library enthält.Dynamische Runtime Libraries (wie DLL-Dateien unter Windows und Shared Object Files unter Linux) sind für den Anfang zu komplex, da hier zusätzlich ein sogenannter Dynamic Loader innerhalb des Betriebssystems benötigt werden würde.Im ersten Schritt möchte ich Ihnen deshalb zeigen, wie der System Call printf() funktioniert. In Listing 1 haben Sie bereits ein Anwendungsprogramm gesehen, das die Funktion printf() aufruft – diese ist aber Bestandteil der Laufzeitbibliothek (Runtime Library) und nicht die printf()-Funktion des Kernels, die Sie bereits kennen. Listing 4 zeigt die Definition dieser Runtime-Library-Funktion.
Listing 4: printf() innerhalb der Runtime Library
#define SYSCALL_PRINTF <span class="hljs-number">1</span><br/><span class="hljs-keyword">void</span> printf(<span class="hljs-built_in">char</span> *<span class="hljs-built_in">string</span>)<br/>{<br/>   SYSCALL1(SYSCALL_PRINTF, <span class="hljs-built_in">string</span>);<br/>}<br/><span class="hljs-built_in">long</span> SYSCALL1(<br/>      <span class="hljs-keyword">int</span> SysCallNumber,<br/>      <span class="hljs-keyword">void</span> *Parameter1)<br/>{<br/>   <span class="hljs-keyword">return</span> SYSCALLASM1(<br/>      SysCallNumber,<br/>      Parameter1);<br/>}<br/>; Raises a SysCall<br/>SYSCALLASM1:<br/>   INT <span class="hljs-number">0</span>x80<br/>   RET 
Wie Sie im Listing sehen, ruft printf() einfach die Funktion SYSCALL1() auf, deren Aufgabe es ist, einen System Call im Kernel auszuführen, der einen Parameter erwartet – im Fall von printf() ist das der Zeiger auf den String, der auf dem Bildschirm ausgegeben werden soll. Der Funktion SYSCALL1() wird im ersten Parameter die Nummer des System Calls übergeben – in diesem Fall ist das die Konstante SYSCALL_PRINTF, die den Wert 1 enthält.1Innerhalb von SYSCALL1() wird nun die Assembly-Funk­tion SYSCALLASM1 aufgerufen, die schlussendlich den Interrupt 0x80 auslöst. Die entsprechenden Parameter werden durch den C-Compiler anhand der x64-Calling-Convention in die Register RDI, RSI, RDX,RCX,R8 und R9 kopiert. Damit kann der System-Call-Handler innerhalb des Kernels die Parameter an die entsprechende Funktion im Kernel weiterreichen. Neben dem System Call printf() habe ich auch noch die beiden System Calls GetPID() und TerminateProcess() implementiert. Über GetPID() kann man die Process-ID des aktuell laufenden Prozesses ermitteln. Möchten Sie einen laufenden Prozess beenden, erledigt das die Funktion TerminateProcess() für Sie. Listing 5 zeigt, wie Sie die beiden Funktionen in einer Anwendung einsetzen können.
Listing 5: Weitere System Calls in Aktion
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">ProgramMain</span><span class="hljs-params">()</span></span><br/>{<br/>   <span class="hljs-keyword">long</span> pid = GetPID();<br/>   <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < <span class="hljs-number">100</span>; i++)<br/>   {<br/>      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Hello World from User Mode </span><br/><span class="hljs-string">         Program #2 with PID "</span>);<br/>      printf_long(pid, <span class="hljs-number">10</span>);<br/>      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"\n"</span>);<br/>   }<br/>   TerminateProcess();<br/>} 

Fazit

Im Rahmen dieser und der vorangegangenen Folge [1] der Serie wurde gezeigt, wie Sie Anwendungsprogramme von der Festplatte laden und in einem eigenen virtuellen Adressraum ausführen. Da Anwendungsprogramme immer im Ring 3 der CPU ausgeführt werden, wurden System-Call-Funktionalitäten im Kernel implementiert, der immer im Ring 0 läuft.Aufbauend auf die System Calls wurden auch die ersten Schritte in Richtung einer eigenen Laufzeitbibliothek unternommen, welche die Entwicklung von Anwendungsprogrammen um einiges einfacher macht. Im kommenden Teil der Serie soll eine einfache Command Shell implementiert werden, mit deren Hilfe Anwendungen von der FAT12-Partition ausgeführt werden können.
Projektdateien herunterladen

Fussnoten

  1. Klaus Aschenbrenner, Anwendungsprogramme, dotnetpro 11/2023, Seite 82 ff., http://www.dotnetpro.de/A2311DIYOS
  2. Difference between DPL and RPL in x86, http://www.dotnetpro.de/SL2312DIYOS1
  3. x64 Calling Convention, http://www.dotnetpro.de/SL2312DIYOS2

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige