¿Un sistema operativo inyecta su propio código de máquina cuando abre un programa?

32

Estoy estudiando CPU y sé cómo lee un programa de la memoria y ejecuto sus instrucciones. También entiendo que un sistema operativo separa los programas en los procesos, y luego alterna entre cada uno tan rápido que cree que se están ejecutando al mismo tiempo, pero de hecho cada programa se ejecuta solo en la CPU. Pero, si el sistema operativo también es un montón de código que se ejecuta en la CPU, ¿cómo puede administrar los procesos?

He estado pensando y la única explicación que podría pensar es: cuando el sistema operativo carga un programa desde la memoria externa a la RAM, agrega sus propias instrucciones en el medio de las instrucciones originales del programa, de modo que el programa se ejecuta, el programa puede llamar al sistema operativo y hacer algunas cosas. Creo que hay una instrucción que el sistema operativo agregará al programa, que permitirá que la CPU regrese al código del sistema operativo en algún momento. Y también, creo que cuando el sistema operativo carga un programa, comprueba si hay algunas instrucciones prohibidas (que saltarían a direcciones prohibidas en la memoria) y las elimina.

¿Estoy pensando bien? No soy un estudiante de CS, pero de hecho, un estudiante de matemáticas. Si es posible, me gustaría un buen libro sobre esto, porque no encontré a nadie que explique cómo el sistema operativo puede administrar un proceso si el sistema operativo también es un montón de código que se ejecuta en la CPU, y no puede ejecutarse al mismo tiempo del programa. Los libros solo dicen que el sistema operativo puede administrar las cosas, pero ahora cómo.

Revertir Sumoda
fuente
77
Consulte: Cambio de contexto El sistema operativo realiza un cambio de contexto a la aplicación. Luego, la aplicación puede solicitar servicios del sistema operativo que devuelven un contexto al sistema operativo. Cuando la aplicación finaliza, el contexto cambia de nuevo al sistema operativo.
Guy Coder
44
Ver también "syscall".
Raphael
1
Si los comentarios y las respuestas no responden su pregunta a su comprensión o satisfacción, solicite más información como comentario y explique qué está pensando o dónde está perdido, o sobre qué necesita específicamente más detalles.
Guy Coder
2
Creo que la interrupción , el enganche (de una interrupción), el temporizador de hardware (con el enganche de programación y manejo) y la paginación (respuesta parcial a su comentario sobre la memoria prohibida) son las palabras clave principales que necesita. El sistema operativo debe cooperar estrechamente con el procesador para ejecutar su código solo cuando sea necesario. Por lo tanto, la mayor parte de la potencia de la CPU se puede utilizar en el cálculo real, no en su gestión.
Palec

Respuestas:

35

No. El sistema operativo no pierde el tiempo con el código del programa inyectando código nuevo en él. Eso tendría una serie de desventajas.

  1. Llevaría mucho tiempo, ya que el sistema operativo tendría que escanear todo el ejecutable haciendo sus cambios. Normalmente, parte del ejecutable solo se carga según sea necesario. Además, la inserción es costosa ya que tiene que mover un montón de cosas fuera del camino.

  2. Debido a la indecidibilidad del problema de detención, es imposible saber dónde insertar las instrucciones "Volver al SO". Por ejemplo, si el código incluye algo como while (true) {i++;}, definitivamente necesita insertar un gancho dentro de ese bucle, pero la condición en el bucle ( trueaquí) podría ser arbitrariamente complicada, por lo que no puede decidir por cuánto tiempo se repite. Por otro lado, sería muy ineficiente insertar ganchos en cada bucle: por ejemplo, volver al SO durante mucho for (i=0; i<3; i++) {j=j+i;}tiempo ralentizaría mucho el proceso. Y, por la misma razón, no puede detectar bucles cortos para dejarlos solos.

  3. Debido a la indecidibilidad del problema de detención, es imposible saber si las inyecciones de código cambiaron el significado del programa. Por ejemplo, suponga que usa punteros de función en su programa en C. La inyección de un nuevo código movería las ubicaciones de las funciones, por lo que, cuando llama a uno a través del puntero, salta al lugar equivocado. Si el programador estuviera lo suficientemente enfermo como para usar saltos calculados, también fallarían.

  4. Sería un infierno con cualquier sistema antivirus, ya que también cambiaría el código del virus y acumularía todas sus sumas de verificación.

Puede solucionar el problema del problema de detención simulando el código e insertando ganchos en cualquier bucle que se ejecute más de un cierto número fijo de veces. Sin embargo, eso requeriría una simulación extremadamente costosa de todo el programa antes de poder ejecutarlo.

En realidad, si quisieras inyectar código, el compilador sería el lugar natural para hacerlo. De esa manera, solo tendría que hacerlo una vez, pero aún así no funcionaría por la segunda y la tercera razón mencionadas anteriormente. (Y alguien podría escribir un compilador que no funcionara).

Hay tres formas principales en que el sistema operativo recupera el control de los procesos.

  1. En los sistemas cooperativos (o no preventivos), hay una yieldfunción que un proceso puede llamar para devolver el control al sistema operativo. Por supuesto, si ese es su único mecanismo, depende de que los procesos se comporten bien y un proceso que no rinda acaparará la CPU hasta que finalice.

  2. Para evitar ese problema, se utiliza una interrupción del temporizador. Las CPU permiten al sistema operativo registrar devoluciones de llamada para todos los diferentes tipos de interrupciones que implementa la CPU. El sistema operativo utiliza este mecanismo para registrar una devolución de llamada para una interrupción del temporizador que se activa periódicamente, lo que le permite ejecutar su propio código.

  3. Cada vez que un proceso intenta leer un archivo o interactuar con el hardware de cualquier otra forma, le está pidiendo al sistema operativo que trabaje por él. Cuando un proceso le pide al sistema operativo que haga algo, puede decidir poner ese proceso en espera y comenzar a ejecutar uno diferente. Esto puede sonar un poco maquiavélico, pero es lo correcto: la E / S del disco es lenta, por lo que también puede dejar que el proceso B se ejecute mientras el proceso A está esperando que los trozos de metal que giran se muevan al lugar correcto. La E / S de red es aún más lenta. La E / S del teclado es glacial porque las personas no son seres de gigahercios.

David Richerby
fuente
55
¿Puedes desarrollar más en tu último punto? Tengo curiosidad acerca de esta pregunta, y siento que la explicación se omite aquí. Me parece que la pregunta es "cómo el sistema operativo recupera la CPU del proceso" y su respuesta dice "El sistema operativo lo maneja". ¿pero cómo? Tome el bucle infinito en su primer ejemplo: ¿cómo no congela la computadora?
BiAiB
3
Algunos sistemas operativos lo hacen, la mayoría de los sistemas operativos al menos se meten con el código para "vincular", por lo que el programa se puede cargar en cualquier dirección
Ian Ringrose,
1
@BiAiB La palabra clave aquí es "interrumpir". La CPU no es solo algo que procesa un flujo dado de instrucciones, sino que también se puede interrumpir de forma asíncrona desde una fuente separada, lo más importante para nosotros, las E / S y las interrupciones del reloj. Dado que solo el código del espacio del kernel puede manejar las interrupciones, Windows puede estar seguro de poder "robar" el trabajo de cualquier proceso en ejecución en cualquier momento que lo desee. Los manejadores de interrupciones pueden ejecutar el código que quieran, incluyendo "almacenar los registros de la CPU en algún lugar y restaurarlos desde aquí (otro hilo)". Extremadamente simplificado, pero ese es el cambio de contexto.
Luaan
1
Agregando a esta respuesta; El estilo de multitarea mencionado en los puntos 2 y 3 se denomina "multitarea preventiva", el nombre se refiere a la capacidad del sistema operativo para evitar un proceso en ejecución. La multitarea cooperativa se usaba con frecuencia en sistemas operativos más antiguos; en Windows, al menos, la multitarea preventiva no se introdujo hasta Windows 95. He leído que hay al menos un sistema de control industrial en uso hoy en día que todavía usa Windows 3.1 únicamente por su comportamiento multitarea cooperativo en tiempo real.
Jason C
3
@BiAiB En realidad, estás equivocado. Las CPU de escritorio no ejecutan código de forma secuencial y sincrónica desde aproximadamente el i486. Sin embargo, incluso las CPU más antiguas todavía tenían entradas asíncronas: interrupciones. Imagine una solicitud de interrupción de hardware (IRQ) como un pin en la propia CPU: cuando se produce 1, la CPU detiene lo que sea que esté haciendo y comienza a procesar la interrupción (que básicamente significa "preservar el estado y saltar a una dirección en la memoria"). El manejo de la interrupción en sí mismo no es x86o el código que sea, literalmente está cableado. Después de saltar, vuelve a ejecutar (cualquier) x86código. Los hilos son una forma de abstracción más alta.
Luaan
12

Si bien la respuesta de David Richerby es buena, es un poco confuso sobre cómo los sistemas operativos modernos detienen los programas existentes. Mi respuesta debe ser precisa para la arquitectura x86 o x86_64, que es la única que se usa comúnmente para computadoras de escritorio y portátiles. Otras arquitecturas deberían tener métodos similares para lograr esto.

Cuando el sistema operativo se está iniciando, configura una tabla de interrupciones. Cada entrada de la tabla apunta a un bit de código dentro del sistema operativo. Cuando ocurren interrupciones, que es controlada por la CPU, mira esta tabla y llama al código. Hay varias interrupciones, como dividir por cero, código no válido y algunos definidos por el sistema operativo.

Así es como el proceso del usuario habla con el núcleo, por ejemplo, si quiere leer / escribir en el disco o algo más que controle el núcleo del sistema operativo. Un sistema operativo también configurará un temporizador que llama a una interrupción cuando finaliza, por lo que el código en ejecución se cambia por la fuerza del programa de usuario al kernel del sistema operativo, y el kernel puede hacer otras cosas, como poner en cola otros programas para ejecutar.

Desde la memoria, cuando esto sucede, el núcleo del sistema operativo tiene que guardar donde estaba el código, y cuando el núcleo ha terminado de hacer lo que debe hacer, restaura el estado anterior del programa. Por lo tanto, el programa ni siquiera sabe que fue interrumpido.

El proceso no puede cambiar la tabla de interrupciones por dos razones, la primera es que se está ejecutando en un entorno protegido, por lo que si intenta llamar a cierto código de ensamblaje protegido, la CPU activará otra interrupción. La segunda razón es la memoria virtual. La ubicación de la tabla de interrupciones es de 0x0 a 0x3FF en la memoria real, pero con los procesos del usuario esa ubicación generalmente no está asignada, e intentar leer la memoria no asignada desencadenará otra interrupción, por lo que sin la función protegida y la capacidad de escribir en RAM real , el proceso del usuario no puede cambiarlo.

Programmdude
fuente
44
Las interrupciones no están definidas por el sistema operativo, sino por hardware. Y la mayoría de las arquitecturas actuales tienen instrucciones especiales para llamar al sistema operativo. i386 usó una interrupción (generada por software) para esto, pero ya no se hace así en sus sucesores.
vonbrand
2
Sé que las interrupciones están definidas por la CPU, pero el núcleo configura los punteros. Posiblemente lo expliqué mal. También pensé que linux usaba int 9 para hablar con el kernel todavía, pero tal vez hay mejores formas ahora.
Programmdude
Esta es una respuesta bastante engañosa, aunque la noción de que los programadores preventivos son controlados por interrupciones del temporizador es correcta. Primero vale la pena señalar que el temporizador está en hardware. También para aclarar que el proceso "guardar ... restaurar" se llama un cambio de contexto y en su mayoría implica guardar todos los registros de la CPU (que incluye el puntero de instrucciones), entre otras cosas. Además, los procesos pueden cambiar efectivamente las tablas de interrupción, esto se llama "modo protegido", que también define la memoria virtual, y ha existido desde el 286: un puntero a la tabla de interrupción se almacena en un registro grabable.
Jason C
(Incluso la tabla de interrupciones en modo real ha sido reubicable, no bloqueada en la primera página de memoria, desde el 8086.)
Jason C
1
Esta respuesta pierde un detalle crítico. Cuando se dispara una interrupción, la CPU no cambia directamente al kernel. En cambio, primero guarda los registros existentes, luego cambia a otra pila, y solo entonces se llama al núcleo. Llamar al kernel con una pila aleatoria desde un programa aleatorio sería una idea bastante mala. Además, la última parte es engañosa. No obtendrá una interrupción "intentando" leer la memoria no asignada; Es simplemente imposible. Lees de direcciones virtuales y la memoria no asignada simplemente no tiene dirección virtual.
MSalters
5

El núcleo del sistema operativo recupera el control del proceso en ejecución debido al controlador de interrupción del reloj de la CPU, no inyectando código en el proceso.

Debe leer sobre las interrupciones para obtener más aclaraciones sobre cómo funcionan y cómo los manejan los núcleos del SO e implementan diferentes características.

Ankur
fuente
No solo la interrupción del reloj: cualquier interrupción. Y también instrucciones de cambio de modo.
Gilles 'SO- deja de ser malvado'
3

No es un método similar a lo que usted describe: la multitarea cooperativa . El sistema operativo no inserta instrucciones, pero cada programa debe escribirse para llamar a las funciones del sistema operativo que pueden elegir ejecutar otro de los procesos cooperativos. Esto tiene las desventajas que usted describe: el bloqueo de un programa elimina todo el sistema. Windows hasta 3.0 incluido e funcionó así; 3.0 en "modo protegido" y superior no.

La multitarea preventiva (el tipo normal en estos días) se basa en una fuente externa de interrupciones. Las interrupciones anulan el flujo normal de control y generalmente guardan los registros en algún lugar, por lo que la CPU puede hacer otra cosa y luego reanudar el programa de manera transparente. Por supuesto, el sistema operativo puede cambiar el registro "cuando deja las interrupciones reanudar aquí", por lo que se reanuda dentro de un proceso diferente.

(Algunos sistemas hacen las instrucciones de reescritura de una forma limitada de la carga del programa, llamado "thunk", y el procesador Transmeta dinámicamente recompilan a su propio conjunto de instrucciones)

pjc50
fuente
AFAICR 3.1 también fue cooperativo. Win95 fue donde entró la multitarea preventiva. El modo protegido trajo principalmente el aislamiento del espacio de direcciones (lo que mejora la estabilidad, pero en gran medida por razones no relacionadas).
cHao
Thunking no reescribe o inyecta código en la aplicación. El cargador que se modifica está basado en el sistema operativo y no es un producto de la aplicación. Los lenguajes interpretativos que se compilan, como el uso de compiladores JIT, no modifican el código ni inyectan nada en el código. Traducen el código fuente en un ejecutable. Nuevamente, esto no es lo mismo que inyectar código en una aplicación.
Dave Gordon
Transmeta tomó el código ejecutable x86 como su fuente, no un lenguaje interpretativo. Y he pensado en un caso en el que se inyecta código : ejecutarse bajo un depurador. Los sistemas X86 generalmente sobrescriben la instrucción en el punto de interrupción con "INT 03", que atrapa al depurador. Al reanudar, se restablece el código de operación original.
pjc50
La depuración no es cómo alguien ejecuta una aplicación; más allá del desarrollador de la aplicación. Así que no creo que eso realmente ayude al OP.
Dave Gordon
3

La multitarea no requiere nada como la inyección de código. En un sistema operativo como Windows, hay un componente del código del sistema operativo llamado planificador que se basa en una interrupción de hardware activada por un temporizador de hardware. El sistema operativo lo utiliza para cambiar entre los diferentes programas y para sí mismo, haciendo que nuestra percepción humana suceda simultáneamente.

Básicamente, el sistema operativo programa el temporizador de hardware para que se active de vez en cuando ... tal vez 100 veces por segundo. Cuando el temporizador se apaga, genera una interrupción de hardware, una señal que le dice a la CPU que pare lo que está haciendo, guarde su estado en la pila, cambie su modo a algo más privilegiado y ejecute el código que encontrará en un lugar especialmente designado colocar en la memoria. Ese código es parte del planificador, que decide qué se debe hacer a continuación. Podría ser reanudar algún otro proceso, en cuyo caso tendrá que realizar lo que se conoce como un "cambio de contexto": reemplazar la totalidad de su estado actual (incluidas las tablas de memoria virtual) con el del otro proceso. Al volver a un proceso, tiene que restaurar todo el contexto de ese proceso,

El lugar "especialmente designado" en la memoria no tiene que ser conocido por nada más que el sistema operativo. Las implementaciones varían, pero lo esencial es que la CPU responderá a varias interrupciones realizando una búsqueda en la tabla; la ubicación de la tabla está en un lugar específico de la memoria (determinado por el diseño del hardware de la CPU), el contenido de la tabla lo establece el sistema operativo (generalmente en el momento del arranque) y el "tipo" de interrupción determinará qué entrada en la tabla debe usarse como "rutina de interrupción del servicio".

Nada de esto implica "inyección de código" ... se basa en el código contenido en el sistema operativo en cooperación con las características de hardware de la CPU y sus circuitos de soporte.

Zenilogix
fuente
2

Creo que el ejemplo del mundo real más cercano a lo que describe es una de las técnicas utilizadas por VMware , la virtualización completa mediante traducción binaria .

VMware actúa como una capa debajo de uno o más sistemas operativos que se ejecutan simultáneamente en el mismo hardware.

La mayoría de las instrucciones que se ejecutan (por ejemplo, en aplicaciones ordinarias) se pueden virtualizar utilizando el hardware, pero el núcleo del sistema operativo hace uso de instrucciones que no se pueden virtualizar, porque si el código de máquina del sistema operativo conjetura se ejecutara sin modificaciones, se "rompería" "del control del host VMware. Por ejemplo, un SO huésped necesitaría ejecutarse en el anillo de protección más privilegiado y configurar la tabla de interrupciones. Si se le permitiera hacer eso, VMware habría perdido el control del hardware.

VMware reescribe esas instrucciones en el código del sistema operativo antes de ejecutarlo, reemplazándolas con saltos en el código de VMware que simula el efecto deseado.

Entonces, esta técnica es algo análoga a lo que usted describe.

Daniel Earwicker
fuente
2

Hay una variedad de casos en los que un sistema operativo puede "inyectar código" en un programa. Las versiones basadas en 68000 del sistema Apple Macintosh construyen una tabla de todos los puntos de entrada de segmento (ubicados inmediatamente antes de las variables globales estáticas, IIRC). Cuando se inicia un programa, cada entrada en la tabla consiste en una instrucción de captura seguida por el número de segmento y el desplazamiento en el segmento. Si se ejecuta la trampa, el sistema mirará las palabras después de la instrucción de la trampa para ver qué segmento y desplazamiento se requiere, cargará el segmento (si aún no lo está), agregará la dirección de inicio del segmento al desplazamiento y luego reemplace la trampa con un salto a esa dirección recién calculada.

En el software de PC más antiguo, aunque técnicamente esto no fue hecho por el "SO", era común que el código se construyera con instrucciones de captura en lugar de instrucciones matemáticas de coprocesador. Si no se instaló un coprocesador matemático, el controlador de trampa lo emularía. Si se instaló un coprocesador, la primera vez que se toma una trampa, el controlador reemplazará la instrucción de trampa con una instrucción de coprocesador; Las futuras ejecuciones del mismo código utilizarán la instrucción del coprocesador directamente.

Super gato
fuente
El método FP todavía está en uso en procesadores ARM, que a diferencia de las CPU x86 todavía tienen variantes sin FP. Pero es raro ya que la mayoría del uso de ARM está en dispositivos dedicados. En esos entornos, generalmente se sabe si la CPU tendrá capacidades FP.
MSalters
En ninguno de estos casos, el sistema operativo inyectó código en la aplicación. Para que el sistema operativo inyecte código necesitaría una licencia del proveedor de software para "modificar" la aplicación que no obtiene. Los sistemas operativos NO inyectan código.
Dave Gordon
@DaveGordon Se puede decir razonablemente que las instrucciones atrapadas son el código de inyección del sistema operativo en la aplicación.
Gilles 'SO- deja de ser malvado'
@MSalters Las instrucciones atrapadas ocurren comúnmente en máquinas virtuales.
Gilles 'SO- deja de ser malvado'