Ich lerne CPU's und weiß, wie es ein Programm aus dem Speicher liest und seine Anweisungen ausführt. Ich verstehe auch, dass ein Betriebssystem Programme in Prozessen trennt und sie dann so schnell abwechselt, dass Sie glauben, dass sie gleichzeitig ausgeführt werden, aber tatsächlich läuft jedes Programm alleine in der CPU. Aber wenn das Betriebssystem auch eine Menge Code in der CPU ausführt, wie kann es dann die Prozesse verwalten?
Ich habe nachgedacht und die einzige Erklärung, die ich denken könnte, ist: Wenn das Betriebssystem ein Programm aus dem externen Speicher in den Arbeitsspeicher lädt, fügt es seine eigenen Anweisungen in die Mitte der ursprünglichen Programmanweisungen ein, damit das Programm ausgeführt wird, das Programm kann das Betriebssystem aufrufen und einige Dinge tun. Ich glaube, es gibt eine Anweisung, die das Betriebssystem zum Programm hinzufügt, damit die CPU einige Zeit zum Betriebssystemcode zurückkehren kann. Außerdem glaube ich, dass das Betriebssystem beim Laden eines Programms prüft, ob es einige vorab gesendete Anweisungen gibt (die zu verbotenen Adressen im Speicher springen würden) und diese dann beseitigt.
Denke ich ernst? Ich bin kein CS-Student, sondern ein Mathematik-Student. Wenn möglich, würde ich mir ein gutes Buch darüber wünschen, da ich niemanden gefunden habe, der erklärt, wie das Betriebssystem einen Prozess verwalten kann, wenn das Betriebssystem auch eine Menge Code in der CPU enthält und nicht gleichzeitig ausgeführt werden kann Uhrzeit des Programms. Die Bücher sagen nur, dass das Betriebssystem Dinge verwalten kann, aber jetzt wie.
quelle
Antworten:
Nein. Das Betriebssystem spielt nicht mit dem Code des Programms, der neuen Code einfügt. Das hätte eine Reihe von Nachteilen.
Dies wäre zeitaufwändig, da das Betriebssystem die gesamte ausführbare Datei durchsuchen und ihre Änderungen vornehmen müsste. Normalerweise wird ein Teil der ausführbaren Datei nur bei Bedarf geladen. Das Einfügen ist außerdem teuer, da Sie eine Menge Material aus dem Weg räumen müssen.
Aufgrund der Unentscheidbarkeit des Halteproblems ist es unmöglich zu wissen, wo Sie die Anweisungen zum Zurückspringen zum Betriebssystem einfügen müssen. Wenn der Code beispielsweise so etwas enthält
while (true) {i++;}
, müssen Sie auf jeden Fall einen Haken in diese Schleife einfügen, aber die Bedingung in der Schleife (true
hier) kann willkürlich kompliziert sein, sodass Sie nicht entscheiden können, wie lange sie wiederholt wird. Andererseits wäre es sehr ineffizient, Hooks in jede Schleife einzufügen : Zum Beispiel würde ein Zurückspringen zum Betriebssystemfor (i=0; i<3; i++) {j=j+i;}
den Prozess erheblich verlangsamen. Und aus dem gleichen Grund können Sie keine kurzen Schleifen erkennen, um sie in Ruhe zu lassen.Aufgrund der Unentscheidbarkeit des Halteproblems ist es unmöglich zu wissen, ob die Code-Injektionen die Bedeutung des Programms geändert haben. Angenommen, Sie verwenden Funktionszeiger in Ihrem C-Programm. Das Einfügen von neuem Code würde die Positionen der Funktionen verschieben, sodass Sie, wenn Sie einen durch den Zeiger aufrufen, an die falsche Stelle springen würden. Wenn der Programmierer krank genug wäre, um berechnete Sprünge auszuführen, würden auch diese fehlschlagen.
Es würde mit jedem Antiviren-System eine Menge Spaß machen, da es auch den Virencode ändern und all Ihre Prüfsummen durcheinander bringen würde.
Sie können das Problem des Anhaltens umgehen, indem Sie den Code simulieren und Hooks in jede Schleife einfügen, die mehr als eine festgelegte Anzahl von Malen ausgeführt wird. Dies würde jedoch eine extrem teure Simulation des gesamten Programms erfordern, bevor es ausgeführt werden konnte.
Wenn Sie Code einfügen möchten, ist der Compiler der richtige Ort dafür. Auf diese Weise müssten Sie es nur einmal tun, aber aus dem zweiten und dritten oben genannten Grund würde es immer noch nicht funktionieren. (Und jemand könnte einen Compiler schreiben, der nicht mitspielt.)
Es gibt drei Hauptmethoden, mit denen das Betriebssystem die Kontrolle über Prozesse zurückerhält.
In kooperativen (oder nicht präemptiven) Systemen gibt es eine
yield
Funktion, die ein Prozess aufrufen kann, um dem Betriebssystem die Kontrolle zurückzugeben. Wenn dies Ihr einziger Mechanismus ist, sind Sie natürlich darauf angewiesen, dass sich die Prozesse gut verhalten, und ein Prozess, der nicht nachgibt, belastet die CPU, bis er beendet wird.Um dieses Problem zu vermeiden, wird ein Timer-Interrupt verwendet. CPUs ermöglichen es dem Betriebssystem, Rückrufe für alle Arten von Interrupts zu registrieren, die die CPU implementiert. Das Betriebssystem verwendet diesen Mechanismus, um einen Rückruf für einen Timer-Interrupt zu registrieren, der regelmäßig ausgelöst wird und der es ihm ermöglicht, seinen eigenen Code auszuführen.
Jedes Mal, wenn ein Prozess versucht, aus einer Datei zu lesen oder auf andere Weise mit der Hardware zu interagieren, fordert er das Betriebssystem auf, dafür zu arbeiten. Wenn das Betriebssystem von einem Prozess aufgefordert wird, etwas zu tun, kann es entscheiden, diesen Prozess anzuhalten und einen anderen Prozess auszuführen. Das hört sich vielleicht etwas machiavellistisch an, ist aber das Richtige: Die Datenträger-E / A ist langsam, sodass Sie auch Prozess B ausführen lassen können, während Prozess A darauf wartet, dass sich die sich drehenden Metallklumpen an die richtige Stelle bewegen. Netzwerk-E / A ist noch langsamer. Die Tastatur-E / A ist eiskalt, weil Menschen keine Gigahertz-Wesen sind.
quelle
1
, stoppt die CPU, was auch immer sie tut, und beginnt mit der Interrupt-Verarbeitung (was im Grunde bedeutet: "Zustand erhalten und zu einer Adresse im Speicher springen"). Die Interrupt-Behandlung selbst ist keinx86
oder welcher Code auch immer, sie ist buchstäblich fest verdrahtet. Nach dem Springen wird erneut (beliebiger)x86
Code ausgeführt. Threads sind eine viel höhere Abstraktion.Die Antwort von David Richerby ist zwar gut, aber es ist eine Art Glasur darüber, wie moderne Betriebssysteme bestehende Programme stoppen. Meine Antwort sollte für die x86- oder x86_64-Architektur zutreffend sein, die die einzige ist, die üblicherweise für Desktops und Laptops verwendet wird. Andere Architekturen sollten ähnliche Methoden haben, um dies zu erreichen.
Beim Start des Betriebssystems wird eine Interrupt-Tabelle erstellt. Jeder Eintrag in der Tabelle verweist auf ein Stück Code im Betriebssystem. Wenn Interrupts auftreten, die von der CPU gesteuert werden, wird diese Tabelle überprüft und der Code aufgerufen. Es gibt verschiedene Interrupts, z. B. Division durch Null, ungültiger Code und einige vom Betriebssystem definierte Interrupts.
Auf diese Weise kommuniziert der Benutzerprozess mit dem Kernel, z. B. wenn er auf die Festplatte oder auf etwas anderes, das der Betriebssystemkernel steuert, lesen / schreiben möchte. Ein Betriebssystem richtet auch einen Timer ein, der nach Beendigung einen Interrupt aufruft, sodass der Ausführungscode zwangsweise vom Benutzerprogramm in den Betriebssystemkern geändert wird und der Kernel andere Aktionen ausführen kann, z. B. andere Programme in die Warteschlange stellen.
Wenn dies geschieht, muss der Betriebssystem-Kernel aus dem Speicher speichern, wo sich der Code befand, und wenn der Kernel das getan hat, was er tun muss, wird der vorherige Status des Programms wiederhergestellt. Somit weiß das Programm nicht einmal, dass es unterbrochen wurde.
Der Prozess kann die Interrupt-Tabelle aus zwei Gründen nicht ändern. Der erste Grund ist, dass sie in einer geschützten Umgebung ausgeführt wird. Wenn also versucht wird, bestimmten geschützten Assembly-Code aufzurufen, löst die CPU einen weiteren Interrupt aus. Der zweite Grund ist der virtuelle Speicher. Der Speicherort der Interrupt-Tabelle liegt im realen Speicher zwischen 0x0 und 0x3FF. Bei Benutzerprozessen wird dieser Speicherort jedoch normalerweise nicht zugeordnet, und der Versuch, nicht zugeordneten Speicher zu lesen, löst einen weiteren Interrupt aus, also ohne die geschützte Funktion und die Fähigkeit, in den realen RAM zu schreiben kann der Benutzerprozess nicht ändern.
quelle
Der Betriebssystemkern erhält aufgrund des CPU-Taktinterrupt-Handlers die Kontrolle über den laufenden Prozess zurück, nicht durch das Einfügen von Code in den Prozess.
Sie sollten sich über Interrupts informieren , um mehr über deren Funktionsweise und den Umgang mit OS-Kernels zu erfahren und verschiedene Funktionen zu implementieren.
quelle
Es gibt eine ähnliche Methode wie die, die Sie beschreiben: kooperatives Multitasking . Das Betriebssystem fügt keine Anweisungen ein, aber jedes Programm muss so geschrieben sein, dass es Betriebssystemfunktionen aufruft, die möglicherweise einen anderen der kooperativen Prozesse ausführen. Dies hat die von Ihnen beschriebenen Nachteile: Ein Programmabsturz bringt das gesamte System zum Erliegen. Windows bis einschließlich 3.0 funktionierte so; 3.0 im "protected mode" und höher nicht.
Präventives Multitasking (die heutzutage übliche Art) beruht auf einer externen Quelle von Interrupts. Interrupts setzen den normalen Kontrollfluss außer Kraft und speichern die Register normalerweise irgendwo ab, damit die CPU etwas anderes tun und das Programm dann transparent fortsetzen kann. Natürlich kann das Betriebssystem das Register "Wenn Sie Interrupts hier fortsetzen" ändern, sodass es in einem anderen Prozess fortgesetzt wird.
(Einige Systeme tun Rewrite Anweisungen in begrenztem Umfang auf Programmladen, genannt „Thunk“ und der Transmeta - Prozessor dynamisch seinen eigenen Befehlssatz neu kompiliert)
quelle
Multitasking erfordert nichts wie Code-Injection. In einem Betriebssystem wie Windows gibt es eine Komponente des Betriebssystemcodes namens Scheduler, die auf einem Hardware-Interrupt beruht, der von einem Hardware-Timer ausgelöst wird. Dies wird vom Betriebssystem verwendet, um zwischen verschiedenen Programmen und sich selbst zu wechseln, was unserer menschlichen Wahrnehmung zufolge alles gleichzeitig geschieht.
Grundsätzlich programmiert das Betriebssystem den Hardware-Timer so, dass er von Zeit zu Zeit ausgelöst wird ... vielleicht 100 Mal pro Sekunde. Wenn der Timer abläuft, wird ein Hardware-Interrupt generiert - ein Signal, das die CPU auffordert, die Ausführung zu stoppen, den Status auf dem Stack zu speichern, den Modus in einen privilegierteren Modus zu ändern und den Code auszuführen, den sie in einem speziell dafür vorgesehenen Ordner findet in Erinnerung behalten. Dieser Code ist zufällig Teil des Schedulers, der entscheidet, was als nächstes getan werden soll. Es kann sein, dass ein anderer Prozess fortgesetzt wird. In diesem Fall muss ein sogenannter "Kontextwechsel" durchgeführt werden, bei dem der gesamte aktuelle Status (einschließlich der virtuellen Speichertabellen) durch den des anderen Prozesses ersetzt wird. Bei der Rückkehr zu einem Prozess muss der gesamte Kontext dieses Prozesses wiederhergestellt werden.
Die "speziell bezeichnete" Stelle im Speicher muss nur dem Betriebssystem bekannt sein. Die Implementierungen variieren, aber das Wesentliche ist, dass die CPU auf verschiedene Interrupts reagiert, indem sie eine Tabellensuche durchführt. Der Speicherort der Tabelle befindet sich an einer bestimmten Stelle im Speicher (bestimmt durch das Hardware-Design der CPU), der Inhalt der Tabelle wird vom Betriebssystem festgelegt (im Allgemeinen beim Start), und der "Typ" des Interrupts bestimmt, welcher Eintrag vorliegt in der Tabelle ist als "Interrupt-Service-Routine" zu verwenden.
Dabei handelt es sich nicht um "Code-Injection". Es basiert auf dem im Betriebssystem enthaltenen Code in Zusammenarbeit mit den Hardwarefunktionen der CPU und ihrer unterstützenden Schaltkreise.
quelle
Ich denke, das realistischste Beispiel für das, was Sie beschreiben, ist eine der von VMware verwendeten Techniken , die vollständige Virtualisierung mithilfe binärer Übersetzung .
VMware fungiert als Schicht unter einem oder mehreren gleichzeitig ausgeführten Betriebssystemen auf derselben Hardware.
Die meisten ausgeführten Anweisungen (z. B. in gewöhnlichen Anwendungen) können mit der Hardware virtualisiert werden, aber ein Betriebssystemkern selbst verwendet Anweisungen, die nicht virtualisiert werden können, da sie "ausbrechen" würden, wenn der Maschinencode des erratenen Betriebssystems unverändert ausgeführt würde "der Steuerung des VMware-Hosts. Beispielsweise muss ein Gastbetriebssystem im privilegiertesten Schutzring ausgeführt und die Interrupt-Tabelle eingerichtet werden. Wäre dies zulässig, hätte VMware die Kontrolle über die Hardware verloren.
VMware schreibt diese Anweisungen vor der Ausführung neu in den Betriebssystemcode und ersetzt sie durch Sprünge in VMware-Code, der den gewünschten Effekt simuliert.
Diese Technik ist also etwas analog zu dem, was Sie beschreiben.
quelle
Es gibt eine Vielzahl von Fällen, in denen ein Betriebssystem Code in ein Programm einschleusen kann. Die 68000-basierten Versionen des Apple Macintosh-Systems erstellen eine Tabelle aller Segmenteintrittspunkte (die sich unmittelbar vor den statischen globalen Variablen (IIRC) befinden). Wenn ein Programm startet, besteht jeder Eintrag in der Tabelle aus einem Trap-Befehl, gefolgt von der Segmentnummer und dem Versatz in das Segment. Wenn der Trap ausgeführt wird, prüft das System anhand der Wörter nach dem Trap-Befehl, welches Segment und welcher Offset erforderlich sind, lädt das Segment (falls noch nicht geschehen), fügt die Startadresse des Segments zum Offset hinzu und Ersetzen Sie dann die Falle durch einen Sprung zu dieser neu berechneten Adresse.
Auf älteren PC-Software war es üblich, Code mit Trap-Befehlen anstelle von Coprozessor-Mathematikbefehlen zu erstellen, obwohl dies vom "Betriebssystem" technisch nicht durchgeführt wurde. Wenn kein mathematischer Coprozessor installiert wäre, würde der Trap-Handler diesen emulieren. Wenn ein Coprozessor installiert wurde, ersetzt der Handler beim ersten Aufrufen eines Traps den Trap-Befehl durch einen Coprozessor-Befehl. Zukünftige Ausführungen desselben Codes verwenden den Coprozessor-Befehl direkt.
quelle