Saltar al contenido

Decodificación de instrucciones cuando las instrucciones son de longitud variable

Este grupo de especialistas pasados varios días de investigación y recopilar de datos, han obtenido la solución, deseamos que todo este artículo sea de gran utilidad para tu plan.

Solución:

Hay muy buenas razones para tener una longitud de instrucción fija, siendo la simplicidad de implementación la más importante. Es por eso que muchos procesadores tienen una longitud de instrucción fija, como los procesadores RISC y muchas de las primeras computadoras.

Los conjuntos de instrucciones CISC como x86 están diseñados para ser decodificados secuencialmente (paso a paso) por microcódigo. (Puede pensar en el microcódigo como una especie de intérprete para las instrucciones CISC) Ese era el estado del arte a principios de los 80, cuando se diseñó x86.

Hoy en día esto es un problema, porque el microcódigo está muerto. Las instrucciones x86 ahora se dividen en µ-ops más pequeñas, al igual que las instrucciones RISC. Pero para hacerlo, las instrucciones x86 deben decodificarse primero. Y las CPU actuales decodifican hasta 4 instrucciones en cada ciclo. Debido a que no hay tiempo para decodificar secuencialmente una instrucción tras otra, esto funciona simplemente por fuerza bruta. Cuando se trae una línea desde la caché de instrucciones, muchos decodificadores decodifican la línea en paralelo. Un decodificador de instrucciones en cada posible desplazamiento de bytes. Después de la descodificación, se conoce la longitud de cada instrucción y el procesador decide qué decodificadores proporcionan realmente instrucciones válidas. Esto es un desperdicio, pero muy rápido.

Los tamaños de instrucción variables presentan más problemas, por ejemplo, una instrucción puede abarcar dos líneas de caché o incluso dos páginas en la memoria. Entonces tu observación es acertada. Hoy en día, nadie diseñaría un conjunto de instrucciones CISC como x86. Sin embargo, algunos RISC han introducido recientemente un segundo tamaño de instrucción para obtener un código más compacto: MIPS16, ARM-Thumb, etc.

EDITAR: con la esperanza de hacerlo más legible.

El hardware no considera la memoria como una larga lista de bytes desorganizados. Todos los procesadores, de longitud de palabra fija o variable, tienen un método de arranque específico. Por lo general, una dirección conocida en la memoria del procesador / espacio de direcciones con una dirección para la primera instrucción del código de arranque o la primera instrucción en sí. A partir de ahí y para cada instrucción, la dirección de la instrucción actual es donde comenzar la decodificación.

Para un x86, por ejemplo, tiene que mirar el primer byte. Dependiendo de la decodificación de ese byte, es posible que deba leer más bytes de código de operación. Si la instrucción requiere una dirección, un desplazamiento o alguna forma de valor inmediato, esos bytes también están allí. Muy rápidamente, el procesador sabe exactamente cuántos bytes hay en esta instrucción. Si la decodificación muestra que la instrucción contiene 5 bytes y comenzó en la dirección 0x10, la siguiente instrucción está en 0x10 + 5 o 0x15. Esto continúa para siempre. Las ramas incondicionales, que dependiendo del procesador pueden venir en varios sabores, no asume que los bytes que siguen a la instrucción son otra instrucción. Las ramas, condicionales o incondicionales, te dan una pista de dónde comienzan en la memoria otra instrucción o serie de instrucciones.

Tenga en cuenta que el X86 de hoy definitivamente no recupera un byte a la vez cuando decodifica una instrucción, ocurren lecturas de tamaño razonable, probablemente 64 bits a la vez, y el procesador extraerá los bytes de eso según sea necesario. Al leer un solo byte de un procesador moderno, el bus de memoria todavía hace una lectura de tamaño completo y presenta todos esos bits en el bus donde el controlador de memoria solo extrae los bits que buscaba o puede ir tan lejos como para mantener esos datos . Verá algunos procesadores en los que puede tener dos instrucciones de lectura de 32 bits en direcciones consecutivas, pero solo una lectura de 64 bits ocurre en la interfaz de memoria.

Te recomiendo que escribas un desensamblador y / o un emulador. Para instrucciones de longitud fija, es bastante fácil, simplemente comience por el principio y decodifique a medida que avanza en la memoria. Un desensamblador de longitud de palabra fija puede ayudarlo a aprender sobre las instrucciones de decodificación, que es parte de este proceso, pero no le ayudará a comprender cómo seguir instrucciones de longitud de palabra variable y cómo separarlas sin desalinearse.

El MSP430 es una buena opción como primer desensamblador. Hay herramientas gnu asm y C, etc. (y llvm para el caso). Comience con ensamblador y luego C o tome algunos binarios prefabricados. Ellos key Si tiene que recorrer el código como el procesador, comenzar con el vector de reinicio y recorrer su camino. Cuando decodifica una instrucción, conoce su longitud y sabe dónde está la siguiente instrucción hasta que llega a una rama incondicional. A menos que el programador haya dejado intencionalmente una trampa para engañar al desensamblador, suponga que todas las ramas condicionales o incondicionales apuntan a instrucciones válidas. Una tarde o una noche es todo lo que se necesita para explotar todo o al menos entender el concepto. No es necesario que descodifique por completo la instrucción, no tiene que convertirlo en un desensamblador completo, solo necesita descodificar lo suficiente para determinar la longitud de la instrucción y determinar si es una rama y, de ser así, dónde. Al ser una instrucción de 16 bits, puede, si lo desea, construir una vez una tabla de todas las posibles combinaciones de bits de instrucción y sus longitudes, lo que puede ahorrar algo de tiempo. Todavía tienes que decodificar tu camino a través de las ramas.

Algunas personas pueden usar la recursividad, en su lugar, uso un mapa de memoria que me muestra qué bytes son el comienzo de una instrucción, qué bytes / palabras son parte de una instrucción, pero no el primer byte / palabra y qué bytes no he decodificado todavía. Empiezo tomando los vectores de interrupción y reinicio y utilizándolos para marcar el punto de partida de las instrucciones. y luego entrar en un bucle que decodifica las instrucciones en busca de más puntos de partida. Si ocurre un pase sin otros puntos de partida, entonces he terminado esa fase. Si en algún momento encuentro un punto de partida de instrucción que cae en medio de una instrucción, hay un problema que requerirá la intervención humana para resolverlo. Desmontando roms de videojuegos antiguos, por ejemplo, es probable que vea este ensamblador escrito a mano. Las instrucciones generadas por el compilador tienden a ser muy limpias y predecibles. Si supero esto con un mapa de memoria limpio de instrucciones y lo que queda, (asumir datos) puedo hacer una pasada sabiendo dónde están las instrucciones y decodificarlas e imprimirlas. Lo que nunca puede hacer un desensamblador para conjuntos de instrucciones de longitud de palabra variable es encontrar todas las instrucciones. Si el conjunto de instrucciones tiene, por ejemplo, una tabla de salto o algún tipo de dirección calculada en tiempo de ejecución para la ejecución, no encontrará todas esas sin ejecutar realmente el código.

Hay una serie de emuladores y desensambladores existentes, si quieres intentar seguir adelante en lugar de escribir el tuyo propio, tengo algunos para mí http://github.com/dwelch67.

Hay pros y contras a favor y en contra de la longitud de palabra variable y fija. Fixed tiene ventajas, seguro, fácil de leer, fácil de decodificar, todo es bueno y correcto, pero piense en la memoria RAM, la caché en particular, puede meter muchas más instrucciones x86 en la misma caché que un ARM. Por otro lado, un ARM puede decodificar mucho más fácilmente, mucho menos lógica, potencia, etc., más por su dinero. Históricamente, la memoria era cara, la lógica era cara y funcionaba con un byte sobre la marcha. un código de operación de un solo byte lo limitaba a 256 instrucciones, por lo que se expandió a algunos códigos de operación que necesitaban más bytes sin mencionar los inmediatos y direcciones que lo hacían con una longitud de palabra variable de todos modos. Mantenga la compatibilidad inversa durante décadas y terminará donde está ahora.

Para aumentar toda esta confusión, ARM, por ejemplo, ahora tiene un conjunto de instrucciones de longitud de palabra variable. Thumb tenía una sola instrucción de palabra variable, la rama, pero puede decodificarla fácilmente como una longitud fija. Pero crearon thumb2 que realmente se asemeja a un conjunto de instrucciones de longitud de palabra variable. Además, muchos / la mayoría de los procesadores que admiten las instrucciones ARM de 32 bits también admiten instrucciones de pulgar de 16 bits, por lo que incluso con un procesador ARM no puede simplemente alinear los datos por palabras y decodificar sobre la marcha, debe usar una longitud de palabra variable. Lo que es peor, las transiciones de ARM a / desde el pulgar se decodifican mediante la ejecución, normalmente no se puede simplemente desmontar y descifrar el brazo del pulgar. A mixed La rama generada por el compilador de modo a menudo implica cargar un registro con la dirección para la rama y luego usar una instrucción bx para hacerlo, por lo que el desensamblador necesitaría mirar el bx, mirar hacia atrás en la ejecución para el registro usado en la rama y esperar que usted encontrar una carga allí y esperar que sea el segmento .text desde el que se está cargando.

No podré responder cómo se decodifican exactamente, pero puedo responder por qué son de longitud variable.

La razón de la longitud variable se debe tanto al deseo de mantener el tamaño del código pequeño como a las extensiones imprevistas del conjunto de instrucciones.


Reducción del tamaño de la instrucción

Algunas instrucciones (por naturaleza) necesitan mucho más espacio para codificar. Si todas las instrucciones se establecieran en una longitud fija lo suficientemente grande para acomodarlas, habría mucho espacio desperdiciado en el código de instrucción. Las instrucciones de longitud variable permiten “comprimir” las instrucciones a un tamaño más pequeño.


Extensiones (imprevistas) del conjunto de instrucciones

La otra razón son las extensiones del conjunto de instrucciones. Originalmente, x86 solo tenía 256 códigos de operación. (1 byte) Luego fue necesario agregar más instrucciones, por lo que descartaron una instrucción y usaron su código de operación como un carácter de escape para nuevos códigos de operación. El resultado es que las instrucciones más nuevas eran más largas. Pero era la única forma de ampliar el conjunto de instrucciones y mantener la compatibilidad con versiones anteriores.

En cuanto a cómo los decodifica el procesador, es un proceso complicado. Para cada instrucción, el procesador necesita encontrar la longitud y decodificar desde allí. Esto conduce a un proceso de decodificación inherentemente secuencial que es un cuello de botella de rendimiento común.

Los procesadores x86 modernos tienen lo que se llama un caché uop (micro-op) que almacena en caché las instrucciones decodificadas en algo que es más manejable (y similar a RISC) por el procesador.

Si te animas, tienes la libertad de dejar un ensayo acerca de qué te ha gustado de este tutorial.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *