En informática , el código automodificable ( SMC o SMoC ) es un código que altera sus propias instrucciones mientras se ejecuta , generalmente para reducir la longitud de la ruta de instrucción y mejorar el rendimiento o simplemente para reducir código que de otro modo sería repetitivamente similar , simplificando así el mantenimiento . El término generalmente solo se aplica al código donde la automodificación es intencional, no en situaciones donde el código se modifica accidentalmente debido a un error como un desbordamiento del búfer .
El código de modificación automática puede implicar sobrescribir instrucciones existentes o generar código nuevo en tiempo de ejecución y transferir el control a ese código.
La automodificación se puede utilizar como alternativa al método de "establecimiento de banderas" y bifurcación condicional del programa, que se utiliza principalmente para reducir la cantidad de veces que es necesario probar una condición.
El método se utiliza con frecuencia para invocar condicionalmente código de prueba/depuración sin requerir una sobrecarga computacional adicional para cada ciclo de entrada/salida .
Las modificaciones podrán realizarse:
En cualquier caso, las modificaciones se pueden realizar directamente en las instrucciones del código de máquina , superponiendo nuevas instrucciones sobre las existentes (por ejemplo: alterar una comparación y una rama a una rama incondicional o, alternativamente, un ' NOP ').
En la arquitectura IBM System/360 y sus sucesores hasta z/Architecture , una instrucción EXECUTE (EX) superpone lógicamente el segundo byte de su instrucción de destino con los 8 bits de orden inferior del registro 1. Esto proporciona el efecto de auto- modificación aunque no se alteren las instrucciones reales de almacenamiento.
La automodificación se puede lograr de varias maneras dependiendo del lenguaje de programación y su soporte para punteros y/o acceso a 'motores' de compilador o intérprete dinámico:
El código automodificable es bastante sencillo de implementar cuando se utiliza lenguaje ensamblador . Las instrucciones se pueden crear dinámicamente en la memoria (o superponerse al código existente en un almacenamiento de programa no protegido), [1] en una secuencia equivalente a las que un compilador estándar puede generar como código objeto . Con los procesadores modernos, pueden producirse efectos secundarios no deseados en la memoria caché de la CPU que deben tenerse en cuenta. El método se utilizó con frecuencia para probar condiciones de "primera vez", como en este ejemplo del ensamblador IBM/360 debidamente comentado . Utiliza la superposición de instrucciones para reducir la longitud de la ruta de instrucción en (N×1) −1 donde N es el número de registros en el archivo (siendo −1 la sobrecarga para realizar la superposición).
SUBRTN NOP ¿ABRIÓ POR PRIMERA VEZ AQUÍ?* El NOP es x'4700'<Dirección_de_abierto> OI SUBRTN+1,X'F0' SÍ, CAMBIAR NOP A RAMA INCONDICIONAL (47F0...) ABRA LA ENTRADA Y ABRA EL ARCHIVO DE ENTRADA YA QUE ES LA PRIMERA VEZABIERTO OBTÉN ENTRADA RESUMEN DEL PROCESAMIENTO NORMAL AQUÍ ...
El código alternativo podría implicar probar una "bandera" cada vez. La rama incondicional es ligeramente más rápida que una instrucción de comparación, además de reducir la longitud total de la ruta. En sistemas operativos posteriores, para programas que residen en un almacenamiento protegido, esta técnica no se podía utilizar, por lo que se utilizaría en su lugar cambiar el puntero a la subrutina . El puntero residiría en un almacenamiento dinámico y podría modificarse a voluntad después del primer paso para evitar OPEN (tener que cargar un puntero primero en lugar de una rama directa y un enlace a la subrutina agregaría N instrucciones a la longitud de la ruta, pero no sería una reducción correspondiente de N para la rama incondicional que ya no sería necesaria).
A continuación se muestra un ejemplo en lenguaje ensamblador Zilog Z80 . El código incrementa el registro "B" en el rango [0,5]. La instrucción de comparación "CP" se modifica en cada bucle.
;========== ORG 0H CALL FUNC00 HALT ;========== FUNC00: LD A , 6 LD HL , label01 + 1 LD B ,( HL ) label00: INC B LD ( HL ), B etiqueta01: CP $ 0 JP NZ , etiqueta00 RET ;==========
A veces se utiliza código automodificable para superar las limitaciones del conjunto de instrucciones de una máquina. Por ejemplo, en el conjunto de instrucciones Intel 8080 , no se puede ingresar un byte desde un puerto de entrada especificado en un registro. El puerto de entrada está codificado estáticamente en la propia instrucción, como el segundo byte de una instrucción de dos bytes. Usando código automodificable, es posible almacenar el contenido de un registro en el segundo byte de la instrucción y luego ejecutar la instrucción modificada para lograr el efecto deseado.
Algunos lenguajes compilados permiten explícitamente código automodificable. Por ejemplo, el verbo ALTER en COBOL se puede implementar como una instrucción de bifurcación que se modifica durante la ejecución. [2] Algunas técnicas de programación por lotes implican el uso de código que se modifica automáticamente. Clipper y SPITBOL también ofrecen posibilidades para la automodificación explícita. El compilador Algol en los sistemas B6700 ofrecía una interfaz para el sistema operativo mediante la cual el código en ejecución podía pasar una cadena de texto o un archivo de disco con nombre al compilador Algol y luego podía invocar la nueva versión de un procedimiento.
Con los lenguajes interpretados, el "código de máquina" es el texto fuente y puede ser susceptible de edición sobre la marcha: en SNOBOL las declaraciones fuente que se ejecutan son elementos de una matriz de texto. Otros lenguajes, como Perl y Python , permiten que los programas creen código nuevo en tiempo de ejecución y lo ejecuten usando una función de evaluación , pero no permiten que se modifique el código existente. La ilusión de modificación (aunque en realidad no se sobrescribe ningún código de máquina) se logra modificando los punteros de función, como en este ejemplo de JavaScript:
var f = función ( x ) { retorno x + 1 }; // asigna una nueva definición a f: f = nueva función ( 'x' , 'return x + 2' );
Las macros Lisp también permiten la generación de código en tiempo de ejecución sin analizar una cadena que contiene código de programa.
El lenguaje de programación Push es un sistema de programación genética que está diseñado explícitamente para crear programas automodificables. Si bien no es un lenguaje de alto nivel, no es de tan bajo nivel como el lenguaje ensamblador. [3]
Antes de la llegada de múltiples ventanas, los sistemas de línea de comandos podían ofrecer un sistema de menú que implicaba la modificación de un script de comando en ejecución. Supongamos que un archivo de script DOS (o "por lotes") MENU.BAT contiene lo siguiente: [4] [nb 1]
:comenzar MOSTRARMENU.EXE
Al iniciar MENU.BAT desde la línea de comando, SHOWMENU presenta un menú en pantalla, con posible información de ayuda, ejemplos de usos, etc. Finalmente, el usuario hace una selección que requiere que se ejecute el comando SOMENAME : SHOWMENU sale después de reescribir el archivo MENU.BAT que contiene
:comenzar MOSTRARMENU.EXE LLAMAR A ALGUIEN NOMBRE .BAT IR A inicio
Debido a que el intérprete de comandos de DOS no compila un archivo de script y luego lo ejecuta, ni lee el archivo completo en la memoria antes de iniciar la ejecución, ni depende del contenido de un búfer de registro, cuando SHOWMENU sale, el intérprete de comandos encuentra un nuevo archivo de script. comando a ejecutar (es para invocar el archivo de script SOMENAME , en una ubicación de directorio y a través de un protocolo conocido por SHOWMENU), y una vez que se completa ese comando, regresa al inicio del archivo de script y reactiva SHOWMENU listo para la siguiente selección. . Si la opción del menú fuera salir, el archivo se reescribiría a su estado original. Aunque este estado inicial no tiene uso para la etiqueta, se requiere esta o una cantidad equivalente de texto, porque el intérprete de comandos de DOS recuerda la posición del byte del siguiente comando cuando debe iniciar el siguiente comando, por lo que el archivo reescrito debe mantener la alineación para que el siguiente punto de inicio del comando sea realmente el inicio del siguiente comando.
Aparte de la conveniencia de un sistema de menú (y posibles funciones auxiliares), este esquema significa que el sistema SHOWMENU.EXE no está en la memoria cuando se activa el comando seleccionado, una ventaja significativa cuando la memoria es limitada. [4] [5]
Se puede considerar que los intérpretes de tablas de control son, en cierto sentido, 'automodificados' por valores de datos extraídos de las entradas de la tabla (en lugar de codificados específicamente a mano en declaraciones condicionales del formulario "IF inputx = 'yyy'").
Algunos métodos de acceso de IBM utilizaban tradicionalmente programas de canal automodificables , donde un valor, como una dirección de disco, se lee en un área a la que hace referencia un programa de canal, donde un comando de canal posterior lo utiliza para acceder al disco.
El IBM SSEC , demostrado en enero de 1948, tenía la capacidad de modificar sus instrucciones o tratarlas exactamente como datos. Sin embargo, esta capacidad rara vez se utilizó en la práctica. [6] En los primeros días de las computadoras, el código automodificable se usaba a menudo para reducir el uso de memoria limitada, mejorar el rendimiento, o ambas cosas. A veces también se usaba para implementar llamadas y retornos de subrutinas cuando el conjunto de instrucciones solo proporcionaba instrucciones simples de bifurcación o omisión para variar el flujo de control . [7] [8] Este uso sigue siendo relevante en ciertas arquitecturas ultra- RISC , al menos teóricamente; consulte, por ejemplo , computadora con un conjunto de instrucciones . La arquitectura MIX de Donald Knuth también utilizó código automodificable para implementar llamadas a subrutinas. [9]
El código automodificable se puede utilizar para varios propósitos:
repetir N veces { si el ESTADO es 1 aumentar A en uno demás disminuir A en uno hacer algo con A}
El código automodificado, en este caso, sería simplemente cuestión de reescribir el bucle de esta manera:
repetir N veces { aumentar A en uno hacer algo con A cuando el ESTADO tiene que cambiar { reemplace el código de operación "aumentar" anterior con el código de operación para disminuir, o viceversa }}
Tenga en cuenta que el reemplazo de dos estados del código de operación se puede escribir fácilmente como 'xor var en la dirección con el valor "opcodeOf(Inc) xor opcodeOf(dec)"'.
La elección de esta solución debe depender del valor de N y de la frecuencia del cambio de estado.
Supongamos que se va a calcular un conjunto de estadísticas como promedio, extremos, ubicación de los extremos, desviación estándar, etc. para un conjunto de datos grande. En una situación general, puede haber una opción de asociar ponderaciones con los datos, de modo que cada x i se asocie con awi y , en lugar de probar la presencia de ponderaciones en cada valor del índice, podría haber dos versiones del cálculo, una para uso con pesas y uno no, con una prueba al inicio. Ahora considere una opción adicional, que cada valor pueda tener asociado un booleano para indicar si ese valor se debe omitir o no. Esto podría manejarse produciendo cuatro lotes de código, uno para cada resultado de permutación y exceso de código. Alternativamente, las matrices de peso y omisión podrían fusionarse en una matriz temporal (con pesos cero para los valores que se omitirán), a costa del procesamiento y aún así habrá hinchazón. Sin embargo, con la modificación del código, a la plantilla para calcular las estadísticas se podría agregar, según corresponda, el código para omitir valores no deseados y para aplicar ponderaciones. No habría pruebas repetidas de las opciones y se accedería a la matriz de datos una vez, al igual que a las matrices de peso y omisión, si estuvieran involucradas.
El código automodificable es más complejo de analizar que el código estándar y, por lo tanto, puede usarse como protección contra la ingeniería inversa y el craqueo de software . El código automodificable se utilizó para ocultar instrucciones de protección contra copia en programas basados en disco de la década de 1980 para plataformas como IBM PC y Apple II . Por ejemplo, en una PC IBM (o compatible ), la instrucción de acceso a la unidad de disquete noint 0x13
aparecería en la imagen del programa ejecutable, pero se escribiría en la imagen de la memoria del ejecutable después de que el programa comenzara a ejecutarse.
El código de modificación automática también lo utilizan a veces programas que no quieren revelar su presencia, como los virus informáticos y algunos códigos shell . Los virus y shellcodes que utilizan código que se modifica automáticamente lo hacen principalmente en combinación con código polimórfico . La modificación de un fragmento de código en ejecución también se utiliza en ciertos ataques, como los desbordamientos del búfer .
Los sistemas tradicionales de aprendizaje automático cuentan con un algoritmo de aprendizaje fijo y preprogramado para ajustar sus parámetros . Sin embargo, desde la década de 1980, Jürgen Schmidhuber ha publicado varios sistemas automodificables con la capacidad de cambiar su propio algoritmo de aprendizaje. Evitan el peligro de autorreescrituras catastróficas al asegurarse de que las automodificaciones sobrevivan solo si son útiles de acuerdo con una función de aptitud , error o recompensa determinada por el usuario . [14]
En particular, el kernel de Linux hace un amplio uso de código que se modifica automáticamente; lo hace para poder distribuir una única imagen binaria para cada arquitectura principal (por ejemplo, IA-32 , x86-64 , ARM de 32 bits , ARM64 ...) mientras adapta el código del kernel en la memoria durante el arranque dependiendo de la CPU específica. modelo detectado, por ejemplo, para poder aprovechar nuevas instrucciones de la CPU o solucionar errores de hardware. [15] [16] En menor medida, el kernel DR-DOS también optimiza las secciones críticas de velocidad de sí mismo en el momento de la carga dependiendo de la generación del procesador subyacente. [10] [11] [nota 2]
Independientemente, en un metanivel , los programas aún pueden modificar su propio comportamiento cambiando los datos almacenados en otro lugar (ver metaprogramación ) o mediante el uso de polimorfismo .
El núcleo de síntesis presentado en el doctorado de Alexia Massalin. La tesis [17] [18] es un pequeño núcleo Unix que adopta un enfoque estructurado , o incluso orientado a objetos , para el código automodificable, donde el código se crea para quajects individuales , como identificadores de archivos. La generación de código para tareas específicas permite que el kernel de Synthesis (como lo haría un intérprete JIT) aplique una serie de optimizaciones , como el plegado constante o la eliminación de subexpresiones comunes .
El núcleo de Synthesis era muy rápido, pero estaba escrito íntegramente en ensamblador. La resultante falta de portabilidad ha impedido que las ideas de optimización de Massalin sean adoptadas por cualquier núcleo de producción. Sin embargo, la estructura de las técnicas sugiere que podrían ser capturadas por un lenguaje de nivel superior , aunque más complejo que los lenguajes de nivel medio existentes. Un lenguaje y un compilador de este tipo podrían permitir el desarrollo de aplicaciones y sistemas operativos más rápidos.
Paul Haeberli y Bruce Karsh se han opuesto a la "marginación" del código que se modifica automáticamente y a la optimización en general, a favor de la reducción de los costos de desarrollo. [19]
En arquitecturas sin caché de instrucciones y datos acoplados (por ejemplo, algunos núcleos SPARC , ARM y MIPS ), la sincronización de la caché debe realizarse explícitamente mediante el código de modificación (vaciar la caché de datos e invalidar la caché de instrucciones para el área de memoria modificada).
En algunos casos, secciones cortas de código que se modifica automáticamente se ejecutan más lentamente en los procesadores modernos. Esto se debe a que un procesador moderno normalmente intentará mantener bloques de código en su memoria caché. Cada vez que el programa reescribe una parte de sí mismo, la parte reescrita debe cargarse nuevamente en la caché, lo que resulta en un ligero retraso, si el codelet modificado comparte la misma línea de caché con el código modificador, como es el caso cuando la memoria modificada La dirección se encuentra a unos pocos bytes de la del código de modificación.
El problema de invalidación de caché en los procesadores modernos generalmente significa que el código automodificado sería más rápido solo cuando la modificación ocurrirá raramente, como en el caso de un cambio de estado dentro de un bucle interno. [ cita necesaria ]
La mayoría de los procesadores modernos cargan el código de máquina antes de ejecutarlo, lo que significa que si se modifica una instrucción que está demasiado cerca del puntero de instrucción , el procesador no lo notará, sino que ejecutará el código como estaba antes de ser modificado. Consulte cola de entrada de captación previa (PIQ). Los procesadores de PC deben manejar correctamente el código que se modifica automáticamente por razones de compatibilidad con versiones anteriores, pero están lejos de ser eficientes en hacerlo. [ cita necesaria ]
Debido a las implicaciones de seguridad que tiene el código automodificado, todos los principales sistemas operativos tienen cuidado de eliminar dichas vulnerabilidades a medida que se conocen. Por lo general, la preocupación no es que los programas se modifiquen a sí mismos intencionalmente, sino que puedan ser modificados maliciosamente mediante un exploit .
Un mecanismo para evitar la modificación de código malicioso es una característica del sistema operativo llamada W^X (para "escribir x o ejecutar"). Este mecanismo prohíbe que un programa haga que cualquier página de la memoria sea escribible y ejecutable. Algunos sistemas impiden que una página grabable se cambie para que sea ejecutable, incluso si se elimina el permiso de escritura. [ cita necesaria ] Otros sistemas proporcionan una especie de " puerta trasera ", que permite que múltiples asignaciones de una página de memoria tengan diferentes permisos. Una forma relativamente portátil de omitir W^X es crear un archivo con todos los permisos y luego asignar el archivo a la memoria dos veces. En Linux, se puede utilizar un indicador de memoria compartida SysV no documentado para obtener memoria compartida ejecutable sin necesidad de crear un archivo. [ cita necesaria ]
El código automodificable es más difícil de leer y mantener porque las instrucciones en la lista del programa fuente no son necesariamente las instrucciones que se ejecutarán. La automodificación que consiste en la sustitución de punteros de función podría no ser tan críptica, si está claro que los nombres de las funciones que se llamarán son marcadores de posición para funciones que se identificarán más adelante.
El código que se modifica automáticamente se puede reescribir como código que prueba una bandera y se bifurca a secuencias alternativas según el resultado de la prueba, pero el código que se modifica automáticamente generalmente se ejecuta más rápido.
El código de modificación automática entra en conflicto con la autenticación del código y puede requerir excepciones a las políticas que exigen que todo el código que se ejecuta en un sistema esté firmado.
El código modificado debe almacenarse por separado de su forma original, lo que entra en conflicto con las soluciones de administración de memoria que normalmente descartan el código en la RAM y lo recargan desde el archivo ejecutable según sea necesario.
En los procesadores modernos con una canalización de instrucciones , el código que se modifica con frecuencia puede ejecutarse más lentamente si modifica instrucciones que el procesador ya ha leído de la memoria en la canalización. En algunos de estos procesadores, la única forma de garantizar que las instrucciones modificadas se ejecuten correctamente es vaciar la canalización y volver a leer muchas instrucciones.
El código automodificable no se puede utilizar en absoluto en algunos entornos, como los siguientes:
REP MOVSW
instrucciones de 16 bits ("copiar palabras") en la imagen de tiempo de ejecución del kernel por Instrucciones de 32 bits REP MOVSD
("copiar palabras dobles") al copiar datos de una ubicación de memoria a otra (y la mitad del número de repeticiones necesarias) para acelerar las transferencias de datos en disco. Se solucionan los casos extremos , como los recuentos impares. [10] [11]El SSEC fue la primera computadora operativa capaz de tratar sus propias instrucciones almacenadas exactamente como datos, modificarlas y actuar sobre el resultado.
[…] Originalmente,
la reescritura binaria
estaba motivada por la necesidad de cambiar partes de un programa durante la ejecución (por ejemplo, parches en tiempo de ejecución en el
PDP-1
en la década de 1960) […](36 páginas)
[…] Además de buscar una instrucción, el
Z80
utiliza la mitad del ciclo para
actualizar
la
RAM dinámica
.
[…] dado que el Z80 debe dedicar la mitad de cada ciclo
de búsqueda de instrucciones
a realizar otras tareas, no tiene tanto tiempo para buscar un
byte de instrucción
como un byte de datos.
Si uno de los
chips RAM
en la ubicación de memoria a la que se accede es un poco lento, el Z80 puede obtener el patrón de bits incorrecto cuando recupera una instrucción, pero obtiene el correcto cuando lee datos.
[…] la prueba de memoria incorporada no detectará este tipo de problema […] es estrictamente una prueba de lectura/escritura de datos.
Durante la prueba, todas las instrucciones obtenidas son de la
ROM
, no de la RAM […] lo que hace que el
H89
pase la prueba de memoria pero aún funcione de manera errática en algunos programas.
[…] Este es un programa que prueba la memoria reubicándose a través de la RAM.
Al hacerlo, la CPU imprime la dirección actual del programa en el
CRT
y luego recupera la instrucción en esa dirección.
Si los circuitos integrados de RAM están bien en esa dirección, la CPU reubica el programa de prueba en la siguiente ubicación de memoria, imprime la nueva dirección y repite el procedimiento.
Pero, si uno de los circuitos integrados de RAM es lo suficientemente lento como para devolver un patrón de bits incorrecto, la CPU malinterpretará la instrucción y se comportará de manera impredecible.
Sin embargo, es probable que la pantalla se bloquee mostrando la dirección del IC defectuoso.
Esto reduce el problema a ocho circuitos integrados, lo que supone una mejora con respecto a tener que comprobar hasta 32. […] El […] programa realizará una prueba de gusano presionando una instrucción RST 7 (RESTART 7) desde el extremo inferior de la memoria. hasta la última dirección laboral.
El resto del programa permanece estacionario y maneja la visualización de la ubicación actual del comando RST 7 y su
reubicación
.
Por cierto, el programa se llama prueba
de gusano
porque, a medida que la instrucción RST 7 avanza por la memoria, deja un
rastro
de
NOP
(NO OPERACIÓN).
[…]