J'essaie de déterminer les détails techniques des raisons pour lesquelles les logiciels produits à l'aide de langages de programmation pour certains systèmes d'exploitation ne fonctionnent qu'avec eux.
Je crois comprendre que les binaires sont spécifiques à certains processeurs en raison du langage machine spécifique au processeur qu'ils comprennent et des jeux d'instructions différents entre les différents processeurs. Mais d'où vient la spécificité du système d'exploitation? J'avais l'habitude de supposer que c'était des API fournies par le système d'exploitation, mais j'ai ensuite vu ce diagramme dans un livre:
Operating Systems - Internals and Design Principles 7th ed - W. Stallings (Pearson, 2012)
Comme vous pouvez le voir, les API ne sont pas indiquées comme faisant partie du système d'exploitation.
Si par exemple je construis un programme simple en C en utilisant le code suivant:
#include<stdio.h>
main()
{
printf("Hello World");
}
Le compilateur fait-il quelque chose de spécifique au système d'exploitation lors de la compilation?
Vous mentionnez comment, si le code est spécifique à un CPU, pourquoi doit-il être également spécifique à un OS. C'est en fait davantage une question intéressante que bon nombre des réponses ici ont supposé.
Le premier programme exécuté sur la plupart des architectures CPU s'exécute à l'intérieur de ce qu'on appelle l'anneau intérieur ou l'anneau . La façon dont un processeur spécifique Arch implémente les anneaux varie, mais il est vrai que presque tous les processeurs modernes ont au moins 2 modes de fonctionnement, l'un privilégié et exécutant du code `` bare metal '' qui peut effectuer toute opération légale que le processeur peut effectuer et l'autre est non fiable et exécute du code protégé qui ne peut exécuter qu'un ensemble défini de fonctionnalités sécurisées. Cependant, certains processeurs ont une granularité beaucoup plus élevée et pour utiliser les VM en toute sécurité, au moins 1 ou 2 anneaux supplémentaires sont nécessaires (souvent étiquetés avec des nombres négatifs), mais cela dépasse le cadre de cette réponse.
Dans les premiers systèmes DOS et autres systèmes à tâche unique, tout le code était exécuté dans l'anneau intérieur, chaque programme que vous avez exécuté avait la pleine puissance sur tout l'ordinateur et pouvait littéralement tout faire s'il se comportait mal, y compris l'effacement de toutes vos données ou même des dommages matériels dans quelques cas extrêmes, tels que la définition de modes d'affichage invalides sur de très vieux écrans d'affichage, pire, cela pourrait être causé par un code bogué sans aucune malveillance.
Ce code était en fait largement indépendant du système d'exploitation, tant que vous disposiez d'un chargeur capable de charger le programme en mémoire (assez simple pour les premiers formats binaires) et que le code ne reposait sur aucun pilote, implémentant lui-même tous les accès matériels sous lesquels il devrait fonctionner. n'importe quel OS tant qu'il est exécuté dans l'anneau 0. Remarque, un OS très simple comme celui-ci est généralement appelé un moniteur s'il est simplement utilisé pour exécuter d'autres programmes et n'offre aucune fonctionnalité supplémentaire.
Des systèmes d'exploitation plus modernes y compris UNIX , des versions de Windows commençant par NT et divers autres systèmes d'exploitation désormais obscurs ont décidé d'améliorer cette situation, les utilisateurs voulaient des fonctionnalités supplémentaires telles que multitâche afin qu'ils puissent exécuter plus d'une application à la fois et la protection, de sorte qu'un bogue (ou code malveillant) dans une application ne pourrait plus causer de dommages illimités à la machine et aux données.
Cela a été fait en utilisant les anneaux mentionnés ci-dessus, le système d'exploitation prendrait la seule place en cours d'exécution dans l'anneau 0 et les applications s'exécuteraient dans les anneaux externes non approuvés, uniquement capables d'effectuer un ensemble restreint d'opérations autorisées par l'OS.
Cependant, cette utilité et cette protection accrues ont un coût, les programmes doivent maintenant travailler avec le système d'exploitation pour effectuer des tâches qu'ils ne sont pas autorisés à effectuer eux-mêmes, ils ne peuvent plus, par exemple, prendre le contrôle direct du disque dur en accédant à sa mémoire et en modifiant arbitrairement données, au lieu de cela, ils devaient demander à l'OS d'effectuer ces tâches pour eux afin qu'il puisse vérifier qu'ils étaient autorisés à effectuer l'opération, sans modifier les fichiers qui ne leur appartenaient pas, il vérifierait également que l'opération était en effet valide et ne laisserait pas le matériel dans un état indéfini.
Chaque système d'exploitation a décidé d'une implémentation différente pour ces protections, en partie sur la base de l'architecture pour laquelle le système d'exploitation a été conçu et en partie sur la base de la conception et des principes du système d'exploitation en question, UNIX par exemple, a mis l'accent sur les machines pouvant être utilisées par plusieurs utilisateurs et ciblées les fonctionnalités disponibles pour cela alors que Windows a été conçu pour être plus simple, pour fonctionner sur du matériel plus lent avec un seul utilisateur. La façon dont les programmes de l'espace utilisateur communiquent également avec le système d'exploitation est complètement différente sur X86 comme elle le serait sur ARM ou MIPS par exemple, forçant un système d'exploitation multiplateforme à prendre des décisions basées sur la nécessité de travailler sur le matériel auquel il est destiné.
Ces interactions spécifiques au système d'exploitation sont généralement appelées "appels système" et englobent la façon dont un programme d'espace utilisateur interagit complètement avec le matériel via le système d'exploitation, elles diffèrent fondamentalement en fonction de la fonction du système d'exploitation et, par conséquent, un programme qui fait son travail via les appels système doit être spécifique au système d'exploitation.
En plus des appels système, chaque système d'exploitation fournit une méthode différente pour charger un programme à partir du support de stockage secondaire et en mémoire , afin d'être chargeable par un système d'exploitation spécifique, le programme doit contenir un en-tête spécial qui décrit au système d'exploitation comment il peut être chargé et exécuté.
Cet en-tête était assez simple pour que écrire un chargeur pour un format différent était presque trivial, cependant avec des formats modernes tels que elf qui supportent des fonctionnalités avancées telles que la liaison dynamique et les déclarations faibles, il est maintenant presque impossible pour un système d'exploitation pour tenter de charger des fichiers binaires qui n'ont pas été conçus pour cela, cela signifie que, même s'il n'y avait pas d'incompatibilités d'appel système, il est extrêmement difficile de placer un programme dans ram de manière à pouvoir l'exécuter.
Les programmes utilisent rarement directement les appels système, cependant, ils gagnent presque exclusivement leurs fonctionnalités via des bibliothèques qui enveloppent les appels système dans un format légèrement plus convivial pour le langage de programmation, par exemple, C a la bibliothèque standard C et la glibc sous Linux et les bibliothèques similaires et win32 sous Windows NT et supérieur, la plupart des autres langages de programmation ont également des bibliothèques similaires qui enveloppent les fonctionnalités du système de manière appropriée.
Ces bibliothèques peuvent dans une certaine mesure même surmonter les problèmes multiplates-formes décrits ci-dessus, il existe une gamme de bibliothèques conçues pour fournir une plate-forme uniforme aux applications tout en gérant en interne les appels vers un large éventail de systèmes d'exploitation tels que SDL , cela signifie que même si les programmes ne peuvent pas être compatibles binaires, les programmes qui utilisent ces bibliothèques peuvent avoir une source commune entre les plates-formes, ce qui rend le portage aussi simple que la recompilation.
Malgré tout ce que j'ai dit ici, il y a eu des tentatives pour surmonter les limites de ne pas pouvoir exécuter des programmes sur plus d'un système d'exploitation. Quelques bons exemples sont le projet Wine qui a réussi à émuler à la fois le chargeur de programme win32, le format binaire et les bibliothèques système permettant aux programmes Windows de s'exécuter sur différents UNIX. Il existe également une couche de compatibilité permettant à plusieurs systèmes d'exploitation BSD UNIX d'exécuter des logiciels Linux et bien sûr le propre module d'Apple permettant d'exécuter d'anciens logiciels MacOS sous MacOS X.
Cependant, ces projets fonctionnent grâce à des niveaux énormes d'efforts de développement manuel. Selon la différence entre les deux systèmes d'exploitation, la difficulté varie d'une cale assez petite à une émulation presque complète de l'autre système d'exploitation, ce qui est souvent plus complexe que d'écrire un système d'exploitation en lui-même et c'est donc l'exception et non la règle.
Comme vous pouvez le voir, les API ne sont pas indiquées comme faisant partie du système d'exploitation.
Je pense que vous lisez trop dans le diagramme. Oui, un système d'exploitation spécifiera une interface binaire pour la façon dont les fonctions du système d'exploitation sont appelées, et il définira également un format de fichier pour les exécutables, mais il fournira également une API, dans le sens de fournir un catalogue de fonctions qui peuvent être appelées par une application pour appeler les services OS.
Je pense que le diagramme essaie simplement de souligner que les fonctions du système d'exploitation sont généralement invoquées via un mécanisme différent d'un simple appel de bibliothèque. La plupart des systèmes d'exploitation courants utilisent des interruptions de processeur pour accéder aux fonctions du système d'exploitation. Les systèmes d'exploitation modernes typiques ne permettront pas à un programme utilisateur d'accéder directement à aucun matériel . Si vous voulez écrire un caractère sur la console, vous devrez demander au système d'exploitation de le faire pour vous. L'appel système utilisé pour écrire dans la console variera d'un système d'exploitation à l'autre, il y a donc un exemple de la raison pour laquelle le logiciel est spécifique au système d'exploitation.
printf est une fonction de la bibliothèque d'exécution C et dans une implémentation typique est une fonction assez complexe. Si vous google, vous pouvez trouver la source de plusieurs versions en ligne. Voir cette page pour une visite guidée . Dans l'herbe, il finit par effectuer un ou plusieurs appels système, et chacun de ces appels système est spécifique au système d'exploitation hôte.
Le compilateur fait-il quelque chose de spécifique au système d'exploitation lors de la compilation?
Probablement. À un certain moment au cours du processus de compilation et de liaison, votre code est transformé en un binaire spécifique au système d'exploitation et lié à toutes les bibliothèques requises. Votre programme doit être enregistré dans un format que le système d'exploitation attend pour que le système d'exploitation puisse charger le programme et commencer à l'exécuter. De plus, vous appelez la fonction de bibliothèque standard printf()
, qui à un certain niveau est implémentée en termes de services fournis par le système d'exploitation.
Les bibliothèques fournissent une interface - une couche d'abstraction du système d'exploitation et du matériel - et qui permet de recompiler votre programme pour un système d'exploitation ou un matériel différent. Mais cette abstraction existe au niveau source - une fois que le programme est compilé et lié, il est connecté à une implémentation spécifique de cette interface spécifique à un système d'exploitation donné.
Il y a un certain nombre de raisons, mais une raison très importante est que le système d'exploitation doit savoir lire la série d'octets qui composent votre programme en mémoire, trouver les bibliothèques qui vont avec ce programme et les charger en mémoire, et puis commencez à exécuter votre code de programme. Pour ce faire, les créateurs de l'OS créent un format particulier pour cette série d'octets afin que le code de l'OS sache où chercher les différentes parties de la structure de votre programme. Parce que les principaux systèmes d'exploitation ont des auteurs différents, ces formats ont souvent peu à voir les uns avec les autres. En particulier, le format exécutable Windows a peu en commun avec le format ELF que la plupart des variantes Unix utilisent. Donc, tout ce chargement, cette liaison dynamique et cette exécution de code doivent être spécifiques au système d'exploitation.
Ensuite, chaque système d'exploitation fournit un ensemble différent de bibliothèques pour parler à la couche matérielle. Ce sont les API que vous mentionnez, et ce sont généralement des bibliothèques qui présentent une interface plus simple au développeur tout en la traduisant en appels plus complexes et plus spécifiques dans les profondeurs du système d'exploitation lui-même, ces appels étant souvent non documentés ou sécurisés. Cette couche est souvent assez grise, les nouvelles API "OS" sont construites partiellement ou entièrement sur des API plus anciennes. Par exemple, dans Windows, la plupart des nouvelles API créées par Microsoft au fil des ans sont essentiellement des couches au-dessus des API Win32 d'origine.
Un problème qui ne se pose pas dans votre exemple, mais qui est l'un des plus importants auxquels les développeurs sont confrontés est l'interface avec le gestionnaire de fenêtres, pour présenter une interface graphique. La question de savoir si le gestionnaire de fenêtres fait partie du "système d'exploitation" dépend parfois de votre point de vue, ainsi que du système d'exploitation lui-même, l'interface graphique de Windows étant intégrée à l'OS à un niveau plus profond, tandis que les interfaces graphiques sous Linux et OS X sont plus directement séparés. Ceci est très important car aujourd'hui ce que les gens appellent généralement "Le système d'exploitation" est une bête beaucoup plus grosse que ce que les manuels scolaires ont tendance à décrire, car il comprend de nombreux composants de niveau application.
Enfin, ce n'est pas strictement un problème de système d'exploitation, mais un problème important dans la génération de fichiers exécutables est que différentes machines ont des cibles de langage d'assemblage différentes, et donc le code d'objet généré doit être différent. Il ne s'agit pas à proprement parler d'un problème de "système d'exploitation" mais plutôt d'un problème matériel, mais cela signifie que vous aurez besoin de versions différentes pour différentes plates-formes matérielles.
De ne autre réponse à moi:
Considérez les premières machines DOS et quelle était la véritable contribution de Microsoft au monde:
Autocad devait écrire des pilotes pour chaque imprimante sur laquelle ils pouvaient imprimer. Lotus 1-2-3 aussi. En fait, si vous vouliez imprimer votre logiciel, vous deviez écrire vos propres pilotes. S'il y avait 10 imprimantes et 10 programmes, alors 100 morceaux différents essentiellement du même code devaient être écrits séparément et indépendamment.
Ce que Windows 3.1 a essayé d'accomplir (avec GEM et tant d'autres couches d'abstraction) est de faire en sorte que le fabricant de l'imprimante écrive un pilote pour leur imprimante et le programmeur écrive un pilote pour la classe d'imprimante Windows.
Maintenant, avec 10 programmes et 10 imprimantes, seulement 20 morceaux de code doivent être écrits, et puisque le côté Microsoft du code était le même pour tout le monde, alors les exemples de MS signifiaient que vous aviez très peu de travail à faire.
Maintenant, un programme ne se limitait pas aux 10 imprimantes qu'ils avaient choisi de prendre en charge, mais à toutes les imprimantes dont les fabricants fournissaient des pilotes sous Windows.
Ainsi, le système d'exploitation fournit des services aux applications afin que les applications n'aient pas à effectuer un travail redondant.
Votre exemple de programme C utilise printf, qui envoie des caractères à stdout - une ressource spécifique au système d'exploitation qui affichera les caractères sur une interface utilisateur. Le programme n'a pas besoin de savoir où se trouve l'interface utilisateur - il peut être sous DOS, il peut être dans une fenêtre graphique, il peut être dirigé vers un autre programme et utilisé comme entrée dans un autre processus.
Étant donné que le système d'exploitation fournit ces ressources, les programmeurs peuvent accomplir beaucoup plus avec peu de travail.
Cependant, même le démarrage d'un programme est compliqué. Le système d'exploitation s'attend à ce qu'un fichier exécutable contienne au début certaines informations qui indiquent au système d'exploitation comment il doit être démarré, et dans certains cas (environnements plus avancés comme Android ou iOS) quelles ressources seront nécessaires qui doivent être approuvées car elles touchent des ressources en dehors du "bac à sable" - une mesure de sécurité pour aider à protéger les utilisateurs et autres applications contre les programmes qui se comportent mal.
Ainsi, même si le code machine exécutable est le même et qu'aucune ressource de système d'exploitation n'est requise, un programme compilé pour Windows ne fonctionnera pas sur un système d'exploitation OS X sans une couche d'émulation ou de traduction supplémentaire, même sur le même matériel exact.
Les premiers systèmes d'exploitation de style DOS pouvaient souvent partager des programmes, car ils implémentaient la même API dans le matériel (BIOS) et le système d'exploitation connecté au matériel pour fournir des services. Donc, si vous avez écrit et compilé un programme COM - qui n'est qu'une image mémoire d'une série d'instructions de processeur - vous pouvez l'exécuter sur CP/M, MS-DOS et plusieurs autres systèmes d'exploitation. En fait, vous pouvez toujours exécuter des programmes COM sur des machines Windows modernes. D'autres systèmes d'exploitation n'utilisent pas les mêmes crochets d'API du BIOS, de sorte que les programmes COM ne s'exécuteront pas sans, encore une fois, une couche d'émulation ou de traduction. Les programmes EXE suivent une structure qui comprend bien plus que de simples instructions de processeur, et ainsi avec les problèmes d'API, il ne fonctionnera pas sur une machine qui ne comprend pas comment la charger en mémoire et l'exécuter.
En fait, la vraie réponse est que si chaque système d'exploitation comprenait la même disposition de fichier binaire exécutable, et vous ne vous êtes limité qu'à des fonctions standardisées (comme dans la bibliothèque standard C) qui le système d'exploitation fourni (que les systèmes d'exploitation fournissent), puis votre logiciel serait, en fait, fonctionner sur n'importe quel système d'exploitation.
Bien sûr, la réalité est que ce n'est pas le cas. Un fichier EXE
n'a pas le même format qu'un fichier ELF
, même si les deux contiennent du code binaire pour le même CPU. * Ainsi, chaque système d'exploitation devrait être capable d'interpréter tous les fichiers formats, et ils ne l'ont tout simplement pas fait au début, et il n'y avait aucune raison pour qu'ils commencent à le faire plus tard (presque certainement pour des raisons commerciales plutôt que techniques).
De plus, votre programme doit probablement faire des choses que la bibliothèque C ne définit pas comment faire (même pour des choses simples comme lister le contenu d'un répertoire), et dans ces cas, chaque système d'exploitation fournit ses propres fonctions pour réaliser votre tâche, ce qui signifie naturellement qu'il n'y aura pas de dénominateur commun le plus bas à utiliser (sauf si vous faites ce dénominateur vous-même).
Donc en principe, c'est parfaitement possible. En fait, WINE exécute les exécutables Windows directement sous Linux.
Mais c'est une tonne de travail et (généralement) commercialement injustifié.
* Remarque: Il y a beaucoup de plus dans un fichier exécutable qu'un simple code binaire. Il y a tonne d'informations qui indiquent au système d'exploitation de quelles bibliothèques le fichier dépend, de combien de mémoire de pile il a besoin, de quelles fonctions il exporte vers autre bibliothèques qui peuvent en dépendre , où le système d'exploitation peut trouver des informations de débogage pertinentes, comment "re-localiser" le fichier en mémoire si nécessaire, comment faire fonctionner correctement la gestion des exceptions, etc. etc .... encore, là pourrait = être un format unique pour lequel tout le monde est d'accord, mais il n'y en a tout simplement pas.
Le diagramme a la couche "application" (la plupart du temps) séparée de la couche "système d'exploitation" par les "bibliothèques", et cela implique que "application" et "OS" n'ont pas besoin de se connaître. C'est une simplification dans le diagramme, mais ce n'est pas tout à fait vrai.
Le problème est que la "bibliothèque" comporte en fait trois parties: l'implémentation, l'interface avec l'application et l'interface avec le système d'exploitation. En principe, les deux premiers peuvent être rendus "universels" en ce qui concerne le système d'exploitation (cela dépend de l'endroit où vous le découpez), mais la troisième partie - l'interface avec le système d'exploitation - ne le peut généralement pas. L'interface avec le système d'exploitation dépendra nécessairement du système d'exploitation, des API qu'il fournit, du mécanisme de conditionnement (par exemple, le format de fichier utilisé par la DLL Windows), etc.
Étant donné que la "bibliothèque" est généralement mise à disposition sous la forme d'un package unique, cela signifie qu'une fois que le programme a choisi une "bibliothèque" à utiliser, il s'engage sur un système d'exploitation spécifique. Cela se produit de deux manières: a) le programmeur choisit complètement à l'avance, puis la liaison entre la bibliothèque et l'application peut être universelle, mais la bibliothèque elle-même est liée au système d'exploitation; ou b) le programmeur configure les choses de sorte que la bibliothèque soit sélectionnée lorsque vous exécutez le programme, mais le mécanisme de liaison lui-même, entre le programme et la bibliothèque, dépend du système d'exploitation (par exemple, le DLL sous Windows). Chacun a ses avantages et ses inconvénients, mais de toute façon vous devez faire un choix à l'avance.
Maintenant, cela ne signifie pas qu'il est impossible de le faire, mais vous devez être très intelligent. Pour surmonter le problème, vous devrez suivre la voie de la sélection de la bibliothèque au moment de l'exécution, et vous devrez trouver un mécanisme de liaison universel qui ne dépend pas du système d'exploitation (vous êtes donc responsable de la maintenir, beaucoup plus de travail). Parfois, cela en vaut la peine.
Vous n'êtes pas obligé de le faire, mais si vous allez faire l'effort de le faire, il y a de fortes chances que vous ne vouliez pas non plus être lié à un processeur spécifique, donc vous écrirez une machine virtuelle et vous compilerez votre programme dans un format de code neutre du processeur.
À présent, vous auriez dû remarquer où je vais. Les plates-formes linguistiques comme Java font exactement cela. Le Java runtime (bibliothèque) définit la liaison neutre du système d'exploitation entre votre Java = programme et la bibliothèque (comment le runtime Java ouvre et exécute votre programme), et il fournit une implémentation spécifique au système d'exploitation actuel. .NET fait la même chose dans une certaine mesure, sauf que Microsoft ne fournit pas de "bibliothèque" (runtime) pour tout sauf Windows (mais d'autres le font - voir Mono). En fait, Flash fait la même chose, bien que sa portée soit plus limitée au navigateur.
Enfin, il existe des moyens de faire la même chose sans mécanisme de liaison personnalisé. Vous pouvez utiliser des outils classiques, mais reporter l'étape de liaison à la bibliothèque jusqu'à ce que l'utilisateur choisisse le système d'exploitation. C'est exactement ce qui se passe lorsque vous distribuez le code source. L'utilisateur prend votre programme et le lie au processeur (compilez-le) et au système d'exploitation (liez-le) lorsque l'utilisateur est prêt à l'exécuter.
Tout dépend de la façon dont vous découpez les calques. À la fin de la journée, vous avez toujours un appareil informatique fabriqué avec du matériel spécifique exécutant un code machine spécifique. Les couches sont là en grande partie comme un cadre conceptuel.
Le logiciel n'est pas toujours spécifique au système d'exploitation. Java et le système de code p antérieur (et même ScummVM) permettent tous les deux un logiciel portable sur les systèmes d'exploitation. Infocom (fabricants de Zork et Z-machine ), avait également une base de données relationnelle basée sur une autre machine virtuelle. Cependant, à un certain niveau, quelque chose doit traduire même ces abstractions en instructions réelles à exécuter sur un ordinateur.
Vous dites
les logiciels produits à l'aide de langages de programmation pour certains systèmes d'exploitation ne fonctionnent qu'avec eux
Mais le programme que vous donnez à titre d'exemple fonctionnera sur de nombreux systèmes d'exploitation, et même certains environnements bare-metal.
L'important ici est la distinction entre le code source et le binaire compilé. Le langage de programmation C est spécifiquement conçu pour être indépendant du système d'exploitation sous forme source. Il le fait en laissant à l'implémenteur l'interprétation de choses comme "imprimer sur la console". Mais C peut être conforme à quelque chose qui est spécifique au système d'exploitation (voir les autres réponses pour des raisons). Par exemple, les formats exécutables PE ou ELF.
D'autres personnes ont bien couvert les détails techniques, je voudrais mentionner une raison moins technique, le côté UX/UI:
Chaque système d'exploitation possède ses propres API d'interface utilisateur et normes de conception. Il est possible d'écrire une interface utilisateur pour un programme et de l'exécuter sur plusieurs systèmes d'exploitation, mais cela ne fait que garantir que le programme se sentira à sa place partout. Faire une bonne interface utilisateur nécessite de peaufiner les détails de chaque plate-forme prise en charge.
Beaucoup de ces informations sont de petits détails, mais faites-les mal et vous frustrerez vos utilisateurs:
Même lorsqu'il est techniquement possible d'écrire une base de code d'interface utilisateur qui s'exécute partout, il est préférable de faire des ajustements pour chaque système d'exploitation pris en charge.