¿Cómo sabe un microcontrolador que un byte solo es un código de operación?

Una respuesta demasiado simplista es que el diseñador de la CPU lo especificó de esa manera. Cuando diseñamos el acelerador de expresión regular cave-creek, que tenía una pequeña “CPU” adentro, me senté y expuse qué patrones de bits causarían qué acciones. Eso esencialmente definió los códigos de operación de esa CPU. El diseño del conjunto de instrucciones puede ser una actividad muy divertida ya que tiene recursos limitados y está tratando de adivinar cómo usarlos de manera más efectiva.

Observe cómo hablé de una parte específica de un chip específico, cuando hablé de la CPU. Una computadora moderna, incluidas las de su teléfono, en realidad tiene muchos cpus, y no solo me refiero a muchos núcleos. A menudo hay pequeños cpus que realizan funciones especiales para su computadora que pueden o no ejecutarse todo el tiempo. La mayoría de los cuales nunca sabrás ni te importarán.

Por ejemplo, su unidad de disco puede tener un controlador de almacenamiento en caché, que puede incluir una CPU en miniatura especializada que decide qué direcciones almacenar en caché y qué direcciones descartar de la memoria caché. Es probable que esa CPU no tenga un código de operación en el sentido en que está pensando, pero aún ejecuta un programa que toma decisiones, y cuanto más complejo y sofisticado sea el controlador, más probable será que el programa sea configurable por software, y más cerca es probable que sea para algo que reconocería como una CPU y para tener un campo específico de “código de operación”.

Del mismo modo, el último procesador cpus de Intel que conocía tenía al menos tres o cuatro CPU auxiliares que manejaban cosas como el arranque, la seguridad y las interrupciones. Uno de ellos era incluso un intérprete de Forth si no recuerdo mal. Muchos de esos cpus a los que no puedes acceder a menos que seas un oem. Incluso puede haber cpus en el interior que se usan solo mientras el chip se está fabricando y probando.

Ahora, hay una razón para todos estos “cpus”. Una vez que la lógica llega a un cierto nivel de complejidad, una forma fácil de razonar sobre ello es como una “máquina de estados”. Este es un modelo bien conocido que casi todos en ciencias de la computación o los campos relacionados se les enseña. Es la base de gran parte de la teoría de autómatas. De todos modos, a medida que las máquinas de estado se vuelven complejas, la evolución “natural” es hacia una “computadora de programa almacenada”, donde un poco de memoria retiene el programa y algún hardware ejecuta ese programa. La acción de “tecla” que realiza el hardware se denomina “operación” y los bits en la memoria que especifican esa acción se denominan “código de operación”.

Tenga en cuenta que no es necesario que el código de operación sea el “primer” byte (o incluso que sea un conjunto contiguo de bits, mucho menos un byte de longitud). Esas son solo convenciones que hacen que el conjunto de instrucciones de longitud variable de procesadores x86 sea fácil de implementar. Las máquinas que no están orientadas a bytes o que no tienen instrucciones de longitud variable no se limitan a esa organización y la mayoría de las veces usan otras formas de organizar los bits que no se ajustan a ese patrón.

Es común que el código de operación se especifique principalmente por un conjunto de bits que se agrupan lógicamente en un extremo de la memoria recuperada, simplemente porque eso hace que sea más fácil para el diseñador pensar en ellos. Sin embargo, si el conjunto de instrucciones surgió de algo más primitivo, o hay problemas con la longitud de los cables que conectan las partes, el código de operación se puede diseñar fácilmente a partir de piezas disjuntas, por ejemplo, los bits que especifican el direccionamiento indirecto a menudo se agrupan cerca de la dirección se están modificando para que esos bits se puedan enrutar juntos a la porción de memoria del chip y no tengan que ir a la “cpu” en absoluto. Del mismo modo, SIMD (datos múltiples de una sola instrucción) o MIMD (datos múltiples de varias instrucciones) o MISD cpus pueden dividir fácilmente el código de operación en diferentes partes.

Finalmente, los códigos de operación no se limitan solo a los cpus de hardware. Las máquinas virtuales pueden diseñarse siguiendo los mismos principios. Por ejemplo, en Yacc ++, el analizador generado es un programa para una computadora abstracta, y tiene códigos de operación y operandos, etc. Hay un software, “el motor” que ejecuta ese programa y al hacerlo implementa el analizador. Por lo tanto, también podemos implementar cpus en el software.

Ahora, como Alec señaló, cada uno de estos cpus solo ejecuta los códigos de operación a los que se dirige mediante el “contador de programa” (que puede o no ser una sola cosa) y si la CPU de alguna manera se dirige a bytes que no están destinados a sean instrucciones, la CPU puede hacer fácilmente algo inesperado. Dependiendo del diseño, puede ser posible recuperarse de eso o no. Afortunadamente, la mayoría de los diseñadores ponen operaciones de tipo a prueba de fallas en el diseño para que haya una manera de volver a un estado conocido. El pequeño botón de reinicio al que necesita un clip para llegar, esa es una de esas cosas. Si “bloquea” su dispositivo, lo ha puesto en un estado del que no sabe (o quizás nadie lo sabe) cómo recuperarse. La necesidad frecuente de reiniciar Windows fue un artefacto de lo mismo: el reinicio devolvió el sistema operativo (una CPU muy grande, compleja y poco entendida) a un estado conocido.

No lo sabe Es tarea del programador asegurarse de que los bytes recuperados cuando la CPU desea una instrucción, los bytes señalados por el contador del programa, no solo son códigos de operación, sino también los códigos de operación que el programador desea ejecutar en este momento.

Todos los datos son solo patrones binarios. Cualquier bit de datos dado puede ser códigos de operación, enteros, texto, coma flotante, datos comprimidos o cualquiera de cientos de otras cosas. El trabajo del programador, entre otras cosas, es garantizar que los datos binarios se interpreten de la manera correcta.

Algunas características de hardware pueden brindar una cantidad limitada de protección. Por ejemplo, si tiene una Unidad de administración de memoria, puede marcar bloques de memoria como ejecutables o no, y se produce una excepción si se usa de manera incorrecta. Pero esta es una protección muy limitada y solo detecta un subconjunto de errores.

¿Cómo sabe un microcontrolador que un byte recién obtenido es un código de operación?”

No lo hace. Asume que la primera instrucción que obtiene es un código de operación y va desde allí.

Cuando un microprocesador sale de reinicio (es decir, una vez que la energía es buena), el Contador de programa se reinicia y el microprocesador recupera el contenido de esa ubicación de memoria en la PC para ejecutarlo. El contador del programa puede haberse reiniciado inicialmente a 0x00000000 (la parte inferior de la memoria) o en una ubicación de memoria señalada en el ‘Vector de reinicio’, solo depende del microprocesador específico (marca / arquitectura).

Se supone que la primera ubicación de la memoria (o el contenido de la ubicación de la memoria señalada por el Vector de reinicio) es una instrucción (que puede o no incluir datos, pero eso es irrelevante).

Durante / al completar esa primera instrucción, el contador del programa se incrementará en la cantidad de palabras apropiadas para esa instrucción recién ejecutada (es decir, si es una instrucción de una sola palabra sin datos, la PC se incrementa en 1; si es una instrucción de 2 palabras que incluye 1 palabra de datos, la PC se incrementará en 2, y así sucesivamente), lista para que se obtenga la segunda instrucción.

Sé lo que probablemente estás pensando en este momento, “¡Eso es una locura! ¿Qué pasa si algo sale mal y la PC de alguna manera apunta a algún lugar cuyo contenido no es realmente una instrucción válida, sino algún tipo de datos? ¡¿Entonces el microprocesador no ejecutará código ‘aleatorio’? ”

Si. Sí lo hará

Glorioso, ¿no es así?

Este es simultáneamente uno de los mejores y peores aspectos de los microprocesadores tradicionales: siempre que la PC se incremente correctamente, todo estará bien. Pero si no es así, se desata el infierno (por lo general, la computadora se bloquea casi de inmediato).

De hecho, es esta cruda realidad la base de gran parte de la inseguridad de la informática moderna. Cuando se entera de problemas de infosec en los que “el pirata informático puede ejecutar código arbitrario como resultado de un escenario de desbordamiento del búfer”, esto es lo que sucede, a menudo porque ese búfer desbordado se coloca y se retira de la pila, pero son más datos de lo anticipado, por lo que cosas como la PC, que también se empuja a la Pila cuando se bifurca a una subrutina, se sacan de la pila al completar esa subrutina con contenidos diferentes de lo que deberían haber sido.

Confía en que lo son.

Ha habido vulnerabilidades en el pasado donde los datos se han ejecutado como código. Entonces, toma eso: p

La CPU busca en la memoria dónde apunta su puntero de instrucción, intenta interpretar los bytes allí como una instrucción y en realidad arroja una excepción (aunque no es exactamente lo mismo que las excepciones en OOP, pero más como una interrupción) si ese no es un conjunto válido de bytes que forman una instrucción.

No es el caso de que aparezca un byte en la CPU, y decide que el byte es un código de operación.

Por el contrario, las CPU pasan por ciclos repetidos de instrucción de búsqueda / instrucción de ejecución en las que provocan que las ubicaciones de memoria a las que hace referencia el contador de programa (PC) se recuperen. Por definición, los bytes de las ubicaciones designadas por PC buscadas son “opcode” (bytes).

Por lo tanto, no descubre códigos de operación. Los busca.