Qué son los Exception Levels en ARM y cómo funcionan

Última actualización: febrero 22, 2026
Autor: Isaac
  • Los Exception Levels de ARM (EL0–EL3) definen jerarquías de privilegio que controlan acceso a registros, memoria e interrupciones en AArch64.
  • El cambio de nivel solo se realiza mediante excepciones o la instrucción ERET, que usan registros como CurrentEL, SPSR_ELx y ELR_ELx.
  • AArch32 mantiene modos clásicos (User, IRQ, FIQ, Supervisor, etc.) con registros bancados, mientras AArch64 simplifica registros y banca solo SP y registros de excepción.
  • Diseñar un sistema embebido eficaz implica decidir qué lógica reside en cada EL para equilibrar simplicidad, seguridad y control del hardware.

Exception Levels en ARM

Los Exception Levels (EL) en ARM son uno de esos conceptos que a primera vista parecen puramente teóricos, pero que en cuanto empiezas a trastear con núcleos como Cortex-A53, AArch64 o sistemas embebidos modernos descubres que afectan directamente a cómo se arranca, cómo se protegen los recursos y cómo se gestionan las interrupciones. Si vienes de hacer DSP en arquitecturas propietarias o de trabajar con microcontroladores sencillos, es normal que al principio te preguntes por qué tanto lío con varios niveles de privilegio.

En este artículo vamos a desgranar con calma qué son los Exception Levels en ARM, cómo encajan en el modelo de modos, registros, estados de ejecución e interrupciones y, sobre todo, qué implicaciones reales tienen cuando diseñas un sistema: desde un Linux completo hasta un firmware mínimo para un procesador ARMv8 que solo ejecuta tu código. La idea es que, al terminar de leer, tengas claro por qué no siempre es buena idea “ir en EL3 a lo loco” y cuándo sí tiene sentido simplificar y quedarte en un único nivel privilegiado.

De ARMv4 a ARMv9: arquitectura, estados de ejecución e instrucciones

La arquitectura ARM ha ido evolucionando desde las primeras versiones como ARMv4, ARMv5 y ARMv6 hasta las familias ARMv8 y ARMv9. Cada salto de versión ha introducido cambios en el conjunto de instrucciones, en el soporte de extensiones (virtualización, seguridad, SIMD, Java, etc.) y, muy importante para este tema, en el modelo de privilegios y excepciones.

Como buen diseño RISC, los núcleos ARM se caracterizan por instrucciones sencillas y relativamente pocas, ejecutadas de forma muy rápida gracias a técnicas como el pipeline, la predicción de saltos o las cachés. A partir de ahí, ARM ha ido ampliando el repertorio de conjuntos de instrucciones especializados, que pueden cambiarse dinámicamente con solo tocar algunos bits en registros de estado.

En los procesadores anteriores a ARMv8, y en el estado de ejecución AArch32, podemos encontrar varios conjuntos de instrucciones alternativos que conviene tener en el radar:

  • ARM: instrucciones de 32 bits, el modo “clásico”. Siempre que se atiende una excepción o interrupción, la CPU pasa a este juego de instrucciones.
  • Thumb: instrucciones de 16 bits, con menos registros y opcodes, pensado para mejorar la densidad de código y ahorrar memoria.
  • Thumb-2: mezcla de instrucciones de 16 y 32 bits, apareció en ARMv6T2 y se popularizó tanto que muchos kernels modernos (incluido Linux) pueden compilarse para Thumb-2 para ganar tamaño sin perder tanto rendimiento.
  • Jazelle: extensión específica para acelerar la ejecución de bytecode Java.
  • SIMD/VFP y otras extensiones: orientadas a cálculo vectorial, coma flotante y seguridad.

Con la llegada de ARMv8 entran en juego los dos grandes estados de ejecución: AArch32 y AArch64. AArch32 mantiene compatibilidad con el mundo anterior (instrucciones de 32 bits y modos “clásicos”), mientras que AArch64 introduce un set de instrucciones de 64 bits y un nuevo modelo de registros y Exception Levels. Es posible pasar de un estado a otro mediante excepciones, lo que permite ejecutar, por ejemplo, un sistema operativo de 64 bits que corre aplicaciones legadas de 32 bits.

Modos de procesador en AArch32 y su relación con las excepciones

Antes de hablar de Exception Levels es importante entender que, en AArch32, ARM ya tenía un modelo de modos de procesador asociados a distintos tipos de excepciones y privilegios. Estos modos definen qué registros están visibles, qué nivel de acceso tiene el código y cómo se entra y sale de una rutina de servicio.

En AArch32, los modos típicos son:

  • User: modo de usuario, mínimo privilegio, sin capacidad para cambiar de modo directamente. Las aplicaciones de un sistema operativo se ejecutan aquí.
  • FIQ: modo al que se entra cuando se recibe una Fast Interrupt Request (interrupción rápida), con prioridad alta.
  • IRQ: modo de interrupción “normal”, se usa para la mayoría de las fuentes de interrupción.
  • Supervisor (SVC): similar a User en cuanto a conjunto de registros visibles, pero con mayor privilegio. Suele usarse para ejecutar llamadas al sistema.
  • Monitor: modo introducido con las extensiones de seguridad, para gestionar el cambio entre mundo seguro y no seguro.
  • Abort: modo al que se entra cuando hay errores de acceso de memoria, tanto de datos (Data Abort) como de instrucciones (Prefetch Abort).
  • Undefined: se usa cuando se ejecuta una instrucción inválida o no implementada.
  • System: modo con todos los privilegios, en el que típicamente corre el kernel del sistema operativo.

Todos estos modos, salvo User, se consideran modos privilegiados. Los cambios entre ellos se producen principalmente por la entrada de una excepción (por ejemplo, una interrupción, un fallo de memoria o una llamada software) y, en casos limitados, a través de instrucciones especiales que solo pueden usarse desde modos de mayor privilegio.

Exception Levels en ARMv8/AArch64: EL0, EL1, EL2 y EL3

Con ARMv8 y el estado AArch64, ARM reorganiza el modelo de privilegios alrededor de los Exception Levels (EL). En vez de tener muchos modos “especiales”, se definen cuatro niveles de privilegio numerados del 0 al 3, donde EL0 es el nivel menos privilegiado y EL3 el más alto. Sobre estos EL se superpone después la lógica de modos y estados.

La distribución típica de usos de cada EL en un sistema moderno es:

  • EL0: código de usuario, el nivel más restringido. Aquí corren las aplicaciones, procesos de usuario, drivers en espacio de usuario, etc.
  • EL1: código privilegiado del sistema operativo, por ejemplo el kernel de Linux o de otros SO. Tiene acceso a recursos sensibles, a la MMU de su nivel, etc.
  • EL2: nivel orientado a virtualización. Es donde se suele ejecutar el hipervisor que gestiona varias máquinas virtuales y controla en gran medida lo que hacen los kernels invitados en EL1.
  • EL3: nivel más alto, reservado habitualmente al Secure Monitor y al firmware seguro. Suele encargarse de funcionalidades como PSCI (Power State Coordination Interface) y de la cadena de arranque confiable.
  Usa Linux: ¡Experimenta un Windows Parecido con la Potencia de Linux!

Un detalle importante es que no todos los SoC ARM tienen por qué implementar los cuatro Exception Levels. En sistemas sencillos puede existir solo EL1 y EL0; en otros, EL3, EL1 y EL0, prescindiendo de EL2 si no se va a usar virtualización. Esto tiene sentido porque ARM se usa desde microcontroladores muy básicos hasta servidores de alto rendimiento, y no todas las plataformas necesitan la misma complejidad.

¿Qué hace a un EL “más privilegiado” que otro? A grandes rasgos, un nivel superior puede ver y modificar registros y recursos que los niveles inferiores ni siquiera pueden tocar. Por ejemplo, en AArch64 el código en EL2 puede modificar los registros de tablas de página TTBR0_EL1 y TTBR1_EL1, que gobiernan la traducción de direcciones para EL1 y EL0. Un kernel en EL1, en cambio, no puede acceder a los registros de configuración de EL2.

Además, los niveles superiores pueden configurar trampas: es decir, decidir que ciertas operaciones realizadas en un EL inferior generen una excepción que suba al EL superior. Así, un hipervisor en EL2 puede hacer que cualquier intento del kernel invitado en EL1 de tocar sus propias tablas de página provoque una trampa a EL2, donde se puede emular o validar la operación.

La gestión de interrupciones también está muy ligada a los Exception Levels. Los EL más altos deciden a qué nivel se enrutará una interrupción concreta y si se atiende directamente o se deja que baje a un nivel inferior. Esto es crítico para mantener el aislamiento entre máquinas virtuales o entre el mundo seguro y el no seguro.

Cambio de Exception Level: excepciones, retornos y el registro CurrentEL

En AArch64, ARM es muy claro: solo hay dos formas de cambiar de Exception Level. O bien se produce una excepción (interrupción, fallo, trampa, etc.), que hace que la CPU salte a un EL superior, o bien se ejecuta una instrucción de retorno de excepción, que permite bajar de nivel.

Las excepciones (incluyendo interrupciones) permiten que la CPU pase, por ejemplo, de EL0 a EL1, o de EL1 a EL2 o EL3, dependiendo de la configuración. Cuando esto ocurre, el procesador guarda automáticamente cierto contexto en registros especiales del EL de destino y salta a una tabla de vectores de excepción.

Para volver a un EL inferior se usa la instrucción ERET (Exception Return). Lo interesante es que ERET puede ejecutarse incluso aunque no haya habido una excepción real. Si se configuran correctamente los registros que indican el EL y la dirección de retorno, ERET actúa como un salto explícito bajando de nivel. Esto se usa, por ejemplo, cuando arrancas en EL3 y quieres pasar a ejecutar tu sistema operativo en EL1 o tu hipervisor en EL2.

El registro CurrentEL permite averiguar en qué nivel estás ejecutando código en cada momento. Es un registro de sistema de 64 bits, pero solo los bits 2 y 3 contienen el valor real del EL (codificado de forma desplazada). Para leerlo se usa la instrucción MRS, que copia su valor a un registro general, y después se compara. Por ejemplo, si su valor es 0b1000, significa que estás en EL2.

Un patrón típico de código de arranque en AArch64 es:

  • Leer CurrentEL con MRS.
  • Comparar el valor para ver si ya estás en el EL deseado (por ejemplo, EL2) o si vienes de un EL inferior o superior.
  • Si estás en EL3, preparar los registros de retorno y usar ERET para caer a EL2 o EL1.
  • Si ya estás en EL1 o EL0 y querías un EL superior, ya no puedes subir, así que solo puedes informar de error o limitarte a trabajar con lo que hay.

En muchos sistemas reales, el firmware de arranque (UEFI, bootloader propietario, etc.) ya se encarga de dejar la CPU en el EL apropiado antes de saltar al kernel o al hipervisor, así que ese código de comprobación se usa sobre todo en proyectos de sistema bare-metal o cuando se quiere aprovechar al máximo la virtualización.

Registros de propósito general y banking en AArch32 y AArch64

El modelo de registros también cambia de forma significativa entre AArch32 y AArch64, y esto afecta a cómo se manejan las excepciones y los cambios de modo o de EL. En AArch32 se suele hablar de 16 registros “visibles” (R0-R15) más los registros de estado, pero en realidad muchos de ellos están “bancados”: existen varias copias físicas a las que se accede según el modo de procesador.

En AArch32 encontramos:

  • R0-R12: registros de propósito general, normalmente compartidos entre varios modos salvo en el caso de FIQ, donde algunos se bancan para acelerar el manejo de interrupciones rápidas.
  • R13: usado como puntero de pila (SP), pero con versiones bancadas para modos como IRQ, FIQ, Supervisor, etc. Por ejemplo, en modo IRQ leer R13 devuelve SP_irq, mientras que en Supervisor da SP_svc.
  • R14: link register (LR), donde se guarda la dirección de retorno de llamadas y excepciones; también puede estar bancado según el modo.
  • R15: contador de programa (PC).
  • CPSR y SPSR: el Current Program Status Register y el Saved Program Status Register, donde se almacenan flags, bits de modo, estado de interrupciones y otros campos de control.

Este banking permite que, al entrar en una excepción, el procesador cambie de modo, use otro SP y LR sin tener que salvar inmediatamente todos los registros en la pila, lo que reduce la latencia en casos como FIQ. Por eso, en FIQ se bancan más registros que en IRQ: la idea es minimizar los push/pop y hacer el servicio lo más rápido posible.

En AArch64 el modelo cambia: tenemos 31 registros generales de 64 bits, X0-X30, donde X30 suele utilizarse como link register. En este caso no hay banking de registros generales: todos los niveles ven el mismo banco X0-X30. El banking se aplica solo a algunos registros especiales como los punteros de pila por nivel (SP_EL0, SP_EL1, SP_EL2, SP_EL3), los registros de enlace de excepción (ELR_ELx) y los Saved Program Status Register por EL (SPSR_ELx).

  ¿Cuál es la mejor luz para el auto?

Para facilitar el acceso a código AArch32 desde AArch64, ARM define una correspondencia entre los registros de 32 bits y los de 64 bits: los registros AArch32 se mapean a los 32 bits menos significativos de los registros X0-X30. La documentación oficial detalla esa tabla de correspondencias, donde se indica, por ejemplo, cómo se asocian los registros bancados de IRQ con distintos Xn.

Modelo de excepciones en ARM: qué es una excepción y tipos básicos

En ARM, cualquier evento que rompa el flujo secuencial normal de instrucciones se considera una excepción. Esto incluye desde una interrupción externa hasta un fallo de memoria, una instrucción inválida o incluso el propio reset al encender el dispositivo. Cada tipo de excepción está bien definido e implica un cambio de modo o de Exception Level, guardar cierta información y saltar a una dirección de vector concreta.

En el mundo AArch32 podemos destacar algunas excepciones típicas:

  • Reset: se produce al encender o reiniciar; la CPU entra en modo Supervisor y empieza a ejecutar en la dirección 0x00 (o la base configurada).
  • Undefined Instruction: se lanza cuando se ejecuta una instrucción no válida o no soportada; el procesador entra en modo Undefined y salta al vector 0x04.
  • Supervisor Call (SVC/SWI): normalmente generada por una instrucción de software para hacer llamadas al sistema; entra en modo Supervisor y usa el vector 0x08.
  • Secure Monitor Call (SMC/SMI): similar a SVC pero orientada a la capa segura; entra en modo Monitor con su propio vector.
  • Prefetch Abort: error al intentar obtener instrucciones desde una dirección inválida o protegida; entra en modo Abort y salta al vector 0x0C.
  • Data Abort: error de acceso a datos (lectura o escritura de una dirección ilegal); entra también en modo Abort y usa el vector 0x10.
  • IRQ: interrupción estándar; entra en modo IRQ con vector 0x18.
  • FIQ: interrupción rápida; entra en modo FIQ con vector 0x1C.

Cuando se produce cualquiera de estas excepciones, el hardware realiza una secuencia de pasos bien definida:

  • El contenido de CPSR se copia al SPSR del modo al que se entra.
  • Se cambia el modo de procesador al asociado a esa excepción (Supervisor, IRQ, FIQ, etc.).
  • Se fuerza el estado de instrucciones a ARM de 32 bits, aunque se estuviese ejecutando Thumb o Thumb-2.
  • Se guarda la dirección de retorno en el link register del nuevo modo.
  • El PC se carga con la dirección del vector correspondiente.

A partir de ahí, el código de servicio de la excepción puede hacer lo que necesite, siempre teniendo cuidado de no corromper registros necesarios y de gestionar bien las anidaciones de excepciones. Es frecuente salvar en la pila los registros de trabajo al entrar y restaurarlos antes de volver mediante instrucciones como LDMFD^ (load multiple con actualización y cambio de estado) o secuencias equivalentes.

Para salir de la excepción y reanudar el código interrumpido, se restauran los registros desde la pila y se vuelve al contexto anterior usando la dirección guardada en LR y el SPSR que, al copiarse de vuelta a CPSR, restaura el modo y el estado original.

Excepciones en AArch64: ESR, SPSR_ELx, ELR_ELx y ERET

En el mundo AArch64 el concepto es similar, pero la implementación es un poco diferente y más rica. Se definen múltiples clases de excepción que representan la causa del salto: ejecución ilegal, WFI/WFE, alineación incorrecta del PC, fallos de memoria, trampas de virtualización, etc. La CPU escribe la información de causa detallada en el registro ESR (Exception Syndrome Register) del EL al que se salta.

Cuando se entra en una excepción en AArch64 ocurre algo equivalente a esto:

  • Se guarda el estado del procesador en SPSR_ELx del Exception Level de destino.
  • Se escribe en ELR_ELx la dirección de retorno preferida (normalmente la instrucción siguiente a la que generó la excepción o un valor ajustado).
  • Se cambia al EL de destino y se salta a la dirección indicada por la tabla de vectores de ese EL.
  • Se selecciona el puntero de pila asociado a ese EL (por ejemplo, SP_EL1 o SP_EL2) según la configuración en SPSR.

Para devolver el control al contexto anterior se usa, como comentábamos, la instrucción ERET. Al ejecutarla:

  • El PC se carga con el valor almacenado en ELR_ELx.
  • El PSTATE (similar al CPSR viejo) se restaura usando SPSR_ELx, lo que recupera el EL previo, los flags y los bits de interrupciones.

Además, ERET no solo sirve para “deshacer” una excepción real. Si desde EL3 preparamos manualmente SPSR_EL3 y ELR_EL3 con un EL objetivo más bajo (por ejemplo EL2) y una dirección de arranque, un ERET nos deja ejecutando en EL2 como si viniéramos de una excepción, aunque en realidad estemos usando el mecanismo como un salto controlado de inicialización.

Punteros de pila por Exception Level y configuración de SP

Otro punto clave a la hora de jugar con los Exception Levels es cómo se maneja la pila. En AArch64 existen punteros de pila específicos por nivel: SP_EL0, SP_EL1, SP_EL2 y SP_EL3. Pero el registro que usas en las instrucciones normales es siempre sp; el hardware se encarga de mapearlo internamente al SP_ELx apropiado.

ARM define dos modos de uso del puntero de pila:

  • Un modo en el que todos los EL usan SP_EL0, de manera que la pila es compartida entre niveles.
  • Otro en el que cada EL usa su SP propio, de modo que sp en EL2 apunta a SP_EL2, sp en EL1 a SP_EL1, etc.

Esta elección se realiza mediante determinados bits en el SPSR cuando se hace el salto con ERET. Por ejemplo, al preparar SPSR_EL3 podemos indicar si en EL2 queremos usar SP_EL0 o SP_EL2 como puntero de pila. En muchos diseños de sistema tiene más sentido darle a cada EL su pila independiente, para aislar contextos y simplificar el razonamiento.

  Solución: ¿Cómo arreglar Windows 7 si no reconoce dispositivos USB?

Un patrón típico cuando arrancas en EL3 y quieres bajar a EL2 con pila propia es:

  • Guardar en un registro general (por ejemplo X0) el valor actual de sp.
  • Copiar este valor a SP_EL2 con MSR, de forma que, cuando bajes a EL2 y sp se mapee a SP_EL2, ya habrá una pila válida.
  • Configurar SPSR_EL3 para seleccionar EL2 como destino y usar SP_EL2 en lugar de SP_EL0.
  • Ejecutar ERET para hacer efectivo el cambio.

Con esto, el código que arranca en EL2 ya puede usar la pila sin preocuparse de quién la puso en su sitio. De forma análoga, al configurar SPSR también se puede decidir si las interrupciones estarán habilitadas o enmascaradas al llegar al nuevo EL, a través de los bits específicos que controlan la generación de excepciones por IRQ, FIQ, SError, etc.

Máscara de interrupciones, SPSR y límites al cambiar de nivel

Los registros SPSR_ELx no sólo guardan flags de condición y el EL de retorno; también contienen máscaras de interrupciones y otros bits de control. En particular, hay bits que permiten inhibir temporalmente varios tipos de excepción mientras se ejecuta código crítico que aún no está preparado para gestionarlas.

Por ejemplo, al preparar una transición de EL3 a EL2, un firmware puede:

  • Poner a 1 los bits que bloquean las interrupciones de varios tipos, de modo que, al entrar en EL2, no salte nada hasta que el código de arranque configure adecuadamente el controlador de interrupciones.
  • Elegir el EL objetivo (EL0-EL2) y el tipo de pila que se va a usar.
  • Dejar el resto de flags en 0 si no se necesitan estados especiales.

También hay restricciones interesantes: desde SPSR_EL2 solo se puede “volver” a EL0-EL2, pero no a EL3; desde SPSR_EL1, únicamente a EL0-EL1. Es decir, los niveles inferiores no pueden “escalar” a los superiores usando ERET: solo pueden bajar al nivel que tienen permitido. Esto es clave para mantener la jerarquía de privilegios.

En código ensamblador suele verse cómo se construye el valor de SPSR en varios pasos con instrucciones como ORR sobre un registro inicializado a cero (con MOV Xn, XZR). La razón es que el conjunto de bits inmediatos que se pueden codificar en una sola instrucción ORR es limitado por el formato binario de la instrucción, así que a veces se necesitan dos ORR para poner a 1 todos los bits requeridos (por ejemplo, primero las máscaras de interrupción, luego el campo de EL y modo de pila).

¿Por qué no quedarse siempre en EL3 en un sistema embebido?

Una duda muy habitual cuando uno viene del mundo del DSP o de microcontroladores “desnudos” es: si mi firmware se ejecuta solo, sin sistema operativo complejo, ¿para qué quiero andar jugando con múltiples niveles de excepción? ¿No sería más sencillo correr todo el código directamente en EL3, con el máximo privilegio, y olvidarme?

La tentación de “quedarse en EL3 toda la vida” es comprensible, sobre todo si el proyecto es un diseño embebido muy específico donde controlas todo el software que va a correr. Sin embargo, en la práctica hay varios motivos de peso para no mezclarlo todo en el nivel máximo y, al menos, separar la capa de firmware/monitor del código de aplicación o de kernel ligero:

  • Seguridad y robustez: si todo se ejecuta con el máximo privilegio, cualquier bug en el código DSP o de aplicación puede comprometer toda la plataforma (configuración de memoria, supervisión de energía, acceso a claves, etc.). Aunque el sistema sea “cerrado”, separar funciones críticas en un EL superior reduce el impacto de errores.
  • Control del hardware sensible: funciones como PSCI, gestión de estados de energía, transiciones entre mundo seguro y no seguro, o incluso algunos mecanismos de arranque, suelen estar pensadas para correr en EL3. Mantener esa lógica separada simplifica tanto la auditoría como las actualizaciones.
  • Posible reutilización futura: puede que hoy tu proyecto no use virtualización ni necesite un sistema operativo completo, pero si en el futuro quieres ejecutar varios entornos aislados (por ejemplo, una capa de control y otra de procesamiento de audio) tener una arquitectura ya pensada por niveles te facilita mucho los cambios.
  • Simplicidad de desarrollo: paradójicamente, un diseño con “todo en EL3” puede volverse más difícil de mantener. Con un esquema claro de: firmware en EL3, posible hipervisor o monitor en EL2, y aplicación o kernel en EL1/EL0, es más sencillo razonar sobre quién toca qué, quién configura las tablas de página o quién recibe qué interrupciones.

Dicho esto, en algunos sistemas embebidos reducidos es legítimo arrancar directamente en EL1 o incluso en EL0, sin usar EL2 o EL3 más allá de la etapa inicial de boot proporcionada por el fabricante. En un diseño DSP minimalista para Cortex-A53, por ejemplo, podrías querer tener un “mini kernel” en EL1 que gestione interrupciones de audio, memoria y planificación, y ejecutar tu bucle DSP en EL0 con restricciones mínimas pero suficientes para evitar que rompa el sistema por un despiste.

En resumen, la pregunta no suele ser “¿puedo quedarme en EL3?”, sino más bien “qué capas de mi diseño necesitan de verdad privilegios máximos y cuáles no”. Aunque un proyecto muy de nicho pueda funcionar todo en un solo EL, aprovechar la estructura de ARM te ayuda a construir sistemas más limpios, fáciles de depurar y más seguros a largo plazo.

Todo este modelo de Exception Levels, modos de procesador, banking de registros y manejo de excepciones no es solo una curiosidad del manual: es la base que permite que ARM escale desde pequeños sistemas bare-metal hasta plataformas con hipervisores, mundos seguro/no seguro y sistemas operativos complejos. Entenderlo bien, aunque luego decidas usar solo una parte, te da mucha más libertad para elegir la arquitectura de tu software sin ir a ciegas.

ciberseguridad general
Artículo relacionado:
Ciberseguridad general: claves, ámbitos y soluciones que debes conocer