Ich versuche herauszufinden, warum Software, die mit Programmiersprachen für bestimmte Betriebssysteme erstellt wurde, nur mit ihnen funktioniert.
Ich verstehe, dass Binärdateien aufgrund der von ihnen verstandenen prozessorspezifischen Maschinensprache und der unterschiedlichen Befehlssätze zwischen den verschiedenen Prozessoren für bestimmte Prozessoren spezifisch sind. Aber woher kommt die Betriebssystemspezifität? Früher habe ich angenommen, dass es sich um APIs handelt, die vom Betriebssystem bereitgestellt wurden, aber dann habe ich dieses Diagramm in einem Buch gesehen:
Betriebssysteme - Interna und Designprinzipien 7. Aufl. - W. Stallings (Pearson, 2012)
Wie Sie sehen, werden APIs nicht als Teil des Betriebssystems angezeigt.
Wenn ich zum Beispiel ein einfaches Programm in C mit dem folgenden Code erstelle:
#include<stdio.h>
main()
{
printf("Hello World");
}
Tut der Compiler beim Kompilieren etwas Betriebssystemspezifisches?
quelle
printf
von msvcr90.dll ist nicht dasselbe wieprintf
von libc.so.6)Antworten:
Sie erwähnen, warum der Code auch für ein Betriebssystem spezifisch sein muss, wenn er für eine CPU spezifisch ist. Dies ist eigentlich eher eine interessante Frage, die viele der Antworten hier angenommen haben.
CPU-Sicherheitsmodell
Das erste Programm, das auf den meisten CPU-Architekturen ausgeführt wird, wird im sogenannten inneren Ring oder Ring 0 ausgeführt . Wie ein bestimmter CPU-Bogen Ringe implementiert, ist unterschiedlich, aber es steht fest, dass fast jede moderne CPU mindestens zwei Betriebsmodi hat, von denen einer privilegiert ist und Bare-Metal-Code ausführt, der alle legalen Operationen ausführen kann, die die CPU ausführen kann, und der andere nicht vertrauenswürdig und führt geschützten Code aus, der nur einen definierten sicheren Satz von Funktionen ausführen kann. Einige CPUs weisen jedoch eine weitaus höhere Granularität auf. Um VMs sicher zu verwenden, sind mindestens 1 oder 2 zusätzliche Ringe erforderlich (häufig mit negativen Zahlen gekennzeichnet). Dies würde jedoch den Rahmen dieser Antwort sprengen.
Woher kommt das Betriebssystem?
Frühe Single-Tasking-Betriebssysteme
In sehr frühen DOS- und anderen frühen Single-Tasking-basierten Systemen wurde der gesamte Code im inneren Ring ausgeführt. Jedes Programm, das Sie jemals ausgeführt haben, hatte die volle Leistung über den gesamten Computer und konnte buchstäblich alles tun, wenn es sich schlecht verhielt, einschließlich des Löschens aller Ihrer Daten oder sogar des Verursachens von Hardwareschäden In einigen extremen Fällen, wie dem Einstellen ungültiger Anzeigemodi auf sehr alten Bildschirmen, kann dies schlimmer sein, wenn der Code einfach fehlerfrei ist.
Dieser Code war in der Tat weitgehend betriebssystemunabhängig, solange Sie einen Loader hatten, der das Programm in den Speicher laden konnte (ziemlich einfach für frühe Binärformate), und der Code war nicht auf Treiber angewiesen und implementierte den gesamten Hardware-Zugriff selbst, unter dem er ausgeführt werden sollte Jedes Betriebssystem, solange es in Ring 0 ausgeführt wird. Beachten Sie, dass ein sehr einfaches Betriebssystem wie dieses normalerweise als Monitor bezeichnet wird, wenn es nur zum Ausführen anderer Programme verwendet wird und keine zusätzlichen Funktionen bietet.
Moderne Multi-Tasking-Betriebssysteme
Modernere Betriebssysteme, einschließlich UNIX , Windows- Versionen ab NT und verschiedene andere mittlerweile unklare Betriebssysteme, haben beschlossen, diese Situation zu verbessern. Benutzer wollten zusätzliche Funktionen wie Multitasking, damit sie mehr als eine Anwendung gleichzeitig ausführen können, und Schutz, also einen Fehler ( oder böswilliger Code) in einer Anwendung können nicht mehr unbegrenzt Schaden an Maschine und Daten anrichten.
Dies geschah mit den oben erwähnten Ringen, das Betriebssystem würde den einzigen Platz in Ring 0 einnehmen und Anwendungen würden in den äußeren nicht vertrauenswürdigen Ringen ausgeführt, nur in der Lage, einen eingeschränkten Satz von Operationen auszuführen, die das Betriebssystem erlaubte.
Diese Erhöhung der Nützlichkeit und des Schutzes war jedoch mit Kosten verbunden. Programme mussten nun mit dem Betriebssystem zusammenarbeiten, um Aufgaben auszuführen, die sie nicht selbst ausführen durften. Sie konnten beispielsweise nicht mehr die direkte Kontrolle über die Festplatte übernehmen, indem sie auf ihren Speicher zugegriffen und willkürlich geändert haben Stattdessen mussten sie das Betriebssystem auffordern, diese Aufgaben für sie auszuführen, damit überprüft werden konnte, ob sie die Operation ausführen durften. Dabei wurden keine Dateien geändert, die nicht zu ihnen gehörten. Außerdem wurde überprüft, ob die Operation tatsächlich gültig und gültig war würde die Hardware nicht in einem undefinierten Zustand belassen.
Jedes Betriebssystem entschied sich für eine andere Implementierung dieser Schutzfunktionen, teilweise basierend auf der Architektur, für die das Betriebssystem entwickelt wurde, und teilweise basierend auf dem Design und den Prinzipien des betreffenden Betriebssystems. UNIX legte beispielsweise den Schwerpunkt auf Maschinen, die für die Verwendung durch mehrere Benutzer geeignet und fokussiert sind Die dafür verfügbaren Funktionen wurden zwar so konzipiert, dass Windows einfacher ist und auf langsamerer Hardware mit einem einzelnen Benutzer ausgeführt werden kann. Die Art und Weise, in der User-Space-Programme auch mit dem Betriebssystem kommunizieren, ist auf X86 völlig anders als beispielsweise auf ARM oder MIPS, sodass ein Betriebssystem mit mehreren Plattformen Entscheidungen treffen muss, die auf der Notwendigkeit basieren, an der Hardware zu arbeiten, für die es gedacht ist.
Diese betriebssystemspezifischen Interaktionen werden in der Regel als "Systemaufrufe" bezeichnet und umfassen die vollständige Interaktion eines User Space-Programms mit der Hardware über das Betriebssystem. Sie unterscheiden sich grundlegend je nach der Funktion des Betriebssystems, und daher muss ein Programm, das seine Arbeit über Systemaufrufe ausführt, dies tun OS-spezifisch sein.
Der Program Loader
Zusätzlich zu den Systemaufrufen bietet jedes Betriebssystem eine andere Methode zum Laden eines Programms vom sekundären Speichermedium und in den Speicher . Um von einem bestimmten Betriebssystem geladen werden zu können, muss das Programm einen speziellen Header enthalten, der dem Betriebssystem beschreibt, wie es sein kann geladen und laufen.
Früher war dieser Header so einfach, dass das Schreiben eines Loaders für ein anderes Format fast trivial war. Mit modernen Formaten wie elf, die erweiterte Funktionen wie dynamische Verknüpfungen und schwache Deklarationen unterstützen, ist es für ein Betriebssystem jedoch nahezu unmöglich, Binärdateien zu laden was nicht dafür gedacht war, bedeutet, dass es auch ohne Systemaufruf-Inkompatibilitäten immens schwierig ist, ein Programm so in den RAM zu legen, dass es ausgeführt werden kann.
Bibliotheken
Programme verwenden selten Systemaufrufe direkt, sie erhalten ihre Funktionalität jedoch fast ausschließlich durch Bibliotheken, die die Systemaufrufe in ein etwas freundlicheres Format für die Programmiersprache umschließen, z. B. C hat die C-Standardbibliothek und glibc unter Linux und ähnlichen und win32-Bibliotheken unter Windows NT und höher, die meisten anderen Programmiersprachen haben ähnliche Bibliotheken, die die Systemfunktionalität in geeigneter Weise umschließen.
Diese Bibliotheken kann bis zu einem gewissen Grad auch die Cross - Plattform - Probleme überwinden , wie oben beschrieben, gibt es eine Reihe von Bibliotheken , die sich um die Bereitstellung eine einheitliche Plattform für Anwendungen ausgelegt sind , während intern Anrufe an eine breite Palette von OSes Verwaltung wie SDL , bedeutet dies , dass , obwohl Programme können nicht binärkompatibel sein. Programme, die diese Bibliotheken verwenden, können eine gemeinsame Quelle für die Plattformen haben, was die Portierung so einfach wie das erneute Kompilieren macht.
Ausnahmen nach oben
Trotz allem, was ich hier gesagt habe, gab es Versuche, die Einschränkungen zu überwinden, dass Programme nicht auf mehr als einem Betriebssystem ausgeführt werden können. Einige gute Beispiele sind das Wine-Projekt, bei dem sowohl der Win32-Programmlader als auch das Binärformat und die Systembibliotheken erfolgreich emuliert wurden, sodass Windows-Programme unter verschiedenen UNIX-Betriebssystemen ausgeführt werden können. Es gibt auch eine Kompatibilitätsschicht, mit der mehrere BSD UNIX-Betriebssysteme Linux-Software ausführen können, und natürlich Apples eigenes Shim, mit dem man alte MacOS-Software unter MacOS X ausführen kann.
Diese Projekte erfordern jedoch einen enormen manuellen Entwicklungsaufwand. Abhängig davon, wie unterschiedlich die beiden Betriebssysteme sind, reicht der Schwierigkeitsgrad von einer relativ kleinen Abweichung bis zur nahezu vollständigen Emulation des anderen Betriebssystems, was häufig komplexer ist als das Schreiben eines gesamten Betriebssystems an sich. Dies ist also die Ausnahme und nicht die Regel.
quelle
Ich denke, Sie lesen zu viel in das Diagramm. Ja, ein Betriebssystem gibt eine binäre Schnittstelle für den Aufruf von Betriebssystemfunktionen an und definiert auch ein Dateiformat für ausführbare Dateien, bietet jedoch auch eine API im Sinne eines Funktionskatalogs, der von aufgerufen werden kann eine Anwendung zum Aufrufen von Betriebssystemdiensten.
Ich denke, das Diagramm versucht nur zu betonen, dass Betriebssystemfunktionen normalerweise über einen anderen Mechanismus als einen einfachen Bibliotheksaufruf aufgerufen werden. Die meisten gängigen Betriebssysteme verwenden Prozessor-Interrupts, um auf Betriebssystemfunktionen zuzugreifen. Typische moderne Betriebssysteme werden nicht ein Anwenderprogramm direkt zugreifen lassen , jede Hardware. Wenn Sie ein Zeichen in die Konsole schreiben möchten, müssen Sie das Betriebssystem bitten, dies für Sie zu tun. Der zum Schreiben auf die Konsole verwendete Systemaufruf ist von Betriebssystem zu Betriebssystem unterschiedlich. Es gibt also ein Beispiel dafür, warum Software betriebssystemspezifisch ist.
printf ist eine Funktion aus der C-Laufzeitbibliothek und in einer typischen Implementierung eine ziemlich komplexe Funktion. Wenn Sie googeln, können Sie die Quelle für mehrere Versionen online finden. Auf dieser Seite finden Sie eine geführte Tour . Unten im Gras führt es jedoch zu einem oder mehreren Systemaufrufen, und jeder dieser Systemaufrufe ist spezifisch für das Host-Betriebssystem.
quelle
Wahrscheinlich. Irgendwann während des Kompilierungs- und Verknüpfungsprozesses wird Ihr Code in eine betriebssystemspezifische Binärdatei umgewandelt und mit allen erforderlichen Bibliotheken verknüpft. Ihr Programm muss in einem vom Betriebssystem erwarteten Format gespeichert werden, damit das Betriebssystem das Programm laden und ausführen kann. Außerdem rufen Sie die Standardbibliotheksfunktion auf
printf()
, die auf einer bestimmten Ebene in Bezug auf die vom Betriebssystem bereitgestellten Dienste implementiert ist.Bibliotheken bieten eine Schnittstelle - eine Abstraktionsebene vom Betriebssystem und der Hardware -, mit der Sie Ihr Programm für ein anderes Betriebssystem oder eine andere Hardware neu kompilieren können. Diese Abstraktion existiert jedoch auf der Quellebene - sobald das Programm kompiliert und verknüpft ist, ist es mit einer bestimmten Implementierung dieser Schnittstelle verbunden, die für ein bestimmtes Betriebssystem spezifisch ist.
quelle
Es gibt eine Reihe von Gründen, aber ein sehr wichtiger Grund ist, dass das Betriebssystem die Reihe von Bytes, aus denen sich Ihr Programm zusammensetzt, lesen und in den Speicher laden muss Starten Sie dann die Ausführung Ihres Programmcodes. Zu diesem Zweck erstellen die Ersteller des Betriebssystems ein bestimmtes Format für diese Reihe von Bytes, damit der Betriebssystemcode weiß, wo die verschiedenen Teile der Struktur Ihres Programms zu suchen sind. Da die wichtigsten Betriebssysteme unterschiedliche Autoren haben, haben diese Formate oft wenig miteinander zu tun. Insbesondere das ausführbare Windows-Format hat wenig mit dem ELF-Format gemein, das die meisten Unix-Varianten verwenden. Das Laden, dynamische Verknüpfen und Ausführen von Code muss also betriebssystemspezifisch sein.
Als nächstes stellt jedes Betriebssystem einen anderen Satz von Bibliotheken zur Verfügung, um mit der Hardwareschicht zu kommunizieren. Dies sind die APIs, die Sie erwähnen, und es handelt sich im Allgemeinen um Bibliotheken, die dem Entwickler eine einfachere Benutzeroberfläche bieten, während sie in komplexere, spezifischere Aufrufe in die Tiefe des Betriebssystems selbst übersetzt werden. Diese Aufrufe sind häufig nicht dokumentiert oder gesichert. Diese Ebene ist häufig recht grau, wobei neuere "OS" -APIs teilweise oder vollständig auf älteren APIs basieren. Beispielsweise sind in Windows viele der neueren APIs, die Microsoft im Laufe der Jahre erstellt hat, im Wesentlichen Schichten über den ursprünglichen Win32-APIs.
Ein Problem, das in Ihrem Beispiel nicht auftritt, bei dem es sich jedoch um eines der größeren Probleme handelt, mit denen Entwickler konfrontiert sind, ist die Schnittstelle mit dem Fenstermanager, um eine GUI zu präsentieren. Ob der Fenstermanager Teil des "Betriebssystems" ist, hängt manchmal von Ihrer Sichtweise sowie vom Betriebssystem selbst ab, wobei die GUI in Windows auf einer tieferen Ebene in das Betriebssystem integriert ist, während die GUIs unter Linux und OS X vorhanden sind direkter getrennt. Dies ist sehr wichtig, da das, was heutzutage üblicherweise als "Betriebssystem" bezeichnet wird, viel umfangreicher ist als das, was in Lehrbüchern beschrieben wird, da es viele, viele Komponenten auf Anwendungsebene enthält.
Schließlich handelt es sich nicht unbedingt um ein Betriebssystemproblem, sondern um ein wichtiges Problem bei der Generierung ausführbarer Dateien. Unterschiedliche Computer haben unterschiedliche Assemblersprachenziele, sodass der tatsächlich generierte Objektcode unterschiedlich sein muss. Dies ist streng genommen kein "Betriebssystem" -Problem, sondern ein Hardwareproblem, bedeutet jedoch, dass Sie für verschiedene Hardwareplattformen unterschiedliche Builds benötigen.
quelle
Aus einer anderen Antwort von mir:
Das Betriebssystem stellt also Dienste für die Anwendungen bereit, damit die Anwendungen keine redundanten Aufgaben ausführen müssen.
Ihr C-Beispielprogramm verwendet printf, das Zeichen an stdout sendet - eine betriebssystemspezifische Ressource, die die Zeichen auf einer Benutzeroberfläche anzeigt. Das Programm muss nicht wissen, wo sich die Benutzeroberfläche befindet - es könnte sich in DOS befinden, es könnte sich in einem grafischen Fenster befinden, es könnte zu einem anderen Programm weitergeleitet und als Eingabe für einen anderen Prozess verwendet werden.
Da das Betriebssystem diese Ressourcen zur Verfügung stellt, können Programmierer mit wenig Arbeit viel mehr erreichen.
Das Starten eines Programms ist jedoch kompliziert. Das Betriebssystem erwartet, dass eine ausführbare Datei zu Beginn bestimmte Informationen enthält, die das Betriebssystem darüber informieren, wie sie gestartet werden soll, und in einigen Fällen (in fortgeschritteneren Umgebungen wie Android oder iOS), welche Ressourcen erforderlich sind, die eine Genehmigung erfordern, da sie Ressourcen außerhalb des Betriebssystems berühren "Sandbox" - eine Sicherheitsmaßnahme zum Schutz von Benutzern und anderen Apps vor fehlerhaften Programmen.
Selbst wenn der ausführbare Maschinencode identisch ist und keine Betriebssystemressourcen erforderlich sind, kann ein für Windows kompiliertes Programm nicht unter einem OS X-Betriebssystem ohne zusätzliche Emulations- oder Übersetzungsschicht ausgeführt werden, auch nicht auf derselben Hardware.
Frühere Betriebssysteme im DOS-Stil konnten häufig Programme gemeinsam nutzen, da sie dieselbe API in der Hardware (BIOS) und das Betriebssystem in die Hardware eingebunden haben, um Dienste bereitzustellen. Wenn Sie also ein COM-Programm geschrieben und kompiliert haben, das nur ein Speicherabbild einer Reihe von Prozessoranweisungen ist, können Sie es auf CP / M, MS-DOS und mehreren anderen Betriebssystemen ausführen. Tatsächlich können Sie COM-Programme weiterhin auf modernen Windows-Computern ausführen. Andere Betriebssysteme verwenden nicht dieselben BIOS-API-Hooks, sodass die COM-Programme ohne Emulations- oder Übersetzungsschicht nicht auf ihnen ausgeführt werden können. EXE-Programme folgen einer Struktur, die viel mehr als nur Prozessoranweisungen enthält, und werden daher zusammen mit den API-Problemen nicht auf einem Computer ausgeführt, der nicht versteht, wie er in den Speicher geladen und ausgeführt wird.
quelle
Die eigentliche Antwort lautet: Wenn jedes Betriebssystem dasselbe ausführbare Binärdateilayout verstehen würde und Sie sich nur auf standardisierte Funktionen (wie in der C-Standardbibliothek) beschränken würden, die vom Betriebssystem bereitgestellt werden (von den Betriebssystemen bereitgestellt), würde Ihre Software dies tun In der Tat laufen auf jedem Betriebssystem.
In Wirklichkeit ist das natürlich nicht der Fall. Eine
EXE
Datei nicht über das gleiche Format wie eineELF
Datei, obwohl beide binären Code für die gleiche CPU enthalten. * Also jedes Betriebssystem in der Lage sein müssten alle Dateiformate zu interpretieren und sie nicht einfach tun dies in der Es gab keinen Grund, dies später zu tun (mit ziemlicher Sicherheit aus kommerziellen und nicht aus technischen Gründen).Darüber hinaus muss Ihr Programm wahrscheinlich Dinge tun, die die C-Bibliothek nicht definiert (selbst für einfache Dinge wie das Auflisten des Inhalts eines Verzeichnisses), und in diesen Fällen bietet jedes Betriebssystem seine eigenen Funktionen, um Ihre Ziele zu erreichen Aufgabe, was natürlich bedeutet, dass es keinen kleinsten gemeinsamen Nenner für Sie gibt (es sei denn, Sie machen diesen Nenner selbst).
Im Prinzip ist das also durchaus möglich. Tatsächlich führt WINE ausführbare Windows-Dateien direkt unter Linux aus.
Aber es ist eine Menge Arbeit und (normalerweise) kommerziell ungerechtfertigt.
* Hinweis: In einer ausführbaren Datei steckt viel mehr als nur Binärcode. Es gibt unzählige Informationen, die dem Betriebssystem mitteilen, von welchen Bibliotheken die Datei abhängt, wie viel Stapelspeicher sie benötigt, welche Funktionen sie in andere Bibliotheken exportiert, die möglicherweise davon abhängen, wo das Betriebssystem möglicherweise relevante Debug-Informationen findet. Suchen Sie "die Datei im Speicher neu", falls erforderlich, um sicherzustellen, dass die Ausnahmebehandlung ordnungsgemäß funktioniert, usw. usw. Auch hier könnte es ein einziges Format geben, über das sich alle einig sind, das es aber einfach nicht gibt.
quelle
Das Diagramm hat die "Anwendungs" -Schicht (meistens), die durch die "Bibliotheken" von der "Betriebssystem" -Schicht getrennt ist, und dies impliziert, dass "Anwendung" und "Betriebssystem" sich nicht kennen müssen. Das ist eine Vereinfachung im Diagramm, aber nicht ganz richtig.
Das Problem ist, dass die "Bibliothek" tatsächlich drei Teile enthält: die Implementierung, die Schnittstelle zur Anwendung und die Schnittstelle zum Betriebssystem. Grundsätzlich können die ersten beiden für das Betriebssystem "universell" gemacht werden (es hängt davon ab, wo Sie sie aufteilen), der dritte Teil - die Schnittstelle zum Betriebssystem - kann dies im Allgemeinen nicht. Die Schnittstelle zum Betriebssystem hängt notwendigerweise vom Betriebssystem, den bereitgestellten APIs, dem Paketierungsmechanismus (z. B. dem von der Windows-DLL verwendeten Dateiformat) usw. ab.
Da die "Bibliothek" im Allgemeinen als einzelnes Paket zur Verfügung gestellt wird, bedeutet dies, dass das Programm, sobald es eine "Bibliothek" zur Verwendung auswählt, eine Festschreibung für ein bestimmtes Betriebssystem vornimmt. Dies geschieht auf zwei Arten: a) Der Programmierer wählt die Option vollständig im Voraus aus, und dann kann die Bindung zwischen der Bibliothek und der Anwendung universell sein, die Bibliothek selbst ist jedoch an das Betriebssystem gebunden. oder b) der Programmierer richtet die Dinge so ein, dass die Bibliothek ausgewählt wird, wenn Sie das Programm ausführen, der Bindungsmechanismus selbst zwischen dem Programm und der Bibliothek jedoch vom Betriebssystem abhängt (z. B. der DLL-Mechanismus in Windows). Jedes hat seine Vor- und Nachteile, aber so oder so muss man eine Entscheidung im Voraus treffen.
Das heißt nicht, dass es unmöglich ist, aber man muss sehr schlau sein. Um das Problem zu lösen, müssten Sie die Bibliothek zur Laufzeit auswählen und einen universellen Bindungsmechanismus entwickeln, der nicht vom Betriebssystem abhängt. viel mehr Arbeit). Manchmal lohnt es sich.
Das müssen Sie nicht, aber wenn Sie sich die Mühe machen, das zu tun, besteht eine gute Chance, dass Sie auch nicht an einen bestimmten Prozessor gebunden sein möchten. Sie werden also eine virtuelle Maschine schreiben und kompilieren Ihr Programm in ein prozessorneutrales Code-Format.
Inzwischen solltest du gemerkt haben, wohin ich gehe. Sprachplattformen wie Java machen genau das. Die Java-Laufzeit (Bibliothek) definiert die betriebssystemneutrale Bindung zwischen Ihrem Java-Programm und der Bibliothek (wie die Java-Laufzeit Ihr Programm öffnet und ausführt) und stellt eine Implementierung bereit, die für das aktuelle Betriebssystem spezifisch ist. .NET macht in gewissem Umfang dasselbe, außer dass Microsoft für nichts anderes als Windows eine "Bibliothek" (Laufzeit) bereitstellt (andere jedoch - siehe Mono). Tatsächlich macht Flash das Gleiche, obwohl der Umfang auf den Browser beschränkt ist.
Schließlich gibt es Möglichkeiten, dasselbe ohne einen benutzerdefinierten Bindungsmechanismus zu tun. Sie können herkömmliche Tools verwenden, den Bindungsschritt für die Bibliothek jedoch verschieben, bis der Benutzer das Betriebssystem auswählt. Genau das passiert, wenn Sie den Quellcode verteilen. Der Benutzer nimmt Ihr Programm und bindet es an den Prozessor (kompiliert es) und das Betriebssystem (verknüpft es), wenn der Benutzer bereit ist, es auszuführen.
Es hängt alles davon ab, wie Sie die Ebenen schneiden. Letztendlich haben Sie immer ein Computergerät, das mit spezifischer Hardware und spezifischem Maschinencode hergestellt wurde. Die Ebenen dienen dort weitgehend als konzeptioneller Rahmen.
quelle
Software ist nicht immer betriebssystemspezifisch. Sowohl Java als auch das frühere p-Code-System (und sogar ScummVM) ermöglichen Software, die für alle Betriebssysteme portierbar ist. Infocom (Hersteller von Zork und der Z-Maschine ) verfügte auch über eine relationale Datenbank, die auf einer anderen virtuellen Maschine basierte. Auf einer gewissen Ebene muss jedoch etwas selbst diese Abstraktionen in tatsächliche Anweisungen umwandeln, die auf einem Computer ausgeführt werden sollen.
quelle
Du sagst
Das von Ihnen als Beispiel angegebene Programm funktioniert jedoch auf vielen Betriebssystemen und sogar in einigen Bare-Metal-Umgebungen.
Wichtig ist hier die Unterscheidung zwischen dem Quellcode und der kompilierten Binärdatei. Die Programmiersprache C wurde speziell so entwickelt, dass sie vom Betriebssystem unabhängig ist. Dies geschieht, indem die Interpretation von Dingen wie "Print to the Console" dem Implementierer überlassen wird. C kann jedoch mit betriebssystemspezifischen Anforderungen konform sein (Gründe finden Sie in den anderen Antworten). Zum Beispiel die ausführbaren Formate PE oder ELF.
quelle
Andere Leute haben die technischen Details gut abgedeckt, ich möchte einen weniger technischen Grund nennen, die UX / UI-Seite der Dinge:
Einmal schreiben, sich überall peinlich fühlen
Jedes Betriebssystem verfügt über eigene APIs für Benutzeroberflächen und Designstandards. Es ist möglich, eine Benutzeroberfläche für ein Programm zu schreiben und auf mehreren Betriebssystemen ausführen zu lassen, dies garantiert jedoch, dass das Programm überall fehl am Platz ist. Für eine gute Benutzeroberfläche müssen die Details für jede unterstützte Plattform angepasst werden.
Viele davon sind kleine Details, aber wenn Sie sie falsch verstehen, werden Sie Ihre Benutzer frustrieren:
Auch wenn es technisch möglich ist, eine UI-Codebasis zu schreiben, die überall ausgeführt wird, empfiehlt es sich, Anpassungen für jedes unterstützte Betriebssystem vorzunehmen.
quelle
Ein wichtiger Unterschied besteht an dieser Stelle darin, den Compiler vom Linker zu trennen. Der Compiler erzeugt höchstwahrscheinlich mehr oder weniger die gleiche Ausgabe (wobei die Unterschiede hauptsächlich auf verschiedene
#if WINDOWS
s zurückzuführen sind). Der Linker hingegen muss alle plattformspezifischen Dinge erledigen - die Bibliotheken verknüpfen, die ausführbare Datei erstellen usw.Mit anderen Worten, der Compiler kümmert sich hauptsächlich um die CPU-Architektur, da er den eigentlichen ausführbaren Code erzeugt und die Anweisungen und Ressourcen der CPU verwenden muss (beachten Sie, dass der IL- oder JVM-Bytecode von .NET als Anweisungssätze einer virtuellen CPU gelten würde in dieser Ansicht). Aus diesem Grund müssen Sie den Code beispielsweise für
x86
und separat kompilierenARM
.Der Linker hingegen muss all diese Rohdaten und Anweisungen in ein Format bringen, das der Loader (heutzutage wäre dies fast immer das Betriebssystem) verstehen kann, und alle statisch verknüpften Bibliotheken verknüpfen (Dazu gehört auch der Code, der für die dynamische Verknüpfung, die Speicherzuweisung usw. erforderlich ist.)
Mit anderen Worten, Sie können den Code möglicherweise nur einmal kompilieren und sowohl unter Linux als auch unter Windows ausführen. Sie müssen ihn jedoch zweimal verknüpfen , um zwei verschiedene ausführbare Dateien zu erstellen . In der Praxis müssen Sie jetzt häufig auch Code berücksichtigen (hier kommen die (Vor-) Compiler-Direktiven zum Einsatz), sodass selbst das zweimalige Kompilieren von Einmal-Links nicht häufig verwendet wird. Ganz zu schweigen davon, dass die Leute das Kompilieren und Verknüpfen während des Builds als einen einzigen Schritt behandeln (genau wie Sie sich nicht mehr um die Teile des Compilers selbst kümmern).
DOS-Software war oft eher binär portierbar, aber Sie müssen verstehen, dass sie nicht nur für DOS oder Unix kompiliert wurde, sondern auch für einen bestimmten Vertrag, der für die meisten PCs im IBM-Stil üblich war Software-Interrupts. Hierfür war keine statische Verknüpfung erforderlich, da Sie nur die erforderlichen Register setzen, z. B.
int 13h
Grafikfunktionen aufrufen und die CPU gerade zu einem in der Interrupt-Tabelle deklarierten Speicherzeiger gesprungen ist. Natürlich war das Üben auch hier viel kniffliger, da Sie alle diese Methoden selbst schreiben mussten, um die Leistung auf dem Metall zu verbessern, aber das bedeutete im Grunde, das Betriebssystem insgesamt zu umgehen. Und natürlich gibt es etwas, das immer eine Interaktion mit der OS-API erfordert - die Programmbeendigung. Wenn Sie jedoch die einfachsten verfügbaren Formate verwendet haben (zCOM
auf DOS, das keinen Header hat, nur Anweisungen) und nicht beenden wollte, na ja, viel Glück! Und natürlich können Sie die ordnungsgemäße Beendigung auch in der Laufzeit erledigen, sodass Sie Code für die Beendigung von Unix und DOS in derselben ausführbaren Datei haben und zur Laufzeit ermitteln können, welche zu verwenden ist :)quelle