Estoy tratando de determinar los detalles técnicos de por qué el software producido usando lenguajes de programación para ciertos sistemas operativos solo funciona con ellos.
Entiendo que los binarios son específicos de ciertos procesadores debido al lenguaje de máquina específico del procesador que entienden y los diferentes conjuntos de instrucciones entre los diferentes procesadores. Pero, ¿de dónde viene la especificidad del sistema operativo? Solía suponer que eran API proporcionadas por el sistema operativo, pero luego vi este diagrama en un libro:
Sistemas operativos: principios internos y principios de diseño 7ª ed. W. Stallings (Pearson, 2012)
Como puede ver, las API no están indicadas como parte del sistema operativo.
Si, por ejemplo, construyo un programa simple en C usando el siguiente código:
#include<stdio.h>
main()
{
printf("Hello World");
}
¿El compilador está haciendo algo específico del sistema operativo al compilar esto?
fuente
printf
desde msvcr90.dll no es lo mismo queprintf
desde libc.so.6)Respuestas:
Usted menciona cómo si el código es específico para una CPU, ¿por qué debe ser específico también para un sistema operativo? Esta es realmente una pregunta más interesante que muchas de las respuestas aquí han asumido.
Modelo de seguridad de la CPU
El primer programa que se ejecuta en la mayoría de las arquitecturas de CPU se ejecuta dentro de lo que se denomina anillo interno o anillo 0 . La forma en que un arco de CPU específico implementa los anillos varía, pero es evidente que casi todas las CPU modernas tienen al menos 2 modos de operación, uno que es privilegiado y ejecuta código 'bare metal' que puede realizar cualquier operación legal que la CPU pueda realizar y el otro es no confiable y ejecuta código protegido que solo puede realizar un conjunto seguro de capacidades definido. Sin embargo, algunas CPU tienen una granularidad mucho mayor y, para poder usar las VM de forma segura, se necesitan al menos 1 o 2 anillos adicionales (a menudo etiquetados con números negativos), sin embargo, esto está fuera del alcance de esta respuesta.
Donde entra el sistema operativo
Primeros sistemas operativos de tareas individuales
En los primeros sistemas basados en DOS y otros sistemas basados en tareas individuales, todo el código se ejecutaba en el anillo interno, cada programa que ejecutaba tenía plena potencia sobre toda la computadora y podía hacer literalmente cualquier cosa si se comportaba mal, incluso borrar todos sus datos o incluso dañar el hardware. En algunos casos extremos, como la configuración de modos de visualización no válidos en pantallas de visualización muy antiguas, peor aún, esto podría deberse a un código con errores sin malicia alguna.
De hecho, este código era en gran medida independiente del sistema operativo, siempre que tuviera un cargador capaz de cargar el programa en la memoria (bastante simple para los primeros formatos binarios) y el código no dependía de ningún controlador, implementando todo el acceso de hardware en sí mismo bajo el cual debería ejecutarse cualquier sistema operativo siempre que se ejecute en el anillo 0. Tenga en cuenta que un sistema operativo muy simple como este generalmente se denomina monitor si simplemente se utiliza para ejecutar otros programas y no ofrece funcionalidad adicional.
Sistemas operativos multitarea modernos
Los sistemas operativos más modernos, como UNIX , las versiones de Windows que comienzan con NT y otros sistemas operativos ahora oscuros decidieron mejorar esta situación, los usuarios querían características adicionales como la multitarea para poder ejecutar más de una aplicación a la vez y protección, por lo que un error ( o código malicioso) en una aplicación ya no podría causar daños ilimitados a la máquina y los datos.
Esto se realizó utilizando los anillos mencionados anteriormente, el sistema operativo ocuparía el único lugar ejecutándose en el anillo 0 y las aplicaciones se ejecutarían en los anillos externos no confiables, solo capaces de realizar un conjunto restringido de operaciones que el sistema operativo permitía.
Sin embargo, esta mayor utilidad y protección tuvo un costo, los programas ahora tenían que trabajar con el sistema operativo para realizar tareas que no se les permitía hacer, ya no podían, por ejemplo, tomar el control directo sobre el disco duro accediendo a su memoria y cambiar arbitrariamente datos, en su lugar, tuvieron que pedirle al sistema operativo que realizara estas tareas por ellos para que pudiera verificar que se les permitiera realizar la operación, sin cambiar los archivos que no les pertenecían, también verificaría que la operación fuera realmente válida y no dejaría el hardware en un estado indefinido.
Cada sistema operativo decidió una implementación diferente para estas protecciones, parcialmente basada en la arquitectura para la cual fue diseñado el sistema operativo y parcialmente basada en el diseño y los principios del sistema operativo en cuestión, UNIX, por ejemplo, se enfocó en que las máquinas sean buenas para el uso multiusuario y enfocadas Las características disponibles para esto mientras Windows fue diseñado para ser más simple, para ejecutarse en hardware más lento con un solo usuario. La forma en que los programas de espacio de usuario también se comunican con el sistema operativo es completamente diferente en X86 como lo sería en ARM o MIPS, por ejemplo, lo que obliga a un sistema operativo multiplataforma a tomar decisiones basadas en la necesidad de trabajar en el hardware al que está dirigido.
Estas interacciones específicas del sistema operativo generalmente se denominan "llamadas al sistema" y abarcan cómo un programa espacial de usuario interactúa completamente con el hardware a través del sistema operativo, fundamentalmente difieren según la función del sistema operativo y, por lo tanto, un programa que hace su trabajo a través de las llamadas del sistema necesita ser específico del sistema operativo.
El cargador de programas
Además de las llamadas al sistema, cada sistema operativo proporciona un método diferente para cargar un programa desde el medio de almacenamiento secundario y en la memoria , para que un sistema operativo específico pueda cargarlo, el programa debe contener un encabezado especial que describa al sistema operativo cómo puede ser cargado y corrido.
Este encabezado solía ser lo suficientemente simple como para que escribir un cargador para un formato diferente fuera casi trivial, sin embargo, con formatos modernos como elf que admiten funciones avanzadas como el enlace dinámico y declaraciones débiles, ahora es casi imposible que un sistema operativo intente cargar binarios. que no fueron diseñados para ello, esto significa que, incluso si no existieran las incompatibilidades de llamadas del sistema, es inmensamente difícil incluso colocar un programa en ram de una manera que pueda ejecutarse.
Bibliotecas
Los programas rara vez usan las llamadas del sistema directamente, sin embargo, obtienen casi exclusivamente su funcionalidad a través de bibliotecas que envuelven las llamadas del sistema en un formato un poco más amigable para el lenguaje de programación, por ejemplo, C tiene la Biblioteca estándar C y glibc en Linux y libs similares y win32 en Windows NT y superior, la mayoría de los otros lenguajes de programación también tienen bibliotecas similares que ajustan la funcionalidad del sistema de manera adecuada.
Hasta cierto punto, estas bibliotecas pueden superar los problemas de plataforma cruzada como se describió anteriormente, hay una gama de bibliotecas que están diseñadas para proporcionar una plataforma uniforme a las aplicaciones mientras se administran internamente las llamadas a una amplia gama de sistemas operativos como SDL , esto significa que aunque los programas no pueden ser compatibles con los binarios, los programas que usan estas bibliotecas pueden tener una fuente común entre plataformas, lo que hace que la transferencia sea tan simple como la recompilación.
Excepciones a lo anterior
A pesar de todo lo que he dicho aquí, ha habido intentos de superar las limitaciones de no poder ejecutar programas en más de un sistema operativo. Algunos buenos ejemplos son el proyecto Wine que ha emulado con éxito el cargador de programas win32, el formato binario y las bibliotecas del sistema, lo que permite que los programas de Windows se ejecuten en varios UNIX. También hay una capa de compatibilidad que permite que varios sistemas operativos BSD UNIX ejecuten software de Linux y, por supuesto, la propia cuña de Apple que permite ejecutar software antiguo de MacOS en MacOS X.
Sin embargo, estos proyectos funcionan a través de enormes niveles de esfuerzo de desarrollo manual. Dependiendo de cuán diferentes sean los dos sistemas operativos, la dificultad varía desde una cuña bastante pequeña hasta una emulación casi completa del otro sistema operativo, que a menudo es más complejo que escribir un sistema operativo completo en sí mismo, por lo que esta es la excepción y no la regla.
fuente
Creo que estás leyendo demasiado en el diagrama. Sí, un sistema operativo especificará una interfaz binaria para la forma en que se llaman las funciones del sistema operativo, y también definirá un formato de archivo para los ejecutables, pero también proporcionará una API, en el sentido de proporcionar un catálogo de funciones a las que puede llamar una aplicación para invocar servicios del sistema operativo.
Creo que el diagrama solo está tratando de enfatizar que las funciones del sistema operativo generalmente se invocan a través de un mecanismo diferente al de una simple llamada a la biblioteca. La mayoría de los sistemas operativos comunes utilizan interrupciones del procesador para acceder a las funciones del sistema operativo. Los sistemas operativos modernos típicos no permitirán que un programa de usuario acceda directamente a ningún hardware. Si desea escribir un personaje en la consola, tendrá que pedirle al sistema operativo que lo haga por usted. La llamada al sistema utilizada para escribir en la consola variará de un sistema operativo a otro, por lo que hay un ejemplo de por qué el software es específico del sistema operativo.
printf es una función de la biblioteca de tiempo de ejecución C y en una implementación típica es una función bastante compleja. Si buscas en Google, puedes encontrar la fuente de varias versiones en línea. Vea esta página para una visita guiada de uno . Abajo, aunque termina haciendo una o más llamadas al sistema, y cada una de esas llamadas al sistema es específica del sistema operativo del host.
fuente
Probablemente. En algún momento durante el proceso de compilación y vinculación, su código se convierte en un binario específico del sistema operativo y se vincula con las bibliotecas necesarias. Su programa debe guardarse en un formato que el sistema operativo espera para que el sistema operativo pueda cargar el programa y comenzar a ejecutarlo. Además, está llamando a la función de biblioteca estándar
printf()
, que en algún nivel se implementa en términos de los servicios que proporciona el sistema operativo.Las bibliotecas proporcionan una interfaz, una capa de abstracción del sistema operativo y el hardware, y eso permite recompilar su programa para un sistema operativo diferente o un hardware diferente. Pero esa abstracción existe en el nivel fuente: una vez que el programa se compila y se vincula, se conecta a una implementación específica de esa interfaz que es específica de un sistema operativo determinado.
fuente
Hay varias razones, pero una muy importante es que el sistema operativo debe saber cómo leer la serie de bytes que componen su programa en la memoria, encontrar las bibliotecas que van con ese programa y cargarlas en la memoria, y luego comience a ejecutar su código de programa. Para hacer esto, los creadores del sistema operativo crean un formato particular para esa serie de bytes para que el código del sistema operativo sepa dónde buscar las diversas partes de la estructura de su programa. Debido a que los principales sistemas operativos tienen diferentes autores, estos formatos a menudo tienen poco que ver entre sí. En particular, el formato ejecutable de Windows tiene poco en común con el formato ELF que utilizan la mayoría de las variantes de Unix. Por lo tanto, todo este código de carga, vinculación dinámica y ejecución debe ser específico del sistema operativo.
A continuación, cada sistema operativo proporciona un conjunto diferente de bibliotecas para hablar con la capa de hardware. Estas son las API que mencionas, y generalmente son bibliotecas que presentan una interfaz más simple para el desarrollador mientras se traducen en llamadas más complejas y específicas a las profundidades del sistema operativo en sí, estas llamadas a menudo están indocumentadas o aseguradas. Esta capa a menudo es bastante gris, con las nuevas API de "SO" que se construyen parcial o totalmente en las API más antiguas. Por ejemplo, en Windows, muchas de las API más nuevas que Microsoft ha creado a lo largo de los años son esencialmente capas superiores a las API Win32 originales.
Un problema que no surge en su ejemplo, pero que es uno de los más grandes que enfrentan los desarrolladores es la interfaz con el administrador de ventanas, para presentar una GUI. Si el administrador de ventanas es parte del "SO" a veces depende de su punto de vista, así como del SO en sí, con la GUI en Windows integrada con el SO en un nivel más profundo, mientras que las GUI en Linux y OS X Más directamente separados. Esto es muy importante porque hoy en día lo que la gente suele llamar "El sistema operativo" es una bestia mucho más grande que lo que los libros de texto tienden a describir, ya que incluye muchos componentes de nivel de aplicación.
Finalmente, no es estrictamente un problema del sistema operativo, pero uno importante en la generación de archivos ejecutables es que diferentes máquinas tienen diferentes objetivos de lenguaje ensamblador, por lo que el código objeto generado real debe ser diferente. Esto no es estrictamente un problema de "sistema operativo", sino un problema de hardware, pero sí significa que necesitará diferentes compilaciones para diferentes plataformas de hardware.
fuente
De otra respuesta mía:
Por lo tanto, el sistema operativo proporciona servicios a las aplicaciones para que las aplicaciones no tengan que hacer un trabajo redundante.
Su programa C de ejemplo utiliza printf, que envía caracteres a stdout, un recurso específico del sistema operativo que mostrará los caracteres en una interfaz de usuario. El programa no necesita saber dónde está la interfaz de usuario: podría estar en DOS, podría estar en una ventana gráfica, podría canalizarse a otro programa y usarse como entrada para otro proceso.
Debido a que el sistema operativo proporciona estos recursos, los programadores pueden lograr mucho más con poco trabajo.
Sin embargo, incluso comenzar un programa es complicado. El sistema operativo espera que un archivo ejecutable tenga cierta información al principio que le indica al sistema operativo cómo debe iniciarse y, en algunos casos (entornos más avanzados como Android o iOS), qué recursos se requerirán que necesiten aprobación, ya que tocan recursos fuera del "sandbox": una medida de seguridad para ayudar a proteger a los usuarios y otras aplicaciones de los programas que se comportan mal.
Entonces, incluso si el código de máquina ejecutable es el mismo, y no se requieren recursos del sistema operativo, un programa compilado para Windows no se ejecutará en un sistema operativo OS X sin una capa de emulación o traducción adicional, incluso en el mismo hardware exacto.
Los primeros sistemas operativos de estilo DOS a menudo podían compartir programas, porque implementaban la misma API en hardware (BIOS) y el sistema operativo conectado al hardware para proporcionar servicios. Entonces, si escribió y compiló un programa COM , que es solo una imagen de memoria de una serie de instrucciones del procesador, podría ejecutarlo en CP / M, MS-DOS y varios otros sistemas operativos. De hecho, aún puede ejecutar programas COM en máquinas Windows modernas. Otros sistemas operativos no utilizan los mismos enlaces API de BIOS, por lo que los programas COM no se ejecutarán en ellos sin, nuevamente, una capa de emulación o traducción. Los programas EXE siguen una estructura que incluye mucho más que simples instrucciones de procesador, por lo que, junto con los problemas de API, no se ejecutará en una máquina que no entienda cómo cargarla en la memoria y ejecutarla.
fuente
En realidad, la respuesta real es que si cada sistema operativo entendiera el mismo diseño de archivo binario ejecutable, y solo se limitara a funciones estandarizadas (como en la biblioteca estándar C) que el sistema operativo proporcionó (qué sistemas operativos proporcionan), entonces su software lo haría , de hecho, se ejecuta en cualquier sistema operativo.
Por supuesto, la realidad es que ese no es el caso. Un
EXE
archivo no tiene el mismo formato que unELF
archivo, aunque ambos contienen código binario para la misma CPU. * Por lo tanto, cada sistema operativo debería ser capaz de interpretar todos los formatos de archivo, y simplemente no hicieron esto en el comenzando, y no había razón para que comenzaran a hacerlo más tarde (casi seguramente por razones comerciales más que técnicas).Además, su programa probablemente necesita hacer cosas que la biblioteca C no define cómo hacer (incluso para cosas simples como enumerar el contenido de un directorio), y en esos casos cada sistema operativo proporciona sus propias funciones para lograr su tarea, lo que naturalmente significa que no habrá un mínimo común denominador para que uses (a menos que lo hagas tú mismo).
Entonces, en principio, es perfectamente posible. De hecho, WINE ejecuta ejecutables de Windows directamente en Linux.
Pero es un montón de trabajo y (generalmente) comercialmente injustificado.
* Nota: Hay mucho más en un archivo ejecutable que solo código binario. Hay un montón de información que le dice al sistema operativo de qué bibliotecas depende el archivo, cuánta memoria de pila necesita, qué funciones exporta a otras bibliotecas que pueden depender de él, dónde el sistema operativo puede encontrar información de depuración relevante, cómo " vuelva a ubicar "el archivo en la memoria si es necesario, cómo hacer que el manejo de excepciones funcione correctamente, etc., etc. de nuevo, podría haber un formato único para esto en el que todos estén de acuerdo, pero simplemente no lo hay.
fuente
El diagrama tiene la capa de "aplicación" (en su mayoría) separada de la capa de "sistema operativo" por las "bibliotecas", y eso implica que "aplicación" y "SO" no necesitan conocerse entre sí. Esa es una simplificación en el diagrama, pero no es del todo cierto.
El problema es que la "biblioteca" tiene tres partes: la implementación, la interfaz de la aplicación y la interfaz del sistema operativo. En principio, los dos primeros se pueden hacer "universales" en lo que respecta al sistema operativo (depende de dónde lo corte), pero la tercera parte, la interfaz con el sistema operativo, generalmente no. La interfaz con el sistema operativo dependerá necesariamente del sistema operativo, las API que proporciona, el mecanismo de empaquetado (por ejemplo, el formato de archivo utilizado por Windows DLL), etc.
Debido a que la "biblioteca" generalmente está disponible como un paquete único, significa que una vez que el programa elige una "biblioteca" para usar, se compromete con un sistema operativo específico. Esto sucede de una de dos maneras: a) el programador selecciona completamente por adelantado, y luego el enlace entre la biblioteca y la aplicación puede ser universal, pero la biblioteca en sí está vinculada al sistema operativo; o b) el programador configura las cosas para que la biblioteca se seleccione cuando ejecuta el programa, pero luego el mecanismo de enlace , entre el programa y la biblioteca, depende del sistema operativo (por ejemplo, el mecanismo DLL en Windows). Cada uno tiene sus ventajas y desventajas, pero de cualquier manera debe elegir con anticipación.
Ahora, esto no significa que sea imposible hacerlo, pero debes ser muy inteligente. Para superar el problema, tendría que seguir la ruta de elegir la biblioteca en tiempo de ejecución, y tendría que encontrar un mecanismo de enlace universal que no dependa del sistema operativo (por lo que es responsable de mantenerlo, mucho más trabajo). Algunas veces vale la pena.
No tiene que hacerlo, pero si va a hacer el esfuerzo para hacerlo, es muy probable que tampoco quiera estar vinculado a un procesador específico, por lo que escribirá una máquina virtual y compilará su programa a un formato de código neutral de procesador.
A estas alturas ya deberías haber notado a dónde voy. Las plataformas de lenguaje como Java hacen exactamente eso. El tiempo de ejecución de Java (biblioteca) define el enlace neutral del sistema operativo entre su programa de Java y la biblioteca (cómo el tiempo de ejecución de Java abre y ejecuta su programa), y proporciona una implementación específica para el sistema operativo actual. .NET hace lo mismo hasta cierto punto, excepto que Microsoft no proporciona una "biblioteca" (tiempo de ejecución) para nada que no sea Windows (pero otros sí, vea Mono). Y, en realidad, Flash también hace lo mismo, aunque tiene un alcance más limitado para el navegador.
Finalmente, hay formas de hacer lo mismo sin un mecanismo de enlace personalizado. Puede usar herramientas convencionales, pero difiere el paso de enlace a la biblioteca hasta que el usuario elija el sistema operativo. Eso es exactamente lo que sucede cuando distribuye el código fuente. El usuario toma su programa y lo vincula al procesador (compilarlo) y al sistema operativo (vincularlo) cuando el usuario está listo para ejecutarlo.
Todo depende de cómo corte las capas. Al final del día, siempre tiene un dispositivo informático hecho con hardware específico que ejecuta un código de máquina específico. Las capas están allí en gran medida como un marco conceptual.
fuente
El software no siempre es específico del sistema operativo. Tanto Java como el sistema de código p anterior (e incluso ScummVM) permiten el software que es portátil en todos los sistemas operativos. Infocom (fabricantes de Zork y la máquina Z ), también tenía una base de datos relacional basada en otra máquina virtual. Sin embargo, en algún nivel algo tiene que traducir incluso esas abstracciones en instrucciones reales para ser ejecutadas en una computadora.
fuente
Tu dices
Pero el programa que da como ejemplo funcionará en muchos sistemas operativos, e incluso en algunos entornos de metal desnudo.
Lo importante aquí es la distinción entre el código fuente y el binario compilado. El lenguaje de programación C está específicamente diseñado para ser independiente del sistema operativo en forma de fuente. Lo hace dejando la interpretación de cosas como "imprimir en la consola" hasta el implementador. Pero C puede cumplir con algo que es específico del sistema operativo (ver otras respuestas por razones). Por ejemplo, los formatos ejecutables PE o ELF.
fuente
Otras personas han cubierto bien los detalles técnicos, me gustaría mencionar una razón menos técnica, el lado UX / UI de las cosas:
Escribe una vez, siéntete incómodo en todas partes
Cada sistema operativo tiene sus propias API de interfaz de usuario y estándares de diseño. Es posible escribir una interfaz de usuario para un programa y ejecutarlo en múltiples sistemas operativos, sin embargo, hacerlo garantiza que el programa se sentirá fuera de lugar en todas partes. Hacer una buena interfaz de usuario requiere ajustar los detalles de cada plataforma compatible.
Muchos de estos son pequeños detalles, pero si se equivocan, frustrará a sus usuarios:
Incluso cuando es técnicamente posible escribir una base de código de interfaz de usuario que se ejecute en todas partes, es mejor realizar ajustes para cada sistema operativo compatible.
fuente
Una distinción importante en este punto es separar el compilador del enlazador. Lo más probable es que el compilador produzca más o menos el mismo resultado (las diferencias se deben principalmente a varios
#if WINDOWS
s). El vinculador, por otro lado, tiene que manejar todas las cosas específicas de la plataforma: vincular las bibliotecas, construir el archivo ejecutable, etc.En otras palabras, el compilador se preocupa principalmente por la arquitectura de la CPU, porque está produciendo el código ejecutable real, y tiene que usar las instrucciones y los recursos de la CPU (tenga en cuenta que el código de bytes IL o JVM de .NET se consideraría un conjunto de instrucciones de una CPU virtual en esta vista). Es por eso que debe compilar código por separado para
x86
yARM
, por ejemplo.El vinculador, por otro lado, tiene que tomar todos estos datos en bruto e instrucciones, y ponerlo en un formato que el cargador (en la actualidad, casi siempre sería el sistema operativo) pueda entender, así como vincular cualquier biblioteca estáticamente vinculada (que también incluye el código requerido para la vinculación dinámica, asignación de memoria, etc.).
En otras palabras, podría compilar el código solo una vez y ejecutarlo tanto en Linux como en Windows, pero debe vincularlo dos veces, produciendo dos ejecutables diferentes. Ahora, en la práctica, a menudo también tiene que hacer concesiones en el código (ahí es donde entran las directivas del (pre) compilador), por lo que incluso compilar una vez el enlace dos veces no se usa mucho. Sin mencionar que las personas tratan la compilación y la vinculación como un solo paso durante la compilación (al igual que a usted ya no le importan las partes del compilador).
El software de la era de DOS a menudo era más portátil binario, pero hay que entender que también se compiló no contra DOS o Unix, sino contra un cierto contrato que era común para la mayoría de las PC de estilo IBM, descargando lo que hoy son llamadas API a interrupciones de software. Esto no necesitaba un enlace estático, ya que solo tenía que establecer los registros necesarios, llamar, por ejemplo,
int 13h
para funciones gráficas, y la CPU simplemente saltó a un puntero de memoria declarado en la tabla de interrupciones. Por supuesto, una vez más, la práctica fue mucho más complicada, porque para obtener el rendimiento de pedal al metal, tenía que escribir todos esos métodos usted mismo, pero eso básicamente equivalía a sortear el sistema operativo por completo. Y, por supuesto, hay algo que invariablemente necesita interacción con la API del sistema operativo: la finalización del programa. Pero aún así, si utilizó los formatos más simples disponibles (p. Ej.COM
en DOS, que no tiene encabezado, solo instrucciones) y no quería salir, bueno, ¡suerte! Y, por supuesto, también podría manejar la terminación adecuada en el tiempo de ejecución, por lo que podría tener código para la terminación de Unix y la terminación de DOS en el mismo ejecutable, y detectar en el tiempo de ejecución cuál usar :)fuente