4Story – Se ha anunciado el editor del MMORPG clásico para PC


El editor de juegos Papaya Play anunció que se está preparando para el lanzamiento el 19 de noviembre de 4Story, el clásico MMORPG para PC desarrollado
Guild Wars 2 – ArenaNet soluciona problemas de rendimiento en una actualización


ArenaNet ha proporcionado una actualización sobre problemas de rendimiento en Guild Wars 2.

La actualización es un seguimiento de
Crowfall – Cambios en las diferentes clases del juego


ArtCraft Entertainment detalla los cambios en las 11 clases del juego.

El clérigo comenzará con la habilidad Bloquear y cambiará el lanzamiento de martillos
Black Desert Online – La versión consola desbloquea 4 clases de sucesión y evento de creación previa de Guardian


Pearl Abyss desbloquea habilidades de sucesión para cuatro clases más en la última actualización de Black
Albion Online – Está disponible la actualización Brimstone & Mist


Sandbox Interactive acaba de lanzar la actualización de mitad de temporada de Albion Online Brimstone & Mist. La actualización presenta nuevos
Bless Unleashed – Se lanza oficialmente en Playstation 4


Bless Unleashed llega a otra plataforma cuando el MMORPG de fantasía se lanza oficialmente en PlayStation 4. El juego se puede descargar gratis desde PlayStation
Fractured – Las pruebas alpha comienzan con nuevos sistemas y contenidos


El desarrollador de Fractured, Dynamight Studios ha introducido un montón de nuevas características en el juego en preparación para el inicio
Final Fantasy XIV – Se lanzó el parche 5.35 con nuevos elementos, contenido y más


El último parche para Final Fantasy XIV se ha lanzado con una tonelada de contenido, elementos y correcciones para los aventureros
Ashes of Creation – Se anuncia las fechas de las pruebas alpha One del juego


El director creativo de Intrepid Studios, Steven Sharif, anunció el calendario de las pruebas de Ashes of Creation Alpha One en su última
Skyforge – Está disponible en Geforce Now de Nvidia


Los desarrolladores detrás de  Skyforge han oído hablar de la exclusividad para plataformas específicas y parecen pensar que es una tontería, porque el juego está

Meltdown y Spectre – Explicación real para geeks.

Ya varios sabemos a primera vista donde van estos sucesos que están afectando a procesadores modernos,
esto ha causado cierto revuelo, sobre todo en la comunidad Gamer, algunos difundiendo cosas falsas, ya sea por fanatismo, o por desconocimiento.

El objetivo de este post es explicar la forma en que funcionan esos hoyos de seguridad desde el punto de vista profesional, intentaré explicar todos los términos de la mejor manera posible, y de poner imágenes que clarifiquen esto, no explicaré cada mínimo detalle de estos sucesos, pero si lo suficiente como para que tengas una idea de:

  1. El funcionamiento de los procesadores.
  2. El funcionamiento de ambas «fallas».
  3. El grado en que nos afecta.

Comencemos definiendo a lo que afecta, los procesadores, según Wikipedia:

Es el hardware dentro de un ordenador u otros dispositivos programables, que interpreta las instrucciones de un programa informático mediante la realización de las operaciones básicas aritméticas, lógicas y de entrada/salida del sistema.

Creo que está más que claro, entonces, mientras se necesitaba que los procesadores fueran más rápidos, dentro de ellos se comenzó a implementar lo que se llama:

  • Ejecución fuera de order (out-of-order execution)
  • Predictor de saltos (branch predictor)
  • Predictor de objetivo de salto (branch target predictor)
  • Ejecución especulativa (Speculative execution)

El objetivo en conjunto de estas tecnologias es hacer que el procesador se encuentre lo más ocupado posible y por ende tener mejor IPC (Instrucciones Por Ciclo) con el fin de que no se produzcan cuellos de botellas por la(s) cadena(s) de dependencia.

Antes de explicar con cierto detalle los amiguitos que enlistamos arriba, debemos hacer una explicación sobre como los datos llegan y salen del procesador, ósea, como este maneja la memoria.

En concepto básico tenemos tres tipos de memoria, ósea, donde almacenar los datos.

El disco -> La RAM -> La caché del procesador.

Mientras que el disco es una memoria no volátil, ósea, que los datos que escribimos allí deben tener persistencia, es más lenta transfiriendo y escribiendo datos que una RAM, y la RAM a su vez, es más lenta que la caché que trae el procesador.

Como ya ustedes sabrán, mientras más rápida es la memoria, menos espacio esta dispone.

Cuando un sistema operativo va a ejecutar un proceso, estas instrucciones y data deben ir a la caché del procesador, a pesar que tanto en el disco, como en la RAM, tanto las instrucciones como los datos son datos, en la caché del procesador llegan a ser «datos» e «instrucciones», pues cada uno va a una cache «diferente».

Tenemos la d-caché que es la cache de los datos y, como adivinan, la i-caché que es la caché de las instrucciones.

¿Por qué el procesador necesita la capa de cache? Debido a que necesita un espacio físicamente cerca y optimizado de donde rápidamente sacar las instrucciones y la data y donde escribir los resultados.

Hagamos una comparación con un carpintero. Un carpintero pudiera tener un almacén, donde guarde todos los materiales que usa, así como sus herramientas. Las herramientas son instrucciones y los materiales es la data, en este punto, el almacén es el disco.

El carpintero también tiene un local no muy lejano al almacén, en el cual también es destinado a tener herramientas y materiales, este local vendría siendo nuestra RAM.

Dentro del local el carpintero tiene unas mesas de trabajo, donde coloca tanto las herramientas y materiales que está usando para el trabajo actual, donde coloca las herramientas es el i-cache y los materiales el d-cache, juntos forman la cache.

Como adivinarán, varios carpinteros serían varios núcleos, cada uno con una mesa de trabajo, actualmente disponemos de núcleos virtuales, por ejemplo 4 núcleos físicos suelen ser 8 núcleos virtuales, esto es, 4 núcleos y 8 hilos, entonces los otros 4 núcleos virtuales vendrían siendo el ayudante del carpintero.

Así que si hay 4 carpinteros y cada carpintero tiene 4 ayudantes para aprovechar la mesa de trabajo, entonces tenemos 8 hilos.

 

Memory hierachy.png

 

En la imagen de arriba pueden ver como se introduce un nuevo concepto, que son Page Table, o las tablas de página en español, estas se encargan de dos cosas

1) Traducir la dirección correspondiente de la memoria virtual a la memoria física.
2) Verificar los privilegios de acceso del determinado proceso.

Alrededor del concepto de Page Table tenemos cosas como:

  • Memory management unit (MMU)
  • Translation lookaside buffer.
  • Page Fault.
  • etc.

No vamos a entrar en esos detalles, pues no es el objetivo de este post.

Ya ha llegado el momento de explicar los 4 amiguitos del principio.

Ejecución fuera de order (out-of-order execution):

Este concepto se basa en re-ordenar el orden de las instrucciones para hacer que el procesador aproveche mejor sus unidades de procesamiento. Para poder «verlo» veamos un ejemplo.

Supongamos que nuestro procesador puede ejecutar un máximo de 2 instrucciones por ciclo, sean cuales fuera.

a = 5 
b = 3

c = a + b 
c = c + 3

d = b * 7
e = a * 2

En el ejemplo de código de arriba el procesador ya conoce los valores de a y de b.

Si el procesador fuera a ejecutar el código de arriba, tal y como está le tomaría 3 ciclos.

/* Primer ciclo */
c = a + b
 
/* Segundo ciclo */
c = c + 3
d = b * 7

/* Tercer ciclo */
e = a * 2

¿Por qué en el primero ciclo solo ejecutó una instrucción y en el segundo solo dos? Por la cadena de dependencia, para calcular correctamente c = c + 3 el procesador necesita conocer primero c = a + b entonces tiene que primero terminar ese cálculo para luego pasar a los siguientes.

En el segundo ciclo c = c + 3 ,  d = b * 7  no dependen entre ellos, por lo cual pueden ejecutarse en paralelo. Como ya el segundo ciclo está lleno e = a * 2 se queda para un tercer ciclo.

Pero recordando, nuestro procesador ejecuta 2 instrucciones por ciclo, en tres ciclos de un total de 6 solo ejecutamos 4 instrucciones, ósea que, hemos desperdiciado prácticamente un ciclo de procesamiento.

Ahora, gracias la ejecución fuera de orden el procesador puede reordenar las instrucciones para llenar esos huecos faltantes, como e = a * 2 no depende de ninguna de las 3 instrucciones anteriores se puede ejecutar junto
con c = a + b

/* Primer ciclo */
c = a + b
e = a * 2 <----------\
                     |
/* Segundo ciclo */  |
c = c + 3            |
d = b * 7            |
e = a * 2 ------------

Y nos queda:

/* Primer ciclo */
c = a + b
e = a * 2
 
/* Segundo ciclo */
c = c + 3
d = b * 7

Y ahora hemos aprovechado al máximo la capacidad de nuestro procesador gracias a esta técnica.

Predictor de saltos y Predictor de objetivo de salto.

Estos dos suelen trabajar bien juntos, dado que la programación no es lineal, tenemos que tomar dediciones, y cada decisión
es una ruta o camino diferente.

Vamos a poner otro ejemplo, ya que estamos en un blog de MMORPG, pongamos un ejemplo común en estos juegos, tu, como jugador vas a la tienda a comprar, digamos, pociones de salud que tienen determinado precio.

if( jugador.dinero >= pociones.cantidad * pociones.precio )
{
   comprar( pociones ) ;
}
else
{
   error( "No tiene dinero suficiente" ) ;
}

El código de muestra de arriba es simple, el jugador quiere comprar cierta cantidad de pociones que tienen cierto precio, si tiene el dinero suficiente la compra se transmita, sino el juego le indicará un mensaje de error «No tiene dinero suficiente».

Entonces mientras la condición se evalúa, el procesador podría ir «tramitando la compra» o ir preparando el «mensaje de error», el punto es que el procesador, para no pararse innecesariamente tiene que decidir si la condición es verdadera o falsa.

Esto es lo que hace el predictor de saltos, intenta predecir si una condición se va a dar o no.

Ahora ¿Qué hace el predictor de objetivo de salto? Intenta predecir «donde», ósea, la posición a la que se va a saltar, cada instrucción tiene un lugar en la memoria, ahora, como ya saben, en lo que esa instrucción se carga, decodifica, y se ejecuta, pueden ir haciendose otras cosas. En pocas palabras, el predictor de objetivo de salto intentará predecir a donde está la proxima instrucción.

Luego, tanto los resultados de predictor de salto, como el predictor de objetivo de salto llegan y terminan evaluandose a su debido tiempo, si ambos acertaron el procesador sigue feliz con su camino y aplicar los cambios a la memoria principal, pero si uno de los dos falló, el procesador tendrá que retroceder hasta cierto punto (hay cosas que no vuelven a su estado anterior, ojo con esto), descartar los cambios y seguir por la ruta correcta, obviamente si se equivoca hay una buena penalización de tiempo, para nuestra fortuna, los procesadores son muy buenos prediciendo ese tipo de cosas.

Ejecución especulativa (Speculative execution)

Aquí es donde se produce principalmente el error, luego de que el Predictor de saltos y el Predictor de objetivo de salto hacen su trabajo, la ejecución especulativa comienza a ejecutar el código basado en los resultados que arrojaron los dos anteriores predictores anteriores.

No vamos a hablar como un procesador ejecuta las instrucciones per se, puesto que varia internamente y yo ni siquiera lo sé.
Así que enfoquémosnos a saber como se produce este hoyo de seguridad.

Una forma de reproducir esta falla es contando básicamente con:

  1. Una condición
  2. Dos arrays (arreglos)
  3. Una dirección de memoria a la cual atacar sea del modo kernel o de otro proceso en modo usuario.
  4. Alguna forma de pre-condicionar al procesador y reiniciar el cache interno.
  5. Poder medir el tiempo a los accesos a la memoria.

 

Comencemos, para quien no sepa, en definir formalmente un array (arreglo).

Esto es un espacio de memoria continua el cual suele estar destinado a guardar un conjunto de valores.

Un array suele venir acompañado de una posición de memoria (en la memoria virtual), llamémosle base_address , un tamaño por elemento elem_size (en bytes) y la cantidad de elementos arr_size.

Comúnmente, cuando un procesador lee una dirección de memoria, este también cargará en la caché sus datos vecinos hasta cierta cantidad, por lo cual, si un array es pequeño puede que lo cargue completamente, si es muy grande puede que solo cargue parte de el en la memoria caché.

/* ? = puede variar, incluso ser calculado en tiempo de ejecución */

size_t array1_size = ? ;
const size_t array2_size = 256 * 512 ; /* 256 espacios de tamaño 512 */

/* Usamos arreglos de 8 bits (1 byte) cada espacio */
uint8_t array1[ array1_size ] ;
uint8_t array2[ array2_size ] ;

ptrdiff_t offset = <TARGET_ADDRESS> - (ptrdiff_t)array1 ;

if( offset < array1_size )
{
  array2[ array1[ offset ] * 512 ] ;  
}

En el código de arriba hemos creado un array de tamaño 256 * 512 y el tamaño de su elemento uint8_t es de 8 bits = 1 byte.

offset es justamente la dirección de memoria a la que queremos atacar, ósea, la dirección de memoria que nosotros no deberíamos tener acceso.

Nuestro objetivo es pre-condicionar al procesador para que:

  • array1_size y array2 no se encuentren en la caché.
  • Que el valor que queremos leer se encuentre en la caché, así poder tener una lectura rápida.
  • Que offset haya pasado anteriores veces muchas condiciones «verdaderas» y así poder engañar al Predictor de Saltos.

Cada procesador podemos pre-condicionarlo de manera diferente, así no explicaré ello, dado que es largo y sería muy tedioso.

Luego de que hayamos pre-condicionado al procesador le pediremos que ejecute.

if( offset < array1_size )
{
  array2[ array1[ offset ] * 512 ] ;  
}

Cuando esto llega el procesador va a darse cuenta de que array1_size no está en la caché, con el punto de no pararse a esperar que el valor venga desde la RAM a la cache (mientras el valor está en la cache leerlo pudiera ser 5 ciclos pero si no se encuentra y está en únicamente en la RAM serían 200 ciclos) el predictor de saltos deducirá que, al igual que veces anteriores, la condición
if( offset < array1_size ) es verdadera, entonces la ejecución especulativa irá a ejecutar:
array2[ array1[ offset ] * 512 ] ; 

En este paso, el procesador leerá la posición que resulta al ejecutar array1[ offset ] que se encuentra en la caché, comúnmente, no deberíamos poder leer esa dirección de memoria porque no le corresponde a nuestro programa, entonces ese valor es justamente el byte (8 bits), al cual llamaremos k que deseamos obtener, pero todavía, el valor no es nuestro, puesto que el procesador no ha aplicado los cambios, de igual forma el procesador calculará k * 512 y por ende, traerá la posición
array2[ k * 512 ], y recordamos que, para que el procesador pueda ejecutar operaciones tiene que leerlo de la cache, por la cual traerá esa posición a la cache, supongamos que el valor k sea 4, entonces el procesador traerá al cache
array2[4 * 512] , de seguro el procesador traerá un gran cumulo de sus vecinos y traiga ese valor y sus próximos 511 (no traerá el array entero porque es muy grande, por eso el 512), por si acaso. A este tiempo, array1_size llega desde la RAM y el procesador se da cuenta de que el predictor de saltos se equivocó.
Por lo tanto el procesador descartará todos los cambios, PERO, el estado actual de la caché se mantiene.

Entonces el truco viene aquí. 

Si efectuamos lecturas en los intervalos array2[ n * 512 ] , siendo n un valor entre 0~255 (un byte solamente puede albergar valores desde 0 a 255, un total de 256 valores) entonces las lecturas en todo n * 512 serán lentas dado que array2 NO está en la caché, pero, una parte del array, en efecto array2[ k * 512 ] que en este caso sería array2[ 4 * 512 ] ESTA en la caché, por lo tanto, la lectura a esa parte del array sería muy rápida.

Entonces el programa ahora solamente tiene que medir el tiempo, y se va a dar cuenta de que todas las lecturas en n fueron lentas, menos cuando fue igual a n = 4. Entonces deducimos que el valor secreto tiene que ser 4.

Así byte a byte se puede obtener datos de otros procesos.

Aunque tanto Spectre como Meltdown se basan en esto, Meltdown solo afecta a Intel por que Meltdown se basa en obtener datos directamente desde el Kernel, pero como los procesadores AMD miran si un programa del modo usuario está leyendo de modo kernel, entonces esto se descarta, aunque Intel también hace esto, el valor llega a la caché por la característica de
«Intel privilege escalation», ahora bien, Spectre hace lo mismo, pero en vez de leer memoria del kernel, lee memoria directamente de otro proceso en modo usuario, afectando por ello a todos los procesadores modernos.

Consecuencias y pronóstico:

A pesar de esto, no hay por qué alarmarse, ya se han tomado cartas en el asunto,con el parche, las operaciones I/O al parecer son las más afectadas, así como la virtualización, que suelen usar los programas emuladores. Pero para la mayoría de otras cosas, posiblemente no veamos cambios notables.

Debido al revuelo que ha producido el bug, las compañías primero quieren calmar a los usuarios mandando parches, pero luego vendrán mejoras que hará que tanto Meltdown, así como Spectre sean más inviables de usar, pero sin penalizar notablemente a los procesadores.

Acerca de mí DarkRoku12

Programador apasionado de su carrera, gustoso siempre de probar y jugar MMORPG, pero sobre todo, que le encanta pasar un buen rato con sus amigos.

Mira mis posts
Author DarkRoku12
Published
Categories Tecnología
Views 500

Comments

No Comments

Leave a Reply

Archivos