¿Existen casos inteligentes de modificación del código en tiempo de ejecución?


119

¿Puede pensar en algún uso legítimo (inteligente) para la modificación del código en tiempo de ejecución (programa que modifica su propio código en tiempo de ejecución)?

Los sistemas operativos modernos parecen desaprobar los programas que hacen esto, ya que los virus han utilizado esta técnica para evitar la detección.

Todo lo que puedo pensar es algún tipo de optimización en tiempo de ejecución que eliminaría o agregaría código al saber algo en tiempo de ejecución que no se puede conocer en tiempo de compilación.


8
En las arquitecturas modernas, interfiere gravemente con el almacenamiento en caché y la canalización de instrucciones: el código que se modifica automáticamente no modificaría la caché, por lo que necesitaría barreras, y esto probablemente ralentizaría su código. Y no puede modificar el código que ya está en la canalización de instrucciones. Por lo tanto, cualquier optimización basada en código de modificación automática debe realizarse mucho antes de que se ejecute el código para que tenga un impacto de rendimiento superior a, digamos, una verificación de tiempo de ejecución.
Alexandre C.

7
@Alexandre: es común que el código auto modificable haga modificaciones que rara vez varíen (por ejemplo, una vez, dos veces) a pesar de ejecutarse un número arbitrario de veces, por lo que el costo único puede ser insignificante.
Tony Delroy

7
No estoy seguro de por qué esto está etiquetado como C o C ++, ya que ninguno tiene ningún mecanismo para esto.
MSalters

4
@Alexandre: Se sabe que Microsoft Office hace exactamente eso. Como consecuencia (?) Todos los procesadores x86 tienen un excelente soporte para el código auto modificable. En otros procesadores es necesaria una costosa sincronización que hace que todo sea menos atractivo.
Mackie Messer

3
@Cawas: Por lo general, el software de actualización automática descargará nuevos ensamblados y / o ejecutables y sobrescribirá los existentes. Luego reiniciará el software. Esto es lo que hacen Firefox, Adobe, etc. La auto modificación generalmente significa que durante el tiempo de ejecución, la aplicación reescribe el código en la memoria debido a algunos parámetros y no necesariamente persiste en el disco. Por ejemplo, podría optimizar rutas de código completas si puede detectar inteligentemente esas rutas que no se ejercitarían durante esta ejecución en particular para acelerar la ejecución.
NotMe

Respuestas:


117

Hay muchos casos válidos para la modificación de código. La generación de código en tiempo de ejecución puede resultar útil para:

  • Algunas máquinas virtuales utilizan la compilación JIT para mejorar el rendimiento.
  • La generación de funciones especializadas sobre la marcha es una práctica habitual en los gráficos por ordenador. Véase, por ejemplo, Rob Pike y Bart Locanthi y John Reiser Compensaciones de software de hardware para gráficos de mapa de bits en Blit (1984) o esta publicación (2006) de Chris Lattner sobre el uso de LLVM por parte de Apple para la especialización de código en tiempo de ejecución en su pila OpenGL.
  • En algunos casos, el software recurre a una técnica conocida como trampolín que implica la creación dinámica de código en la pila (u otro lugar). Algunos ejemplos son las funciones anidadas de GCC y el mecanismo de señal de algunos Unices.

A veces, el código se traduce en código en tiempo de ejecución (esto se denomina traducción binaria dinámica ):

  • Los emuladores como Rosetta de Apple utilizan esta técnica para acelerar la emulación. Otro ejemplo es el software de transformación de código de Transmeta .
  • Los depuradores y perfiladores sofisticados como Valgrind o Pin lo utilizan para instrumentar su código mientras se ejecuta.
  • Antes de que se hicieran extensiones al conjunto de instrucciones x86, el software de virtualización como VMWare no podía ejecutar directamente código x86 privilegiado dentro de máquinas virtuales. En su lugar, tuvo que traducir las instrucciones problemáticas sobre la marcha en un código personalizado más apropiado.

La modificación de código se puede utilizar para solucionar las limitaciones del conjunto de instrucciones:

  • Hubo un tiempo (hace mucho tiempo, lo sé), cuando las computadoras no tenían instrucciones para regresar de una subrutina o para abordar indirectamente la memoria. El código de modificación automática era la única forma de implementar subrutinas, punteros y matrices .

Más casos de modificación de código:

  • Muchos depuradores reemplazan las instrucciones para implementar puntos de interrupción .
  • Algunos enlazadores dinámicos modifican el código en tiempo de ejecución. Este artículo proporciona algunos antecedentes sobre la reubicación en tiempo de ejecución de las DLL de Windows, que es efectivamente una forma de modificación de código.

10
Esta lista parece mezclar ejemplos de código que se modifica a sí mismo y código que modifica otro código, como enlazadores.
AShelly

6
@AShelly: Bueno, si considera que el vinculador / cargador dinámico es parte del código, entonces sí se modifica. Viven en el mismo espacio de direcciones, así que creo que ese es un punto de vista válido.
Mackie Messer

1
Bien, la lista ahora distingue entre programas y software del sistema. Espero que esto tenga sentido. Al final, cualquier clasificación es discutible. Todo se reduce a lo que incluye exactamente en la definición de programa (o código).
Mackie Messer

35

Esto se ha hecho en gráficos por computadora, específicamente en renderizadores de software con fines de optimización. En tiempo de ejecución, se examina el estado de muchos parámetros y se genera una versión optimizada del código rasterizador (eliminando potencialmente muchos condicionales) que permite renderizar primitivas gráficas, por ejemplo, triángulos mucho más rápido.


5
Una lectura interesante son los artículos de Pixomatic en 3 partes de Michael Abrash sobre DDJ: drdobbs.com/architecture-and-design/184405765 , drdobbs.com/184405807 , drdobbs.com/184405848 . El segundo enlace (Parte 2) habla sobre el soldador de código Pixomatic para la canalización de píxeles.
typo.pl

1
Un artículo muy bonito sobre el tema. De 1984, pero sigue siendo una buena lectura: Rob Pike y Bart Locanthi y John Reiser. Compensaciones de hardware y software para gráficos de mapa de bits en Blit .
Mackie Messer

5
Charles Petzold explica un ejemplo de este tipo en un libro titulado "Beautiful Code": amazon.com/Beautiful-Code-Leading-Programmers-Practice/dp/…
Nawaz

3
Esta respuesta habla sobre la generación de código, pero la pregunta es sobre la modificación del código ...
Timwi

3
@Timwi: modificó el código. En lugar de manejar una gran cadena de if, analizó la forma una vez y reescribió el renderizador para que se configurara para el tipo correcto de forma sin tener que verificar cada vez. Curiosamente, esto ahora es común con el código opencl, ya que se compila sobre la marcha, puede reescribirlo para el caso específico en tiempo de ejecución
Martin Beckett

23

Una razón válida es porque el conjunto de instrucciones ASM carece de algunas instrucciones necesarias, que podría construir usted mismo. Ejemplo: en x86 no hay forma de crear una interrupción a una variable en un registro (por ejemplo, hacer interrupción con el número de interrupción en ax). Solo se permitían números constantes codificados en el código de operación. Con código automodificable se podría emular este comportamiento.


Lo suficientemente justo. ¿Existe algún uso de esta técnica? Parece peligroso.
Alexandre C.

4
@Alexandre C .: Si mal no recuerdo, muchas bibliotecas en tiempo de ejecución (C, Pascal, ...) tenían en DOS tiempos una función para realizar llamadas de interrupción. Como tales funciones obtienen el número de interrupción como parámetro, tenía que proporcionar dicha función (por supuesto, si el número fuera constante, podría haber generado el código correcto, pero eso no estaba garantizado). Y todas las bibliotecas lo implementaron con código automodificable.
flolo

Puede usar una caja de interruptor para hacerlo sin modificar el código. La reducción es que el código de salida será más grande
phuclv

17

Algunos compiladores solían usarlo para la inicialización de variables estáticas, evitando el costo de un condicional para accesos posteriores. En otras palabras, implementan "ejecutar este código solo una vez" sobrescribiendo ese código con no-ops la primera vez que se ejecuta.


1
Muy bueno, especialmente si se trata de evitar bloqueos / desbloqueos mutex.
Tony Delroy

2
De Verdad? ¿Cómo funciona esto para el código basado en ROM o para el código ejecutado en el segmento de código protegido contra escritura?
Ira Baxter

1
@Ira Baxter: cualquier compilador que emita código reubicable sabe que el segmento de código se puede escribir, al menos durante el inicio. Así que la declaración "algunos compiladores lo usaron" todavía es posible.
MSalters

17

Hay muchos casos:

  • Los virus suelen utilizar código de modificación automática para "desofuscar" su código antes de la ejecución, pero esa técnica también puede ser útil para frustrar la ingeniería inversa, el craqueo y la piratería no deseada.
  • En algunos casos, puede haber un punto particular durante el tiempo de ejecución (por ejemplo, inmediatamente después de leer el archivo de configuración) cuando se sabe que, durante el resto de la vida útil del proceso, una rama en particular siempre o nunca se tomará: en lugar de innecesariamente Verificando alguna variable para determinar de qué manera bifurcar, la instrucción de bifurcación en sí podría modificarse en consecuencia
    • Por ejemplo, puede llegar a saberse que solo se manejará uno de los posibles tipos derivados, de modo que el despacho virtual se pueda reemplazar con una llamada específica
    • Habiendo detectado qué hardware está disponible, el uso de un código coincidente puede estar codificado
  • El código innecesario se puede reemplazar con instrucciones de no operación o un salto sobre él, o hacer que el siguiente bit de código se cambie directamente a su lugar (más fácil si se utilizan códigos de operación independientes de la posición)
  • El código escrito para facilitar su propia depuración podría inyectar una instrucción trampa / señal / interrupción esperada por el depurador en una ubicación estratégica.
  • Algunas expresiones de predicado basadas en la entrada del usuario pueden ser compiladas en código nativo por una biblioteca
  • Incluyendo algunas operaciones simples que no son visibles hasta el tiempo de ejecución (por ejemplo, desde una biblioteca cargada dinámicamente) ...
  • Agregar condicionalmente pasos de autoinstrumentación / creación de perfiles
  • Los cracks pueden implementarse como bibliotecas que modifican el código que los carga (no se "auto" modifican exactamente, pero necesitan las mismas técnicas y permisos).
  • ...

Los modelos de seguridad de algunos sistemas operativos significan que el código que se modifica automáticamente no se puede ejecutar sin privilegios de administrador o root, lo que lo hace poco práctico para uso general.

De Wikipedia:

El software de aplicación que se ejecuta en un sistema operativo con estricta seguridad W ^ X no puede ejecutar instrucciones en las páginas en las que está permitido escribir; solo el sistema operativo tiene permiso para escribir instrucciones en la memoria y luego ejecutarlas.

En tales sistemas operativos, incluso programas como Java VM necesitan privilegios de administrador / root para ejecutar su código JIT. (Consulte http://en.wikipedia.org/wiki/W%5EX para obtener más detalles)


2
No necesitas privilegios de root para modificar el código automáticamente. Tampoco lo hace la máquina virtual Java.
Mackie Messer

No sabía que algunos sistemas operativos fueran tan estrictos. Pero ciertamente tiene sentido en algunas aplicaciones. Sin embargo, me pregunto si ejecutar Java con privilegios de root realmente aumenta la seguridad ...
Mackie Messer

@Mackie: Creo que debe disminuirlo, pero tal vez pueda establecer algunos permisos de memoria y luego cambiar el uid efectivo a alguna cuenta de usuario ...
Tony Delroy

Sí, esperaría que tuvieran un mecanismo detallado para otorgar permisos para acompañar el modelo de seguridad estricto.
Mackie Messer

15

El sistema operativo Synthesis básicamente evaluó parcialmente su programa con respecto a las llamadas a la API y reemplazó el código del sistema operativo con los resultados. El principal beneficio es que desaparecieron muchas comprobaciones de errores (porque si su programa no va a pedirle al sistema operativo que haga algo estúpido, no necesita comprobarlo).

Sí, ese es un ejemplo de optimización del tiempo de ejecución.


No veo el punto. Si dice que el sistema operativo va a prohibir una llamada al sistema, es probable que reciba un error que le indicará que tendrá que verificar el código, ¿no es así? Me parece que modificar el ejecutable en lugar de devolver un código de error es una especie de sobreingeniería.
Alexandre C.

@Alexandre C.: es posible que pueda eliminar las comprobaciones de puntero nulo de esa manera. A menudo es trivialmente obvio para la persona que llama que un argumento es válido.
MSalters

@Alexandre: Puedes leer la investigación en el enlace. Creo que obtuvieron aceleraciones bastante impresionantes, y ese sería el punto: -}
Ira Baxter

2
Para llamadas al sistema relativamente triviales y no vinculadas a E / S, los ahorros son significativos. Por ejemplo, si está escribiendo un demonio para Unix, hay un montón de llamadas al sistema de placa de caldera que hace para desconectar stdio, configurar varios manejadores de señales, etc. Si sabe que los parámetros de una llamada son constantes y que el los resultados siempre serán los mismos (cerrando stdin, por ejemplo), gran parte del código que ejecuta en el caso general es innecesario.
Mark Bessey

1
Si lee la tesis, el capítulo 8 contiene algunas cifras realmente impresionantes sobre E / S en tiempo real no triviales para la adquisición de datos. ¿Recuerda que se trata de una tesis de mediados de la década de 1980 y que la máquina en la que estaba ejecutando era 10? Mhz 68000, pudo en el software capturar datos de audio con calidad de CD (44.000 muestras por segundo) con un software antiguo. Afirmó que las estaciones de trabajo Sun (Unix clásico) solo podían alcanzar alrededor de 1/5 de esa tasa. Soy un antiguo codificador de lenguaje ensamblador de esos días, y esto es bastante espectacular.
Ira Baxter

9

Hace muchos años, pasé una mañana tratando de depurar algún código auto-modificable, una instrucción cambió la dirección de destino de la siguiente instrucción, es decir, estaba calculando una dirección de sucursal. Estaba escrito en lenguaje ensamblador y funcionó perfectamente cuando revisé el programa una instrucción a la vez. Pero cuando ejecuté el programa falló. Finalmente, me di cuenta de que la máquina estaba obteniendo 2 instrucciones de la memoria y (como las instrucciones estaban colocadas en la memoria) la instrucción que estaba modificando ya había sido recuperada y, por lo tanto, la máquina estaba ejecutando la versión no modificada (incorrecta) de la instrucción. Por supuesto, cuando estaba depurando, solo estaba haciendo una instrucción a la vez.

Mi punto, el código auto-modificable puede ser extremadamente desagradable de probar / depurar y, a menudo, tiene suposiciones ocultas sobre el comportamiento de la máquina (ya sea hardware o virtual). Además, el sistema nunca podría compartir páginas de códigos entre los diversos subprocesos / procesos que se ejecutan en las (ahora) máquinas de múltiples núcleos. Esto anula muchos de los beneficios de la memoria virtual, etc. También invalidaría las optimizaciones de rama realizadas a nivel de hardware.

(Nota: no incluyo JIT en la categoría de código que se modifica automáticamente. JIT está traduciendo de una representación del código a una representación alternativa, no está modificando el código)

En general, es solo una mala idea, realmente ordenada, realmente oscura, pero realmente mala.

por supuesto, si todo lo que tiene son 8080 y ~ 512 bytes de memoria, es posible que tenga que recurrir a tales prácticas.


1
No sé, lo bueno y lo malo no parecen ser las categorías adecuadas para pensar en esto. Por supuesto, debe saber realmente lo que está haciendo y también por qué lo está haciendo. Pero el programador que escribió ese código probablemente no quería que vieras lo que estaba haciendo el programa. Por supuesto, es desagradable tener que depurar un código como ese. Pero ese código probablemente estaba destinado a ser así.
Mackie Messer

Las CPU x86 modernas tienen una detección de SMC más fuerte que la requerida en papel: observando la obtención de instrucciones obsoletas en x86 con código de modificación automática . Y en la mayoría de las CPU que no son x86 (como ARM), la caché de instrucciones no es coherente con las cachés de datos, por lo que se requiere una limpieza / sincronización manual antes de que los bytes recién almacenados se puedan ejecutar de manera confiable como instrucciones. community.arm.com/processors/b/blog/posts/… . De cualquier manera, el rendimiento de SMC es terrible en las CPU modernas, a menos que lo modifique una vez y lo ejecute muchas veces.
Peter Cordes

7

Desde el punto de vista del núcleo de un sistema operativo, cada Just In Time Compiler y Linker Runtime realiza la auto modificación del texto del programa. Un ejemplo destacado sería el intérprete de secuencias de comandos ECMA V8 de Google.


5

Otra razón por la que el código se modifica automáticamente (en realidad, un código "autogenerado") es implementar un mecanismo de compilación Just-In-time para el rendimiento. Por ejemplo, un programa que lee una expresión algebric y la calcula en un rango de parámetros de entrada puede convertir la expresión en código de máquina antes de establecer el cálculo.


5

Conoces la vieja casta de que no hay una diferencia lógica entre hardware y software ... también se puede decir que no hay una diferencia lógica entre código y datos.

¿Qué es el código auto modificable? Código que coloca valores en el flujo de ejecución para que se pueda interpretar no como datos sino como un comando. Seguro que existe el punto de vista teórico en los lenguajes funcionales de que realmente no hay diferencia. Estoy diciendo que podemos hacer esto de una manera sencilla en lenguajes imperativos y compiladores / intérpretes sin la presunción de igualdad de estatus.

A lo que me refiero es en el sentido práctico de que los datos pueden alterar las rutas de ejecución del programa (en cierto sentido, esto es extremadamente obvio). Estoy pensando en algo así como un compilador-compilador que crea una tabla (una matriz de datos) que uno atraviesa al analizar, moviéndose de un estado a otro (y también modificando otras variables), al igual que cómo se mueve un programa de un comando a otro. , modificando variables en el proceso.

Entonces, incluso en el caso habitual en el que un compilador crea un espacio de código y se refiere a un espacio de datos completamente separado (el montón), aún se pueden modificar los datos para cambiar explícitamente la ruta de ejecución.


4
No hay diferencia lógica, cierto. Sin embargo, no he visto demasiados circuitos integrados auto-modificables.
Ira Baxter

@Mitch, en mi opinión, cambiar la ruta de ejecución no tiene nada que ver con la (auto) modificación del código. Además, confundes datos con información. No puedo responder a mi comentario en LSE porque estoy prohibido allí, desde febrero, durante 3 años (1,000 días) por expresar en meta-LSE mi punto de vista de que los estadounidenses y los británicos no poseen inglés.
Gennady Vanin Геннадий Ванин

4

Implementé un programa usando la evolución para crear el mejor algoritmo. Usó código de modificación automática para modificar el plano de ADN.


2

Un caso de uso es el archivo de prueba EICAR, que es un archivo COM ejecutable legítimo de DOS para probar programas antivirus.

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

Tiene que utilizar la modificación del código propio porque el archivo ejecutable debe contener solo caracteres ASCII imprimibles / mecanografiables en el rango [21h-60h, 7Bh-7Dh], lo que limita significativamente el número de instrucciones codificables

Los detalles se explican aquí.


También se usa para el despacho de operaciones de punto flotante en DOS

Algunos compiladores emitirán CD xxcon xx que van desde 0x34-0x3B en lugar de instrucciones de coma flotante x87. Dado que CDes el código de operación para la intinstrucción, saltará a la interrupción 34h-3Bh y emulará esa instrucción en el software si el coprocesador x87 no está disponible. De lo contrario, el manejador de interrupciones reemplazará esos 2 bytes con 9B Dxpara que las ejecuciones posteriores sean manejadas directamente por x87 sin emulación.

¿Cuál es el protocolo para la emulación de punto flotante x87 en MS-DOS?


1

El kernel de Linux tiene módulos de kernel cargables que hacen precisamente eso.

Emacs también tiene esta habilidad y la uso todo el tiempo.

Todo lo que admita una arquitectura de complemento dinámico es esencialmente modificar su código en tiempo de ejecución.


4
apenas. tener una biblioteca cargable dinámicamente que no siempre es residente tiene muy poco que ver con el código auto-modificable.
Dov

1

Realizo análisis estadísticos contra una base de datos continuamente actualizada. Mi modelo estadístico se escribe y se reescribe cada vez que se ejecuta el código para adaptarse a los nuevos datos que están disponibles.


0

El escenario en el que se puede utilizar es un programa de aprendizaje. En respuesta a la entrada del usuario, el programa aprende un nuevo algoritmo:

  1. busca el código base existente para un algoritmo similar
  2. si no hay un algoritmo similar en la base del código, el programa simplemente agrega un nuevo algoritmo
  3. Si existe un algoritmo similar, el programa (quizás con ayuda del usuario) modifica el algoritmo existente para poder servir tanto al propósito anterior como al nuevo.

Hay una pregunta sobre cómo hacer eso en Java: ¿Cuáles son las posibilidades de auto-modificación del código Java?


-1

La mejor versión de esto puede ser Lisp Macros. A diferencia de las macros de C, que son solo un preprocesador, Lisp le permite tener acceso a todo el lenguaje de programación en todo momento. Esta es la característica más poderosa de lisp y no existe en ningún otro idioma.

¡De ninguna manera soy un experto, pero haz que uno de los chicos ceceo hable de ello! Hay una razón por la que dicen que Lisp es el lenguaje más poderoso que existe y la gente inteligente no es que probablemente tengan razón.


2
¿Eso realmente crea código auto modificable o es solo un preprocesador más poderoso (uno que generará funciones)?
Brendan Long

@Brendan: de hecho, pero es la forma correcta de realizar el preprocesamiento. Aquí no hay modificación del código de tiempo de ejecución.
Alexandre C.
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.