Modificar binario durante la ejecución

10

A menudo me encuentro con la situación cuando desarrollo, donde estoy ejecutando un archivo binario, por ejemplo, a.outen el fondo, ya que hace un trabajo largo. Mientras lo hace, hago cambios en el código C que produjo a.outy compilé a.outnuevamente. Hasta ahora, no he tenido ningún problema con esto. El proceso que se ejecuta a.outcontinúa de manera normal, nunca se bloquea y siempre ejecuta el código antiguo desde el que se inició originalmente.

Sin embargo, digamos que a.outera un archivo enorme, quizás comparable al tamaño de la RAM. ¿Qué pasaría en este caso? Y digamos que está vinculado a un archivo de objeto compartido libblas.so, ¿qué pasa si modifico libblas.sodurante el tiempo de ejecución? ¿Qué pasaría?

Mi pregunta principal es: ¿garantiza el sistema operativo que cuando ejecuto a.out, el código original siempre se ejecutará normalmente, según el binario original , independientemente del tamaño del binario o los .soarchivos a los que se vincula, incluso cuando esos .oy los .soarchivos se modifican durante tiempo de ejecución?

Sé que hay estas preguntas que abordan problemas similares: /programming/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory -at-once ¿Qué sucede si edita un script durante la ejecución? ¿Cómo es posible hacer una actualización en vivo mientras se ejecuta un programa?

Lo que me ha ayudado a entender un poco más sobre esto, pero no creo que me pregunten exactamente lo que quiero, que es una regla general para las consecuencias de modificar un binario durante la ejecución

texasflood
fuente
Para mí, las preguntas que vinculó (especialmente la de Stack Overflow) ya proporcionan una ayuda significativa para comprender estas consecuencias (o la ausencia de las mismas). Dado que el núcleo carga su programa en regiones / segmentos de texto de memoria , no debería verse afectado por los cambios realizados a través del subsistema de archivos.
John WH Smith
@JohnWHSmith En Stackoverflow, la respuesta principal dice if they are read-only copies of something already on disc (like an executable, or a shared object file), they just get de-allocated and are reloaded from their source, así que tuve la impresión de que si su binario es enorme, si parte de su binario se queda sin RAM, pero luego se necesita nuevamente, se "recarga de la fuente", por lo que cualquier cambio en El .(s)oarchivo se reflejará durante la ejecución. Pero, por supuesto, es posible que haya entendido mal, por eso estoy haciendo esta pregunta más específica
texasflood el
@JohnWHSmith También la segunda respuesta dice: No, it only loads the necessary pages into memory. This is demand paging.De hecho, tenía la impresión de que lo que pedí no puede garantizarse.
texasflood

Respuestas:

11

Si bien la pregunta de Stack Overflow parecía ser suficiente al principio, entiendo, por sus comentarios, por qué aún puede tener dudas al respecto. Para mí, este es exactamente el tipo de situación crítica involucrada cuando los dos subsistemas UNIX (procesos y archivos) se comunican.

Como ya sabrá, los sistemas UNIX generalmente se dividen en dos subsistemas: el subsistema de archivos y el subsistema de procesos. Ahora, a menos que se indique lo contrario a través de una llamada al sistema, el núcleo no debe hacer que estos dos subsistemas interactúen entre sí. Sin embargo, hay una excepción: la carga de un archivo ejecutable en las regiones de texto de un proceso . Por supuesto, uno puede argumentar que esta operación también se activa mediante una llamada al sistema ( execve), pero esto es usualmente conocido por ser el único caso en el que el subsistema de proceso hace una solicitud implícita al subsistema de archivos.

Debido a que el subsistema de proceso, naturalmente, no tiene forma de manejar los archivos (de lo contrario no tendría sentido dividir todo en dos), tiene que usar lo que el subsistema de archivos proporciona para acceder a los archivos. Esto también significa que el subsistema de proceso se somete a cualquier medida que tome el subsistema de archivos con respecto a la edición / eliminación de archivos. En este punto, recomendaría leer la respuesta de Gilles a esta pregunta de U&L . El resto de mi respuesta se basa en esta más general de Gilles.

Lo primero que debe tenerse en cuenta es que internamente, solo se puede acceder a los archivos a través de inodos . Si se le da una ruta al núcleo, su primer paso será traducirlo a un inodo para ser utilizado para todas las demás operaciones. Cuando un proceso carga un ejecutable en la memoria, lo hace a través de su inodo, que ha sido proporcionado por el subsistema de archivos después de la traducción de una ruta. Los inodos pueden estar asociados a varias rutas (enlaces), y los programas solo pueden eliminar enlaces. Para eliminar un archivo y su inodo, userland debe eliminar todos los enlaces existentes a ese inodo y asegurarse de que no se utilice por completo. Cuando se cumplen estas condiciones, el núcleo eliminará automáticamente el archivo del disco.

Si echas un vistazo a la parte de los ejecutables de reemplazo de la respuesta de Gilles, verás que dependiendo de cómo edites / elimines el archivo, el kernel reaccionará / se adaptará de manera diferente, siempre a través de un mecanismo implementado dentro del subsistema de archivos.

  • Si prueba la estrategia uno ( abrir / truncar a cero / escribir o abrir / escribir / truncar a un nuevo tamaño ), verá que el núcleo no se molestará en manejar su solicitud. Obtendrá un error 26: Archivo de texto ocupado ( ETXTBSY). No hay consecuencias de ningún tipo.
  • Si prueba la estrategia dos, el primer paso es eliminar su ejecutable. Sin embargo, dado que está siendo utilizado por un proceso, el subsistema de archivos se activará y evitará que el archivo (y su inodo) se eliminen realmente del disco. Desde este punto, la única forma de acceder al contenido del archivo antiguo es hacerlo a través de su inodo, que es lo que hace el subsistema de proceso cada vez que necesita cargar nuevos datos en secciones de texto (internamente, no tiene sentido usar rutas, excepto al traducirlos en inodes). Aunque hayas desvinculadoel archivo (eliminó todas sus rutas), el proceso aún puede usarlo como si no hubiera hecho nada. Crear un nuevo archivo con la ruta anterior no cambia nada: el nuevo archivo recibirá un inodo completamente nuevo, del cual el proceso en ejecución no tiene conocimiento.

Las estrategias 2 y 3 también son seguras para los ejecutables: aunque ejecutar ejecutables (y bibliotecas cargadas dinámicamente) no son archivos abiertos en el sentido de tener un descriptor de archivo, se comportan de una manera muy similar. Mientras algún programa ejecute el código, el archivo permanece en el disco incluso sin una entrada de directorio.

  • La estrategia tres es bastante similar ya que la mvoperación es atómica. Esto probablemente requerirá el uso de la renamellamada al sistema, y ​​dado que los procesos no se pueden interrumpir mientras está en modo kernel, nada puede interferir con esta operación hasta que se complete (con éxito o no). Nuevamente, no hay alteración del inodo del archivo antiguo: se crea uno nuevo, y los procesos que ya se están ejecutando no tendrán conocimiento de él, incluso si se ha asociado con uno de los enlaces del inodo anterior.

Con la estrategia 3, el paso de mover el nuevo archivo al nombre existente elimina la entrada de directorio que conduce al contenido anterior y crea una entrada de directorio que conduce al nuevo contenido. Esto se hace en una operación atómica, por lo que esta estrategia tiene una gran ventaja: si un proceso abre el archivo en cualquier momento, verá el contenido antiguo o el nuevo contenido; no hay riesgo de obtener contenido mixto o el archivo no existente.

Recompilar un archivo : cuando se usa gcc(y el comportamiento es probablemente similar para muchos otros compiladores), está usando la estrategia 2. Puede ver eso ejecutando uno stracede los procesos de su compilador:

stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
  • El compilador detecta que el archivo ya existe a través de las staty lstatllamadas al sistema.
  • El archivo está desvinculado . Aquí, aunque ya no es accesible a través del nombre a.out, su inodo y contenido permanecen en el disco, siempre y cuando estén siendo utilizados por procesos que ya se están ejecutando.
  • Se crea un nuevo archivo y se hace ejecutable con el nombre a.out. Este es un inodo completamente nuevo, y nuevos contenidos, que los procesos que ya se ejecutan no se preocupan.

Ahora, cuando se trata de bibliotecas compartidas, se aplicará el mismo comportamiento. Mientras un objeto de biblioteca sea utilizado por un proceso, no se eliminará del disco, sin importar cómo cambie sus enlaces. Cuando algo tiene que cargarse en la memoria, el núcleo lo hará a través del inodo del archivo y, por lo tanto, ignorará los cambios que realizó en sus enlaces (como asociarlos con nuevos archivos).

John WH Smith
fuente
Fantástica respuesta detallada. Eso explica mi confusión. Entonces, estoy en lo cierto al suponer que debido a que el inodo todavía está disponible, los datos del archivo binario original todavía están en el disco y, por lo tanto, usar dfla cantidad de bytes libres en el disco es incorrecto, ya que no toma inodes que ¿Se han tenido en cuenta todos los enlaces del sistema de archivos eliminados? ¿Entonces debería usar df -i? (¡Esto es solo una curiosidad técnica, realmente no necesito saber el uso exacto del disco!)
texasflood
1
Solo para aclarar a los futuros lectores: mi confusión fue que pensé en la ejecución, todo el binario se cargaría en la RAM, por lo que si la RAM era pequeña, entonces parte del binario abandonaría la RAM y tendría que volver a cargarse desde el disco, lo que causar problemas si ha cambiado el archivo. Pero la respuesta ha dejado en claro que el binario nunca se elimina realmente del disco, incluso si usted rmo mvél como el inodo del archivo original no se elimina hasta que todos los procesos eliminen su enlace a ese inodo.
texasflood
@texasflood Exactamente. Una vez que se han eliminado todas las rutas, ningún proceso nuevo ( dfincluido) puede obtener información sobre el inodo. Cualquier información nueva que encuentre está relacionada con el nuevo archivo y con el nuevo inodo. El punto principal aquí es que el subsistema de proceso no tiene interés en este problema, por lo que las nociones de administración de memoria (paginación de demanda, intercambio de procesos, fallas de página, ...) son completamente irrelevantes. Este es un problema del subsistema de archivos y el subsistema de archivos lo soluciona. El subsistema de proceso no se molesta con eso, eso no es para lo que está aquí.
John WH Smith
@texasflood Una nota sobre df -i: esta herramienta probablemente recupera información del superbloque de fs, o su caché, lo que significa que puede incluir el inodo del binario antiguo (para el cual se han eliminado todos los enlaces). Sin embargo, esto no significa que los nuevos procesos sean libres de usar esos datos antiguos.
John WH Smith
2

Según tengo entendido, debido a la asignación de memoria de un proceso en ejecución, el núcleo no permitiría actualizar una parte reservada del archivo asignado. Supongo que en caso de que un proceso se esté ejecutando, todo su archivo está reservado, por lo tanto, actualizarlo porque compiló una nueva versión de su fuente en realidad resulta en la creación de un nuevo conjunto de inodes. En resumen, las versiones anteriores de su ejecutable permanecen accesibles en el disco a través de eventos de falla de página. Por lo tanto, incluso si actualiza un archivo enorme, debería permanecer accesible y el núcleo debería ver la versión intacta mientras el proceso se esté ejecutando. Los inodos del archivo original no deben reutilizarse mientras el proceso se esté ejecutando.

Esto por supuesto tiene que ser confirmado.


fuente
2

Este no es siempre el caso cuando se reemplaza un archivo .jar. Los recursos Jar y algunos cargadores de clases de reflexión en tiempo de ejecución no se leen del disco hasta que el programa solicite explícitamente la información.

Esto es solo un problema porque un jar es simplemente un archivo en lugar de un solo ejecutable que se asigna a la memoria. Esto es un poco extraño pero sigue siendo una rama de tu pregunta y algo con lo que me pegué un tiro en el pie.

Entonces para ejecutables: sí. Para archivos jar: quizás (dependiendo de la implementación).

Zhro
fuente