1 puntos por GN⁺ 1 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Elevator traduce de forma estática un ejecutable x86-64 completo a AArch64 sin asumir información de depuración, código fuente ni layout del binario
  • En lugar de usar heurísticas para distinguir código y datos, construye un CFG superconjunto que contiene todas las interpretaciones posibles de cada byte y solo elimina las rutas que terminan en finalización del programa
  • Mapea el estado de x64 uno a uno a registros de AArch64 y maneja los saltos indirectos con una tabla de búsqueda que va de la dirección original al código traducido
  • Un banco de tiles offline escribe la semántica de instrucciones x64 como plantillas en C y luego las compila con LLVM 20 a secuencias de bytes AArch64
  • El resultado es un binario AArch64 autocontenido, sin traducción en tiempo de ejecución, y en SPECint 2006 ofrece un rendimiento igual o mejor que el JIT en modo usuario de QEMU

Objetivo de Elevator

  • Elevator es un traductor binario completamente estático que lleva un ejecutable x86-64 completo a AArch64
  • No usa información de depuración, código fuente, patrones de código del binario original ni supuestos sobre el layout del binario
  • Los traductores estáticos existentes dependen de heurísticas o de un fallback en tiempo de ejecución para distinguir código y datos, pero Elevator traduce por adelantado todos los bytes del ejecutable original según cada interpretación posible
  • Como cualquier byte puede ser dato, parte de un opcode o parte de un operando, construye un CFG superconjunto que incluye todos los flujos de control posibles y solo elimina las rutas que llevan a una terminación excepcional del programa
  • La salida consiste en un binario AArch64 autocontenido que incluye código traducido, el binario x64 original, una tabla de búsqueda de direcciones y un driver de ejecución
  • Una vez terminada la traducción, puede ejecutarse sin JIT ni soporte de traducción en tiempo de ejecución
  • Si se traduce dos veces el mismo binario de entrada, se genera la misma secuencia de bits de salida, y aquello que se prueba, verifica, certifica o firma criptográficamente coincide con el código realmente desplegado
  • El costo principal es el aumento del tamaño de código y, a cambio, ofrece mayor posibilidad de validación previa al despliegue que un emulador o un compilador JIT
  • La evaluación incluyó el benchmark completo de SPECint 2006 y binarios hechos a mano, y el rendimiento resultó igual o mejor que la emulación en modo usuario de QEMU con aceleración JIT
  • El equipo de investigación indicó que publicará todo el proyecto como open source al finalizarlo

Por qué se necesita la traducción estática y cuáles son los límites actuales

  • Cuando el hardware cambia de una ISA a otra, hace falta llevar el software existente a la nueva plataforma, y recompilar el código fuente disponible puede no ser suficiente
  • En código legado validado o certificado, muchas veces el objetivo de la certificación no es el código fuente sino un ejecutable binario autoritativo específico y bien probado
  • Reproducir más adelante ese mismo binario bit a bit a partir del código fuente puede requerir las versiones exactas del compilador, enlazador y sistema de build de ese momento, algo poco realista en la práctica
  • Si el fabricante aplicó parches directamente al binario sin pasar por el código fuente, volver a compilar desde el código archivado puede reintroducir errores que ya habían sido corregidos
  • Los enfoques existentes que procesan binarios directamente combinan emulación, traducción estática y traducción dinámica, pero los componentes adicionales del sistema que se ejecutan junto al programa traducido pasan a formar parte de la base de código de confianza
  • El comportamiento dinámico puede variar según el orden de prueba o las entradas, lo que dificulta verificar la confiabilidad total
  • Horspool y Marovac demostraron en 1980 que, para destransformar un ejecutable, hay que distinguir con certeza entre código y datos y que, en la mayoría de arquitecturas, esto equivale al problema de la parada, por lo que en general no puede resolverse
  • Los lifters binarios estáticos actuales aproximan la distinción entre código y datos con heurísticas, y el problema se agrava especialmente al predecir los destinos de transferencias de control indirectas
  • LLBT eleva instrucciones ARM a LLVM IR para recompilarlas a la arquitectura objetivo, pero usa heurísticas para detectar destinos de ramas indirectas y asume varias cosas sobre el binario de entrada
  • Incluso una buena heurística falla en algunas entradas, y como para elevar correctamente un binario completo todas las decisiones código/dato deben ser correctas, la probabilidad de falla crece con el tamaño del binario
  • Los enfoques dinámicos pueden recuperar instrucciones y manejar flujo de control indirecto porque siguen el flujo realmente ejecutado, pero no pueden elevar instrucciones que no se alcanzan en una ejecución concreta
  • En ISA con instrucciones de longitud variable como x64, una secuencia de instrucciones puede contener otras secuencias superpuestas, y si se salta al medio de una instrucción multibyte, operandos previos pueden decodificarse como instrucciones separadas
  • Los ataques ROP y la ofuscación de código pueden aprovechar esta característica
  • Rosetta II de Apple y Prism de Microsoft combinan traducción anticipada con componentes de traducción dinámica
  • WYTIWYG y Polynima elevan estáticamente rutas de flujo de control identificadas mediante perfilado dinámico y usan un fallback dinámico para recolectar información de flujo de control cuando se alcanza una dirección objetivo no vista
  • Elevator no decide qué byte es código o dato, ni si forma parte de una palabra de instrucción o de un operando, sino que incluye cada byte del ejecutable dentro de rutas de flujo de control separadas para todas sus interpretaciones posibles
  • Este enfoque aplica el desensamblado superconjunto a la recompilación estática y a la compilación cruzada entre ISA, intercambiando precisión de decodificación por crecimiento de código

Flujo de control y preservación del estado

  • Elevator opera con el principio de preservar por completo el estado x64 dentro del código AArch64 traducido
  • Mapea los registros x64 y AArch64 uno a uno para emular cada estado de registro x64 en su registro AArch64 correspondiente
  • La pila x64 se emula directamente sobre la pila AArch64, y la expansión normal de pila en ejecución queda a cargo del sistema operativo
  • No analiza el ABI del binario x64 de entrada, y solo realiza traducción de ABI en los puntos donde la ejecución pasa a código externo o vuelve desde él, siguiendo las reglas del ABI System V x64 y del AArch64 Procedure Call Standard
  • Gracias a la preservación completa del estado y a la correspondencia uno a uno de registros, cada instrucción x64 puede traducirse de forma independiente sin conocer las instrucciones anterior o posterior
  • Cada desplazamiento de byte ejecutable del binario original se interpreta simultáneamente como dato y como posible inicio de una secuencia de instrucciones
  • Todo destino potencial que no pueda analizarse estáticamente, como saltos indirectos, callbacks o dispatch en tiempo de ejecución, recibe un punto de aterrizaje correspondiente dentro del binario reescrito
  • En tiempo de ejecución, los destinos se resuelven con una tabla de búsqueda incluida en el binario final que mapea direcciones de instrucción originales a direcciones de código traducido
  • Ejemplo de instrucciones superpuestas

    • Listing 1 muestra que si se empieza a decodificar en .byte 0xB0, aparece MOV AL, 0xC3 seguido de RET, mientras que si se empieza un byte después, en ReturnC2, aparece solo RET
    • Ambas decodificaciones pueden alcanzarse desde el jz anterior, y si el traductor eligiera una sola interpretación para esos dos bytes, perdería una de las rutas
  • Ejemplo de salto indirecto calculado

    • Listing 2 muestra que call Label crea una dirección base para la tabla, pop rsi la recupera y luego suma un desplazamiento dependiente de la entrada para construir el destino de jmp rsi
    • La rama puede aterrizar en una de cuatro instrucciones inc eax colocadas a intervalos de 2 bytes dentro del flujo de codificación
    • Un traductor que solo reescriba destinos de salto interpretables estáticamente no tendría dónde hacer aterrizar una rama así
  • Llamadas, retornos y ramas

    • Las instrucciones call, return y branch no pueden expresarse como tiles en C porque la ubicación de la dirección de retorno, el contador de programa y el layout de las banderas condicionales difieren entre x64 y AArch64
    • Una llamada directa empuja la dirección de retorno x64 original a la pila emulada y salta al tile traducido del callee
    • Una llamada indirecta verifica si el destino está dentro del binario traducido o en una biblioteca externa; los destinos internos se traducen con la tabla x64 offset-to-tile y luego se salta al tile correspondiente
    • Para un destino externo, coloca en X30 la dirección del gadget de traducción ABI inversa al que volverá la biblioteca AArch64, ejecuta la traducción ABI de salida y luego salta al destino externo
    • Un retorno extrae de la pila emulada una dirección de retorno de 8 bytes, la compara con el rango del binario x64 incrustado y, si el retorno es interno, traduce la dirección con la tabla de búsqueda y salta al tile correspondiente
    • Las ramas directas tienen el destino conocido en tiempo de traducción, y las ramas condicionales se traducen como ramas condicionales AArch64 que inspeccionan los bits de flags x64 guardados en X14
    • Las ramas indirectas emiten la misma verificación de límites que las llamadas indirectas y los retornos, y si el destino es externo ejecutan la traducción ABI de salida

Pipeline de traducción basado en tiles

  • La traducción de Elevator se divide en tres etapas: generación offline del banco de tiles, reescritura por binario de entrada y empaquetado final
  • La etapa offline expresa la semántica de instrucciones x64 como funciones en C, las especializa para cada combinación de operandos bajo un mapeo fijo de registros x64 a AArch64 y luego las compila con un LLVM 20 modificado para producir secuencias reutilizables de bytes AArch64
  • La etapa por binario de entrada realiza el desensamblado superconjunto y, para cada instrucción candidata descubierta, busca el tile por nombre y concatena la secuencia de bytes AArch64 correspondiente
  • Las categorías de instrucciones difíciles de expresar como tiles en C, como las transferencias de flujo de control y los límites ABI, se manejan con pequeñas plantillas escritas a mano
  • La etapa de empaquetado combina el código traducido, el binario x64 original, la tabla de búsqueda de direcciones y el driver de ejecución para generar un binario AArch64 independiente
  • Banco de tiles offline

    • Es poco práctico escribir manualmente una secuencia equivalente de instrucciones AArch64 para cada instrucción x64
    • Incluso una sola plantilla como ADD Reg8, Reg8 se expande a 256 combinaciones concretas de registros, y el conjunto completo de instrucciones x64 tiene muchas variantes de registros, operandos de memoria y direccionamiento con inmediatos
    • Elevator escribe la semántica de cada instrucción x64 como una pequeña función en C, la especializa para combinaciones concretas de operandos y deja que LLVM la compile a AArch64
    • En el ejemplo de ADD Reg8, Reg8, la plantilla actualiza los 8 bits bajos del registro destino con una suma de 8 bits y conserva los 56 bits altos para respetar la semántica de escritura parcial de registros en x64
    • Como ADD Reg8, Reg8 en x64 también modifica las banderas Carry, Parity, Auxiliary Carry, Zero, Sign y Overflow de RFLAGS, y una función en C solo puede tener un valor de retorno, la actualización de banderas se captura en un tile de flags separado
    • Una sola instrucción x64 puede corresponder a uno o varios tiles, que luego se vuelven a concatenar de forma contigua al emitirse para restaurar la semántica completa
    • El atributo aarch64_custom_reg declara en qué registros AArch64 debe colocar LLVM el valor de retorno y cada argumento
    • El mapeo fijo se elige para alinear el carácter callee-saved/caller-saved de System V x64 y AAPCS64, reducir el reordenamiento de registros de argumentos enteros y dejar registros AArch64 callee-saved libres para un posible estado sombra a futuro
    • Los bits de RFLAGS de x64 y el archivo de registros XMM también se conservan en registros AArch64 dedicados bajo el mismo principio uno a uno
    • El LLVM 20 modificado maneja el atributo aarch64_custom_reg por función y reclasifica como callee-saved dentro del asignador de registros aquellos registros AArch64 que contienen el estado x64 emulado
    • TileGen recorre las plantillas en C, crea copias especializadas para cada combinación permitida de operandos y sintetiza mecánicamente los atributos a partir de la posición de los parámetros y del mapeo de registros de la plantilla
  • Reescritura por binario de entrada

    • Dado un binario x64 de entrada, la etapa por binario realiza un desensamblado superconjunto y recorre el CFG resultante
    • En cada nodo, el formateador construye el nombre del tile a partir del opcode y operandos de la instrucción decodificada y, cuando se necesitan varios tiles, combina varios nombres
    • x64 no impone restricciones de alineación para el puntero de pila, pero AArch64 exige alineación de 16 bytes cuando el puntero de pila se usa en operandos de memoria
    • Si RSP se mapea directamente a SP, patrones comunes de código x64 como PUSH consecutivos en el prólogo de una función pueden provocar excepciones de alineación en AArch64
    • Elevator hace que los tiles accedan a la pila a través de un registro separado, X25, y solo materializa SP dentro de él cuando el tile realmente lo necesita
    • Los tiles compilados con LLVM esperan una alineación de 16 bytes en SP al entrar, así que antes de ejecutar tiles detectados como asignadores de espacio de spill, SP se alinea hacia abajo y luego se restaura al terminar
    • Como los tiles de cálculo de flags son relativamente costosos, si las flags van a sobrescribirse antes de ser leídas por una instrucción post-dominante, se elimina el cálculo de flags del nodo actual
    • Las instrucciones no soportadas por ahora son principalmente las extensiones vectoriales anchas de x64, AVX2 y posteriores, y en esos puntos se inserta una instrucción de interrupción en lugar de un tile
    • En la evaluación completa de SPECint 2006, el ISA entero de x86-64 y el subconjunto SSE usado por SPECint bastaron para ejecutar todos los benchmarks
    • El soporte para instrucciones adicionales puede ampliarse agregando nuevos tiles, aunque los autores consideran que más trabajo de ingeniería difícilmente aportaría nuevas ideas científicas

Manejo de límites ABI

  • Elevator solo soporta binarios con enlace dinámico
  • Los binarios con enlace estático pueden incluir directamente instrucciones específicas de arquitectura como CPUID, mientras que los binarios dinámicos delegan eso en libc, lo que reduce la necesidad de traducción
  • Al interactuar con bibliotecas enlazadas dinámicamente, soporta la transición entre el entorno x64 emulado y el código nativo de bibliotecas AArch64, traduciendo entre el ABI Linux x64 y el ABI Linux AArch64
  • Los elementos clave que requieren traducción ABI son la disposición de argumentos y la ubicación de la dirección de retorno
  • El ABI System V x64 usa seis registros de argumentos: RDI, RSI, RDX, RCX, R8 y R9, y pasa argumentos adicionales en la pila a partir de [RSP+8]
  • CALL en x64 guarda la dirección de retorno en [RSP]
  • El AArch64 Procedure Call Standard usa ocho registros de argumentos, X0-X7, coloca los argumentos restantes en la pila en [SP] y guarda la dirección de retorno en X30
  • Llamadas a bibliotecas externas

    • Si una llamada x64 traducida apunta a una biblioteca externa, hay que reorganizar los argumentos para ajustarlos a la convención de llamada AArch64
    • Primero resta 8 a SP para volver a alinearlo a un límite de 16 bytes y deja la dirección de retorno x64 que ya estaba en la pila en [SP+0x8]
    • Carga en X6 y X7 los valores ubicados en [SP+0x10] y [SP+0x18], para que la biblioteca AArch64 pueda ver los posibles argumentos 7 y 8 que el código x64 dejó en la pila
    • Los argumentos de pila restantes seguirían desde [SP+0x20], lo que no coincide con la posición esperada por AArch64
    • Eliminar de la pila la dirección de retorno x64 y los valores movidos a X6 y X7 no es seguro, porque podrían no ser argumentos reales sino espacio de spill del caller o parte de una estructura colocada en su pila
    • Elevator no toca el layout de pila del caller: en su lugar reserva espacio adicional de pila de n×8 bytes y copia desde la ubicación actual los n posibles argumentos de 8 bytes
    • El valor por defecto de n es 10 y puede aumentarse por configuración si el binario de entrada pasa más de 16 argumentos en total a funciones de bibliotecas externas
    • Por último, guarda en X30 la dirección del gadget al que volverá la biblioteca externa
  • Retorno desde una biblioteca externa

    • Cuando el control vuelve al gadget guardado en X30 antes de la llamada a la biblioteca externa, suma n×8 al puntero de pila para limpiar los argumentos copiados previamente
    • Mueve el valor de retorno de la biblioteca externa desde X0 a X9, que es donde el código x64 emulado espera RAX
    • Extrae de la pila la dirección de retorno x64 original y el padding asociado, traduce esa dirección y salta allí para reanudar la ejecución después del CALL original
  • Callbacks que entran al código traducido

    • Si código nativo AArch64 llama al binario traducido, hay que convertir la convención de llamada AArch64 a la convención de llamada x64
    • El código x64 emulado espera el séptimo y octavo argumento en la pila, no en X6 y X7, así que primero hace push de X7 y luego de X6 para colocarlos donde x64 los espera
    • Si el callee no espera realmente un séptimo y octavo argumento, esos valores empujados no afectan nada
    • También hace push de la dirección de retorno que la instrucción AArch64 branch-and-link de la biblioteca externa dejó en X30, para ponerla en la posición de pila que espera una instrucción x64 de retorno
  • Retorno desde un callback a una biblioteca externa

    • Cuando el código traducido vuelve desde un callback a una biblioteca externa, realiza el proceso de entrada a la inversa
    • Saca la dirección de retorno de la pila, hace push de X6 y X7, y limpia el espacio de pila asignado sumando 0x10 al puntero de pila

1 comentarios

 
GN⁺ 1 시간 전
Comentarios en Hacker News
  • No sé exactamente qué hace el JIT en modo usuario de QEMU, pero parece que todavía tiene bastante margen de mejora
    En 2013 hice un motor JIT que traducía de x86-64 a aarch64, y en ese momento podía ejecutar binarios beta de Fedora para aarch64 y recompilar gran parte del port de Fedora a aarch64 sobre Linux x86_64
    También hice el JIT en la dirección opuesta, de aarch64 → x86-64, y por diversión mostré que ambos JIT podían ejecutarse entre sí en bucle dentro del mismo proceso, en algo como x86-64 → aarch64 → x86_64
    El JIT que hice mapeaba instrucciones y estado de CPU en una relación de uno a muchos, y era aproximadamente entre 2 y 5 veces más lento que el código recompilado de forma nativa
    Más tarde lo comparé con el JIT de QEMU, y QEMU parecía estar en un rango de entre 10 y 50 veces más lento
    Lamentablemente no tenía una licencia de código abierto, así que no puedo publicar el código para demostrarlo

    • Sí, el JIT de QEMU es casi un blanco fácil de superar
      Sobre todo si puedes especializar el diseño a “solo x86 a aarch64” y “solo modo usuario”, hay mucho rendimiento que se puede ganar
      El soporte de modo usuario de QEMU es más bien un apéndice de “funciona por casualidad” pegado al soporte de emulación de sistema completo, y toda la estructura del JIT también sigue el enfoque de “invitado → representación intermedia → anfitrión”, lo cual está bien para soportar varias arquitecturas invitadas y varias arquitecturas anfitrionas, pero dificulta aprovechar propiedades de combinaciones concretas invitado/anfitrión, como “x86 tiene pocos registros enteros, así que se puede hacer asignación fija” o “si pones el CPU aarch64 en el modo adecuado, la semántica compleja de punto flotante siempre queda correcta”
      Además, en el desarrollo de QEMU se invierte más tiempo en “emular la nueva característica X de arquitectura” que en encontrar oportunidades de optimización de rendimiento, porque quienes pagan el desarrollo consideran eso más importante
    • QEMU es más bien TCG que un traductor, y como fue diseñado para funcionar con n arquitecturas, tiene límites
  • Que la sección .text se vuelva 50 veces más grande es enorme, pero parece un costo aceptable a cambio de obtener una traducción completamente determinista
    En muchos casos, la diferencia de rendimiento frente a la emulación pesará más que la incomodidad del aumento de tamaño
    También es interesante que el multihilo y el manejo de excepciones no sean imposibles, sino que estén fuera del alcance de este proyecto
    Me pregunto si el siguiente paso será usar heurísticas para recortar el espacio de posibilidades y reducir el tamaño del binario
    Eso rompería la garantía de traducción, pero podría mejorar de forma práctica la portabilidad del binario

    • No necesariamente habrá una diferencia de rendimiento mejor que la emulación
      Este traductor es mucho más lento que Box64 o FEX, y salvo que por alguna razón no puedas usar JIT, simplemente es una peor opción
  • Siempre me he preguntado cómo maneja el traductor los saltos indirectos
    Al analizar un binario, solo puedes descubrir bloques de código conectados por saltos directos cuyo destino conoces
    Entonces eso implica que cada vez que ocurre un salto indirecto hay que encontrar la función objetivo, traducirla si hace falta y volver al código traducido, ¿no sería lento?
    Me pregunto si hay una forma más rápida, si se puede hacer coincidir la dirección de la función traducida con la dirección original, o si en la dirección original se inserta un salto hacia el código traducido

    • El traductor que hice es de nivel hobby, pero tenía una tabla grande de “si haces un jmp indirecto a la dirección X, el bloque correspondiente está en la posición Y”
      Este método es más lento que un jmp directo sin tabla, pero en el programa original los saltos indirectos ya eran más lentos de entrada, y normalmente no aparecen mucho dentro de los bucles críticos para el rendimiento
  • Me gusta muchísimo la idea del grafo de flujo de control superconjunto, pero si alguien va a leer el artículo, conviene tener en cuenta lo siguiente
    El tiempo de ejecución mejora unas 4.75 veces (más rápido que QEMU, pero bastante más lento que Box64), el número de instrucciones ejecutadas aumenta 7 veces y el tamaño del binario aumenta 50 veces
    Emula el ABI de x86 hasta antes de las llamadas externas
    Hay que emular gran parte del estado de CPU de x86, como EFLAGS, y hasta mov complejos deben calcularse individualmente
    Solo soporta binarios de un solo hilo
    No hay manejo de excepciones ni desenrollado de pila (unwinding)
    No soporta todo el conjunto de instrucciones

  • Es un trabajo interesante
    No lo revisé a fondo, pero parece que los offsets relativos todavía podrían ser un problema
    De todos modos, como el tamaño del resultado generado va a ser distinto, parecería que hace falta algún tipo de capa de traducción o MMU, y que eso afectará sobre todo a las tablas de salto y a las bifurcaciones internas
    Yo trato principalmente con cosas de los 90, y los desensambladores asumen muchas cosas sobre el inicio y el fin del código
    Pero a veces, si no tienes conocimiento previo como un puntero de punto de entrada en una ubicación fija, ni siquiera puedes encontrar ciertos bloques binarios
    Tras unas cuantas pasadas, parecería posible refinar el binario hasta dejarlo en “regiones que definitivamente son código”

  • Si “Elevator considera todas las interpretaciones posibles de cada byte y genera por adelantado una traducción separada para cada una de las posibilidades [...] podando solo los casos que terminan en fallo anormal”, entonces ¿todos los programas reales con posibles colisiones terminan siendo podados?

    • Supongo que en la tabla de búsqueda de dirección→código se configurará una ruta de colisión estandarizada
      Entonces seguiría habiendo colisión, pero no sería la misma que el fallo por ejecutar directamente código incorrecto
  • La parte que más me interesa es desde la perspectiva de la certificación
    En industrias reguladas como aviación o dispositivos médicos, muchas veces no se puede usar JIT precisamente porque el código que se ejecuta tiene que ser código certificado
    Una traducción estática que produzca un binario firmable podría ser un avance real, incluso aceptando la expansión del código

    • Me pregunto qué tan grande es esta área dentro de la industria del software
      Probablemente tampoco haya forma de aplicar LLM a gran escala aquí, pero en la gran conversación sobre “IA en el trabajo” casi no se toca este tipo de cosas
  • 50 veces no es razonable, es un desastre para la caché
    Puede comerse por completo cualquier ganancia de rendimiento obtenida al evitar JIT

    • Eso solo sería así si todo ese código se usara en tiempo de ejecución; probablemente la gran mayoría de los puntos de inicio de decodificación posibles nunca se usen
    • Este es un caso que encaja muy bien con la reubicación de código en tiempo de enlace
      Si juntas el código caliente en un solo lugar, puedes hacer que el código no usado nunca llegue a cargarse
    • No sacaría conclusiones apresuradas
      Las instrucciones de por sí no son tan grandes, y además el CPU optimiza mientras ejecuta
  • ¿Puede manejar código automodificable?
    También me pregunto por qué solo x86_64
    Parecería más útil traducir programas de 32 bits, como juegos viejos

    • Si lees el artículo enlazado, esto se trata explícitamente
      “Código automodificable y código compilado por JIT. Elevator, como cualquier reescritor de binarios completamente estático, no soporta código automodificable ni código compilado por JIT”
    • El código automodificable fuera de runtimes JIT me parece bastante raro hoy en día comparado con los 80 y 90
      Hoy la sección .text casi siempre es de solo lectura, y no parece que las exigencias de seguridad vayan a disminuir
    • Si manejara código automodificable, entonces ya no sería “completamente estático”
      Sería una contradicción fundamental
    • Desde la perspectiva de desarrollar x86 nuevo, el código automodificable puede ser posible, pero normalmente es terrible
      Porque destruye el rendimiento de predicción de bifurcaciones en caché y pipeline
      Además viola W^X, así que normalmente solo debe usarse en páginas de memoria compatibles con JIT
      Por eso casi siempre debe evitarse
      En la época del 486 o del P5 se usaba un poco, por ejemplo con inmediatos como si fueran variables de bucle interno, pero hoy ya casi no
      Para lograr una emulación o traducción casi perfecta hay que manejar muchos casos excepcionales sucios de x86
  • ¿Dónde está el código fuente?