El software que se modifica a sí mismo suena como un truco prohibido: un programa interviene en su propio código, cambia las instrucciones y continúa convertido en algo ligeramente distinto.
Esa imagen es real, pero también demasiado limitada. La versión moderna es más amplia: el software puede generar código en tiempo de ejecución, aplicar parches a funciones en ejecución, especializarse según el comportamiento observado, reescribir su propio código fuente, cargar nuevo código nativo o producir versiones futuras de sí mismo. Algunas de estas técnicas son ejemplos elegantes de ingeniería. Otras son malware. Y otras son la base de runtimes de alto rendimiento.
La idea importante es esta: la automodificación no es un caso excepcional ajeno a las ciencias de la computación. Es una de las consecuencias de tratar los programas como datos.
¿Qué se considera software que se modifica a sí mismo?
Hay al menos cinco significados útiles:
Mutación de instrucciones. Un programa cambia bytes que más adelante se ejecutarán como instrucciones de máquina. Este es el significado clásico en lenguaje ensamblador.
Generación de código en tiempo de ejecución. Un programa escribe nuevo código ejecutable mientras está en marcha y luego salta a ese código. Los compiladores JIT hacen esto constantemente.
Reemplazo dinámico. Un sistema en ejecución redirige la ejecución del código anterior al nuevo sin reiniciarse. El livepatching del kernel y la actualización dinámica de software entran en esta categoría.
Reescritura a nivel de código fuente. Un programa cambia sus propios archivos de código fuente o de configuración y luego se reinicia, se recarga o se recompila.
Autorreproducción y autorreferencia. Un programa puede obtener, imprimir, transformar o incorporar una descripción de sí mismo. Los quines, el bootstrapping de compiladores y la replicación viral son los ejemplos canónicos.
Son mecanismos distintos, pero comparten una idea fundamental: la frontera entre «programa» y «datos» se puede desplazar. En una máquina de von Neumann, tanto las instrucciones como los datos se almacenan en memoria. En un lenguaje de programación, el código fuente puede representarse como cadenas, árboles de sintaxis, bytecode o una representación intermedia. Una vez que el código tiene una representación como datos, otro código puede inspeccionarlo, transformarlo y ejecutar el resultado.
El fundamento teórico: los puntos fijos
La demostración teórica más clara proviene de la teoría de la computabilidad.
El segundo teorema de recursión de Kleene dice, a grandes rasgos, que para cualquier transformación computable de programas existe un programa que se comporta igual que su propia versión transformada. Más formalmente, para una función computable f sobre descripciones de programas, existe un índice e tal que la función computable parcial descrita por e es extensionalmente igual a la descrita por f(e).
Es un teorema de punto fijo para código.
La interpretación práctica es poderosa: si puedes escribir un transformador de programas, la teoría de la computabilidad garantiza la existencia de un programa que puede, en efecto, tener en cuenta su propia descripción dentro de esa transformación. Por eso los quines son posibles sin una regresión infinita. Un programa que se imprime a sí mismo no contiene una copia infinita de sí mismo. Contiene una representación finita y un procedimiento para reconstruir el conjunto.
Neil D. Jones conecta directamente el teorema clásico con ideas de los lenguajes de programación: el teorema S-m-n, las máquinas universales y el segundo teorema de recursión de Kleene corresponden a la evaluación parcial, la autointerpretación y la reflexión. También destaca que la demostración original de Kleene es constructiva y, en esencia, construye un programa que se reproduce a sí mismo.
Ese es el primer tipo de evidencia: no una demostración práctica, sino un teorema. Existen programas que pueden operar sobre sus propias descripciones porque los puntos fijos existen en cualquier modelo de computación suficientemente expresivo.
Los quines son la prueba práctica más pequeña
Un quine es un programa que produce su propio código fuente sin limitarse a leer el archivo fuente. Es el «hola mundo» de la autorreferencia.
La idea de la demostración es simple:
- Colocar una parte del programa en un valor de datos.
- Imprimir ese valor tanto como texto ejecutable como datos entre comillas.
- Organizar las comillas de modo que la salida reconstruya exactamente el programa original.
En Python, el patrón puede ser tan breve como:
s = 's = %r\nprint(s %% s)'
print(s % s)
Ese fragmento no se modifica a sí mismo, pero demuestra la misma idea de fondo: el código puede contener una descripción finita que reconstruye el objeto de código completo. Los sistemas que se modifican a sí mismos agregan un transformador entre la «descripción» y el «siguiente programa».
La conferencia de Ken Thompson de 1984 al recibir el premio Turing, «Reflections on Trusting Trust», hizo famosa la versión de este problema relacionada con la seguridad. Thompson pasa de los programas que se reproducen a sí mismos a un compilador capaz de transmitir su propia puerta trasera a futuros binarios del compilador, incluso cuando el código fuente visible parece limpio. La lección sigue siendo contundente: inspeccionar el código fuente por sí solo no puede demostrar que una cadena de suministro de software está limpia si las herramientas que lo compilan ya están comprometidas.
Evaluación parcial: programas que especializan programas
La evaluación parcial es el punto donde la teoría empieza a parecerse a una herramienta de ingeniería.
Si un intérprete recibe dos entradas, interpreter(program, data), y especializas el intérprete con respecto a un program fijo, el resultado es un nuevo programa que ejecuta ese program directamente sobre futuros valores de data. Esta es la primera proyección de Futamura:
specialize(interpreter, program) = compiled_program
El trabajo de Futamura mostró que se puede generar un compilador a partir de un intérprete formal mediante la especialización de la computación. La segunda y la tercera proyección de Futamura van más allá: especializa el propio especializador y obtendrás generadores de compiladores.
Esto importa porque cambia la forma de entender la automodificación. Un sistema no necesita mutar instrucciones de máquina a ciegas para modificarse a sí mismo de una manera significativa. Puede incorporar un intérprete, recopilar información conocida, especializarse y producir una versión más rápida. El programa generado no es magia. Es el código residual que queda después de evaluar las partes conocidas de la computación.
Los compiladores JIT modernos son los descendientes industriales de esta idea, incluso cuando usan técnicas de implementación diferentes.
Compiladores JIT: el caso más extendido
Los motores de JavaScript, las JVM, los runtimes de .NET, LuaJIT, Julia y muchos motores de bases de datos y consultas dependen de la generación de código en tiempo de ejecución. Observan la ejecución, infieren información, generan código de máquina y, a veces, descartan ese código cuando dejan de cumplirse sus supuestos.
V8 es un ejemplo claro porque el equipo documenta públicamente su modelo de niveles de compilación.
En el pipeline de Ignition y TurboFan de V8, JavaScript primero se compila a bytecode y se interpreta. Después, los compiladores optimizadores usan la información recopilada durante la ejecución para producir código de máquina. El compilador Maglev de V8 agrega un nivel intermedio entre Sparkplug y TurboFan: usa información de ejecución, como las estructuras y los tipos de objetos, para construir nodos SSA especializados, genera código de máquina y recurre a la desoptimización cuando los supuestos dejan de cumplirse.
Esto es software que se modifica a sí mismo en el sentido práctico del runtime:
- El programa en ejecución produce nuevo código ejecutable.
- El nuevo código se especializa según el comportamiento observado.
- El runtime puede invalidar o reemplazar ese código más adelante.
- El comportamiento del programa debe seguir siendo semánticamente compatible con la especificación del lenguaje.
La evidencia no es solo conceptual. V8 reporta efectos concretos en benchmarks: el pipeline de Ignition/TurboFan mejoró Speedometer en un 5-10% y redujo el uso de memoria en un 5-10% en Chrome M59. Según lo reportado, el nivel Maglev compilaba aproximadamente 10 veces más rápido que TurboFan y, a la vez, producía código más rápido que Sparkplug.
ORC JIT de LLVM muestra el mismo patrón como infraestructura reutilizable. ORC ofrece APIs para enlazar archivos objeto reubicables a un proceso de destino en tiempo de ejecución, así como componentes que facilitan agregar LLVM IR a un proceso que usa JIT. No es una curiosidad de laboratorio. Es un subsistema oficial de LLVM para la compilación en tiempo de ejecución.
Livepatching: cambiar sistemas en ejecución sin reiniciarlos
La versión de los sistemas operativos es el livepatching.
La documentación del kernel de Linux describe el livepatching como una forma de corregir funciones críticas sin reiniciar el sistema, mediante la redirección de llamadas a funciones. También explica el mecanismo: kprobes, ftrace y livepatching están relacionados con la redirección de la ejecución del código, y los tres enfoques necesitan modificar código existente en tiempo de ejecución.
La documentación de Red Hat describe el mismo patrón en producción: el live patching del kernel puede aplicar parches a un kernel en ejecución sin reiniciar el sistema ni los procesos, y los módulos de parches proporcionan las funciones originales junto con punteros a las funciones de reemplazo.
Esta es evidencia práctica de que la modificación controlada de código en tiempo de ejecución ya forma parte de las operaciones en producción. El kernel no puede simplemente «reiniciar el proceso» porque es el sustrato, similar a un proceso, sobre el que se ejecuta todo lo demás. Por eso, el problema de ingeniería pasa a ser:
- identificar puntos seguros para aplicar parches
- redirigir la ejecución
- preservar un modelo de consistencia
- evitar aplicar un parche a una función mientras una tarea la ejecuta en un estado inseguro
- exponer suficientes metadatos para revertir o reemplazar parches cuando sea posible
Eso es automodificación con controles.
Actualización dinámica de software: más allá de los kernels
La actualización dinámica de software plantea la misma pregunta para aplicaciones generales: ¿podemos actualizar el código, los datos y los tipos de un programa en ejecución sin detenerlo?
Michael Hicks, Jonathan Moore y Scott Nettles propusieron un sistema para lenguajes similares a C en el que los parches dinámicos contienen tanto el código actualizado como el código de transición de la versión anterior a la nueva. Su implementación del servidor web FlashEd mostró una sobrecarga típica de actualización inferior al 1%.
Ese resultado es importante porque separa la automodificación del caos. El objetivo no es que «el programa se edite a sí mismo cuando quiera». El objetivo es un protocolo de actualización disciplinado:
- definir puntos de actualización
- transformar el estado anterior en el nuevo
- verificar la seguridad del nuevo código nativo
- usar los mecanismos de enlazado dinámico
- preservar la continuidad del servicio
La actualización dinámica es automodificación como ingeniería de operaciones.
Verificación: ¿se puede demostrar que el código que se modifica a sí mismo es correcto?
La objeción habitual es válida: si el código cambia mientras se ejecuta, ¿cómo podemos razonar sobre él?
«Certified Self-Modifying Code», de Hongxu Cai, Zhong Shao y Alexander Vaynberg, responde cambiando el modelo. En lugar de asumir que la memoria de código es inmutable, su framework trata el código del programa como una estructura de datos mutable ordinaria. Presentan un framework similar a la lógica de Hoare para código de máquina con manipulación de código en tiempo de ejecución, mecanizan la demostración de solidez en Coq y certifican ejemplos que se ejecutan en SPIM y en hardware x86 estándar.
Esa es una evidencia más sólida que «funciona en mi máquina». Muestra que el código de máquina que se modifica a sí mismo puede someterse a razonamiento formal cuando la lógica se diseña desde el principio para una memoria de código mutable.
Otros trabajos de verificación abordan el problema del análisis desde la verificación de modelos. Touili y Ye modelan programas que se modifican a sí mismos mediante sistemas de pila que se modifican a sí mismos y reducen la verificación de modelos LTL a un problema de vacuidad para sistemas de pila de Büchi que se modifican a sí mismos. Su implementación detectó varias muestras de malware que se modifica a sí mismo, incluidas muestras que varias herramientas antivirus conocidas no detectaron.
Así que la conclusión de la investigación no es que «el código que se modifica a sí mismo no se puede verificar». Es más precisa: las técnicas de verificación convencionales suelen asumir que el código es inmutable, por lo que los sistemas que se modifican a sí mismos necesitan semánticas y sistemas de demostración que hagan explícita la memoria de código.
Malware: la prueba oscura de su utilidad práctica
El código que se modifica a sí mismo también es común en el malware porque dificulta el análisis estático.
«Computer Viruses: Theory and Experiments», de Fred Cohen, definió los virus como programas que pueden modificar otros programas para incluir una copia de sí mismos que puede haber evolucionado. Más adelante, distintas familias de malware usaron cifrado, empaquetado, polimorfismo y metamorfismo para que cada instancia se viera diferente mientras conservaba su comportamiento.
Esta es evidencia desde el lado adversarial. Si el software que se modifica a sí mismo fuera solo teórico, quienes lo combaten no necesitarían verificadores de modelos, análisis dinámico, desempaquetadores, emuladores ni detección basada en comportamiento. Los autores de malware usan la automodificación porque cambia la economía del análisis: los bytes en disco no son necesariamente las instrucciones que importan en tiempo de ejecución.
El mismo mecanismo puede ser benigno u hostil. Los JIT generan código para mejorar la velocidad. El malware muta código para ocultarse. El livepatching redirige código para mejorar la confiabilidad. La diferencia está en la intención, el control y la verificación.
Agentes de IA y bases de código que se modifican a sí mismas
Los agentes modernos de programación con IA introducen una nueva capa: la automodificación a nivel de código fuente mediada por modelos de lenguaje.
Un agente puede inspeccionar un repositorio, editar archivos, ejecutar pruebas, observar fallas y volver a editar. No es lo mismo que un JIT que reescribe código de máquina en memoria, pero sí es automodificación a nivel del sistema de software. La base de código se convierte en artefacto y espacio de trabajo a la vez; el agente se convierte en un transformador de programas que opera en un ciclo de retroalimentación.
La teoría sigue siendo importante. Un agente que genera código es poderoso precisamente porque los programas pueden representar y transformar otros programas. Pero lo difícil no es generar un diff. Lo difícil es asegurar que el nuevo código mejore el sistema según un objetivo confiable.
Para la automodificación impulsada por IA, el estándar práctico de seguridad debería parecerse más al de la actualización dinámica de software y el SMC verificado que a un ciclo de mutación sin restricciones:
- versionar cada cambio
- ejecutar pruebas y análisis estáticos
- limitar la superficie editable
- exigir aprobación humana para cambios de alto riesgo
- preservar vías de reversión
- medir el comportamiento, no solo la forma del código
- mantener los secretos y los permisos de despliegue fuera del ciclo de reescritura
La lección de Thompson sigue siendo relevante aquí. Si un sistema puede reescribir las herramientas que lo evalúan, el pipeline de evaluación pasa a formar parte de la base de computación confiable.
¿Qué está demostrado y qué no?
Esta es la versión más sólida de la afirmación:
Demostrado en teoría: la autorreferencia y la autorreproducción están garantizadas por resultados de punto fijo como el segundo teorema de recursión de Kleene.
Demostrado de forma constructiva: los quines y el bootstrapping de compiladores demuestran una autorreferencia finita y ejecutable.
Demostrado en la práctica: los compiladores JIT, la traducción dinámica de binarios, el livepatching, el enlazado dinámico y la actualización dinámica de software generan, reemplazan o redirigen código ejecutable en producción.
Demostrado mediante métodos formales: el código de máquina que se modifica a sí mismo puede modelarse y certificarse cuando la semántica trata la memoria de código como mutable.
Demostrado en contextos adversariales: el malware usa la automodificación porque funciona contra la inspección estática y la detección basada en firmas.
Pero a menudo se introduce de forma subrepticia una afirmación más débil:
No está demostrado: que un sistema que se modifica a sí mismo vaya a mejorar de manera confiable.
Modificar es fácil. Mejorar es difícil. Un sistema necesita un objetivo, un proceso de búsqueda, restricciones, validación, reversión y una forma de evitar que lo que se evalúa corrompa al evaluador. Sin eso, la automodificación es solo movimiento.
El patrón práctico
A lo largo de la literatura, se repite el patrón seguro:
- Separar la representación del código de la autoridad para ejecutarlo.
- Hacer explícita la transformación.
- Definir cuándo se permite el reemplazo.
- Preservar un modelo de consistencia para el estado en ejecución.
- Validar el código generado o modificado mediante parches.
- Mantener un registro de auditoría y una vía de reversión.
Esto se cumple tanto si el mecanismo es un compilador JIT, un livepatch del kernel, una actualización dinámica de software o un agente de programación con IA.
El software que se modifica a sí mismo no es una sola cosa. Es una familia de técnicas en torno a una idea: el código puede ser un dato para otra computación. La teoría demuestra que la autorreferencia es posible. La evidencia de los sistemas demuestra que la modificación en tiempo de ejecución es útil. La literatura de seguridad demuestra que es peligrosa cuando los límites de confianza son débiles.
La versión futura probablemente tendrá menos que ver con programas que se reescriben al azar y más con la autoadaptación controlada: sistemas que se especializan, se aplican parches, se reparan y se amplían dentro de un ciclo medible y revisable.
La magia es real. La disciplina es lo que la convierte en ingeniería.
Fuentes
- Una navaja suiza para la computabilidad - Neil D. Jones
- Teoría de los autómatas que se reproducen a sí mismos - John von Neumann
- Evaluación parcial del proceso de computación: un enfoque para un compilador de compiladores - Yoshihiko Futamura
- Reflexiones sobre confiar en la confianza - Notas de la conferencia de Ken Thompson
- Código certificado que se modifica a sí mismo - Hongxu Cai, Zhong Shao, Alexander Vaynberg
- Lanzamiento de Ignition y TurboFan - Equipo de V8
- Maglev: el JIT optimizador más rápido de V8 - Equipo de V8
- Diseño e implementación de ORC - Documentación de LLVM
- Livepatch - Documentación del kernel de Linux
- Aplicación de parches con live patching del kernel - Documentación de Red Hat
- Actualización dinámica de software - Michael Hicks, Jonathan T. Moore, Scott Nettles
- Virus informáticos: teoría y experimentos - Fred Cohen
- Verificación de modelos LTL de código que se modifica a sí mismo - Tayssir Touili, Xin Ye
- Código que se modifica a sí mismo en sistemas evolutivos abiertos - Patrik Christen