Armv8 아키텍처에서 익셉션 레벨은 어떻게 변경될까요? 크게 3가지 방식으로 익셉션 레벨이 변경됩니다. 

 SVC, HVC, SMC 명령어 실행
 IRQ/FIQ 인터럽트 발생
 ERET 명령어 실행

다음 표를 보면 익셉션 레벨로 진입하는 방법을 확인할 수 있습니다. 

 

표 6.3 Armv8 아키텍처에서 익셉션 레벨로 진입하는 방법


먼저 유저 애플리케이션이 실행되는 EL0에서 진입되는 익셉션 레벨을 알아봅시다.

 

EL0에서 진입하는 익셉션 레벨


EL0에서 EL1으로 진입하는 방법은 크게 3가지로 분류할 수 있습니다. 

첫 번째로는 소프트웨어적으로 EL1에 진입하는 방법입니다. EL0에서 SVC 명령어를 실행하면 EL0 Synchronous 익셉션이 유발되면서 EL1으로 진입합니다. 이런 동작을 운영체제에서는 시스템 콜이라고 합니다. 

또한 EL0는 비특권 레벨(Unprivileged Level)로 실행되므로 시스템 메모리 공간에 직접 접근하거나 인터럽트와 같은 하드웨어를 설정하는 데 제약이 있어 시스템 콜을 통해 하드웨어 리소스에 접근할 수 있는 EL1으로 스위칭됩니다. 운영체제의 커널을 통해 시스템 리소스를 설정하는 방식입니다.

두 번째로는 메모리 어보트를 유발하는 명령어를 EL0에서 실행하면 EL0 Synchronous 익셉션이 유발되면서 EL1으로 진입합니다. 일반적으로 EL0에서 실행되는 유저 애플리케이션이 메모리 어보트를 유발하는 명령어를 실행하면 EL1으로 익셉션 레벨을 변경한 다음, 운영체제 커널은 해당 프로세스를 종료하는 동작을 수행합니다. 다른 관점으로 EL1으로 진입하는 두 번째 방식은 소프트웨어적으로 논리적인 오류가 있는 어셈블리 명령어를 실행하는 것입니다.

[정보] 메모리 어보트는 언제 유발될까?

NULL 포인터 익셉션을 유발하는 코드를 작성하면 메모리 어보트 타입 익셉션이 유발됩니다.


세 번째로는, EL0에서 소프트웨어 루틴이 실행되다가 'IRQ 인터럽트' 익셉션이 유발되면 EL1으로 진입합니다. 달리 말하면 EL0에서 유저 애플리케이션이 실행되다가 인터럽트가 발생하면 EL1으로 진입합니다. 세부 동작은 다음과 같이 분류할 수 있습니다.

1. EL0에서 유저 애플리케이션 실행
2. IRQ 인터럽트 익셉션이 유발됨
3. EL1으로 스위칭
4. EL1에 존재하는 익셉션 벡터 주소로 프로그램 카운터를 분기

여기까지 EL0에서 EL1으로 진입하는 3가지 방식을 알아봤습니다. 

 


[중요] EL0에서 EL1으로 진입하는 동작 방식과 디버깅
사실 실전 프로젝트에서 EL0에서 EL1으로 진입하는 동작 방식을 알면 유저 애플리케이션의 성능 이슈를 디버깅할 때 도움이 됩니다. 유저 애플리케이션을 구성하는 코드를 아무리 열심히  리뷰해도 그 원인을 파악하기가 어려운 상황에 종종 직면합니다. 이럴 때 다음과 같이 시스템 소프트웨어 관점으로 문제를 분석할 수 있습니다.

 

 

 시스템 콜이 발생한 횟수
 유저 애플리케이션이 실행되다가 인터럽트를 처리하는 횟수

또한 전체 소프트웨어 스택의 실행 흐름을 파악하려면 익셉션 레벨로 진입하는 방식을 잘 알면 좋습니다.

EL1에서 진입하는 익셉션 레벨

 

이번에는 EL1에서 EL2로 진입하는 방법을 알아봅시다. EL1에서 'HVC' 명령어를 실행하면 EL1에서 EL2로 진입합니다. EL1에서는 게스트 OS(운영체제 커널)가 구동되는데, 게스트 OS가 여러 게스트 OS의 리소스를 관리하는 하이퍼바이저에게 서비스를 요청할 때 EL2로 진입합니다.

이어서 EL1에서 시큐어 모니터 콜을 유발하는 'SMC' 명령어를 실행하면 EL3로 진입해 모니터 모드로 진입합니다. EL3 모니터 모드는 논 시큐어 상태에서 시큐어 상태로 이동할 때 문지기(게이트키퍼)와 같은 역할을 수행합니다.  

이번 절에서는 Armv8 아키텍처의 익셉션 레벨을 살펴봤습니다. 이어지는 절에서 익셉션 레벨과 관련된 레지스터에 대해 알아봅시다. 

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>
 
 

 

 

Armv8 아키텍처는 PL0 ~ PL3까지 4개의 privilege level(접근 권한)을 정의합니다.  privilege levels은 Armv7 아키텍처에서 다룬 내용과 거의 유사합니다. 

[중요] Privilege level과 유저 애플리케이션

privilege levels은 Arm 아키텍처에서만 지원할까요? 그렇지는 않습니다.

x86을 포함한 대부분의 CPU 아키텍처는 privilege levels과 같은 기능을 지원합니다. 그 이유는 무엇일까요? 주로 리눅스와 같은 범용 운영체제를 설계할 때 시스템에 결함을 일으킬 수 있는 유저 애플리케이션으로부터 시스템을 보호하기 위해서입니다. 리눅스 커널과 같은 운영체제 커널은 privilege level에서 실행되며 privilege level에서만 주요 시스템 설정(시스템 레지스터, 인터럽트, 캐시 접근)을 할 수 있게 권한을 부여합니다. 유저 애플리케이션이 실행될 때는 privilege level 레벨이 아닌 unprivileged level로 실행해, 시스템의 세부 설정을 할 수 없게 설계합니다.

이처럼 privilege level 레벨로 권한을 부여하는 이유는 무엇일까요? 운영체제 커널의 세부 동작을 잘 모르는 유저 애플리케이션 개발자가 privilege level이 없는 환경에서 애플리케이션을 개발한다고 가정해봅시다. 유저 애플리케이션에서 메모리를 제대로 관리하면 괜찮겠지만 실수로 코드를 잘못 작성해서 커널 자료구조나 함수가 위치한 메모리 공간을 오염시키면 어떻게 될까요? 혹은 해커들이 고의로 시스템을 오동작시키기 위한 명령어를 실행하는 애플리케이션을 제작하면 어떤 결과를 초래할까요? 커널 패닉으로 시스템이 리부팅되거나 예상치 못한 시스템 오동작이 일어날 가능성이 있습니다.

따라서 유저 애플리케이션은 unprivileged level로 실행되도록 시스템을 설계합니다. 즉, 유저 애플리케이션에서 시스템 메모리 공간에 직접 접근하지 못하게 제약을 둡니다. 만약 유저 애플리케이션에서 시스템 메모리에 접근을 해야 하는 상황이라면 어떤 방법을 사용하면 될까요? 시스템 콜을 통해 privilege level로 실행되는 운영체제 커널로 이동해 실행 흐름을 스위칭한 다음 메모리에 접근하도록 시스템을 설계할 수 있습니다. 

  

익셉션 레벨과 Privilege Level


EL0에서는 유저 애플리케이션이 unprivileged 레벨 혹은 PL0로 실행됩니다. EL0에서 소프트웨어(유저 애플리케이션)가 실행되면 PL0 권한으로 실행되는데 다음과 같은 특징을 지닙니다.

 EL0에서는 인터럽트, MMU, 캐시 기능을 직접 설정할 수 없다.
 EL0에서는 메모리 접근 권한에 제약이 있다.
 IRQ나 MMU를 설정할 수 없다.
 EL0에서는 시스템 레지스터를 직접 설정할 수 없다. 

만약 EL0에서 시스템 레지스터나 메모리 주소와 하드웨어 리소스를 직접 설정하면 익셉션이 유발됩니다. 

이어서 운영체제의 커널이 구동되는 EL1은 PL1 권한으로 실행되는 익셉션 레벨입니다. EL1에서 구동되는 커널에서는 인터럽트, MMU, 캐시와 같은 하드웨어를 직접 설정할 수 있습니다. EL1은 EL0보다 privilege level이 높으므로 EL0에서 실행되는 코드나 데이터에 접근할 수 있습니다.

PL2는 EL2에게 부여되는 권한 레벨로, EL1으로 실행되는 게스트 OS의 시스템 리소스에 접근할 수 있습니다. 일반적으로 EL2에서 하이퍼바이저가 실행되며, 하이퍼바이저는 PL2 권한 레벨로 실행됩니다. EL2는 EL0 혹은 EL1보다 privilege level이 높아 EL0 혹은 EL1에서 실행되는 코드나 데이터에 직접 접근할 수 있습니다. 

PL2 권한이 있는 EL2는 자신보다 낮은 EL1(PL1)에서 실행되는 각각의 게스트 OS에서 설정된 레지스터에 접근할 수 있습니다. 이를 통해 EL2에서 실행되는 하이퍼바이저는 2개 이상의 게스트 OS의 리소스를 관리할 수 있습니다.


[용어] 하이퍼바이저란?

익셉션 레벨에 대한 설명을 하이퍼바이저 같은 낯선 용어와 함께 읽으면 무슨 내용인지 이해하기 어려울 수 있습니다. 익셉션 레벨에서 EL2를 설명하면 하이퍼바이저라는 용어가 등장합니다. 하이퍼바이저는 데스크톱 PC에서 윈도우와 리눅스를 사용하듯이, 2개 이상의 운영체제를 동시에 실행할 수 있는 아키텍처 혹은 플랫폼입니다. 

다음 그림을 보면 EL2에서 하이퍼바이저가 실행됩니다. 

 


그림 6.1 EL2에서 실행되는 하이퍼바이저 

하이퍼바이저에 대한 자세한 설명은 ‘16장. 가상화’를 참고하세요.

익셉션 레벨별로 접근할 수 있는 레지스터와 Privilege Level


이번에는 익셉션 레벨별로 접근할 수 있는 레지스터에 대해 알아봅시다. 이번 절에서 알아봤듯이 익셉션 레벨은 privilege level과 비례합니다. EL0는 PL0, EL1은 PL1 실행 권한을 지닙니다.

그런데 각 익셉션 레벨에서 접근할 수 있는 레지스터 범위가 한정돼 있습니다. EL1에서는 EL0에서 설정된 레지스터에 접근할 수 있고, EL2에서는 EL1, EL0에서 사용되는 레지스터에 모두 접근할 수 있습니다. 반대로 EL1에서는 EL2에서만 사용되는 레지스터에 접근할 수 없고 EL0에서는 EL1의 레지스터에 접근할 수 없습니다. 자신보다 높은 익셉션 레벨에서 정의된 레지스터에 접근하면 엑세스 권한 위반(Access Violation)으로 감지해 폴트를 유발합니다. 

이처럼 각 익셉션 레벨에 부여된 privilege level 권한 레벨만큼 레지스터에 접근할 수 있는 권한이 있습니다. 이런 특징을 활용해 보안 관점에서 시스템의 리소스를 체계적으로 관리할 수 있습니다.

다음은 EL2에서 실행되는 XEN 하이퍼바이저에서 EL1 레지스터에 직접 접근하는 어셈블리 명령어입니다. 

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/arch/arm/arm64/entry.S
01 0000000000269ba4 <guest_sync_slowpath>:
02  0x269ba4:    0xd10083ff     sub    sp, sp, #0x20
03  0x269ba8:    0xa9bf77fc     stp    x28, x29, [sp, #-16]!
...
04  0x269c00:    0xd53c4116     mrs    x22, sp_el1
05  0x269c04:    0xd5384037     mrs    x23, elr_el1
06  0x269c08:    0xa9005eb6     stp    x22, x23, [x21]
...
07  0x269c68:    0xa9405eb6     ldp    x22, x23, [x21]
08  0x269c6c:    0xd51c4116     msr    sp_el1, x22
09  0x269c70:    0xd5184037     msr    elr_el1, x23
10  0x269c74:    0x14000174     b    26a244 <return_from_trap>

[정보] guest_sync_slowpath 함수 소개

guest_sync_slowpath 함수는 EL2에서 실행되는 XEN 하이퍼바이저에서 게스트 OS를 관리하는 목적으로 구현된 함수입니다. guest_sync_slowpath 함수는 게스트 OS에서 하이퍼바이저에게 어떤 서비스를 요청할 때 서브루틴으로 호출됩니다. guest_sync 레이블을 통해 guest_sync_slowpath 함수가 호출되는데, XEN 하이퍼바이저 관점의 더 자세한 코드 분석은 15.5 절 'XEN 하이퍼바이저 코드 리뷰'를 참고하세요.


앞에서 소개한 코드는 어느 익셉션 레벨에서 실행될까요? 하이퍼바이저가 실행되는 EL2입니다. 이 점을 염두에 둡시다.

04 ~ 05번째 줄은 sp_el1과 elr_el1 레지스터를 읽는 명령어이고, 08 ~ 09번째 줄은 sp_el1과 elr_el1 레지스터를 설정하는 명령어입니다. 여기서 sp_el1은 EL1에서 접근하는 스택 포인터 레지스터, elr_el1은 EL1에서 접근하는 익셉션 링크 레지스터입니다. 그런데 PL2 권한으로 실행되는 EL2에서는 EL1에서 설정된 sp_el1 혹은 elr_el1 레지스터에 접근할 수 있습니다.

 

PL3 권한으로 실행되는 EL3

마지막으로 PL3는 EL3에 부여되는 권한 레벨로, 가장 높은 특권 레벨(highest privilege level)이라고도 합니다. 시스템 리소스를 모두 설정할 수 있고, 익셉션 레벨에 존재하는 모든 레지스터에 엑세스할 수 있습니다. 

[정보] 시스템이 부팅할 때 가장 먼저 어떤 익셉션 레벨을 설정할까?

시스템에 전원을 인가하면 가장 먼저 부트로더가 실행됩니다. 시스템이 부팅하면 가장 먼저 실행되는 소프트웨어인 부트로더는 EL3로 익셉션 레벨을 설정합니다. 실행 권한이 가장 높은 EL3에서 시스템을 구성하는 주요 기능(MMU, 시스템 레지스터)을 설정합니다. 

부트로더에서 익셉션 레벨을 EL3로 설정하는 다른 이유는 EL2에서 EL3로 진입하려면 익셉션을 유발하면서 EL2에서 실행한 정보를 담고 있는 레지스터 세트를 어딘가에 백업하는 추가 루틴이 실행되어야 하기 때문입니다. 결국 소프트웨어 복잡도가 늘어납니다. 그래서 EL3에서 시스템 리소스를 충분히 설정한 다음 EL3에서 EL2로 익셉션 레벨을 변경합니다. 이어서 EL2에서 EL1으로 익셉션 레벨을 변경하면서 부팅이 진행됩니다.  

시스템이 부팅하는 과정에서 실행되는 소프트웨어 스택은 SoC 칩마다 다르므로 구체적인 내용은 SoC에서 배포한 데이터 시트 문서를 참고하세요.   

익셉션 레벨과 Privilege Level 정리

지금까지 설명한 익셉션 레벨별 특권 레벨은 다음 표로 정리할 수 있습니다.

 

표 6.2 Armv8 아키텍처의 익셉션 레벨과 특권 레벨
 


익셉션 레벨과 권한 레벨은 비례하고 EL0는 가장 낮은 특권 레벨, EL3는 가장 높은 특권 레벨인 익셉션 레벨입니다.

다음 그림에서 익셉션 레벨과 특권 레벨의 관계를 확인할 수 있습니다. 

 


그림 6.2 익셉션 레벨과 특권 레벨

그림을 보면 익셉션 레벨과 Privilege level이 비례한다는 사실을 알 수 있습니다.  

 

 

익셉션 레벨의 실행 흐름과 소프트웨어 스택

 

이번에는 다음 그림을 보면서 각 익셉션 레벨에서 실행되는 소프트웨어 스택과 실행 흐름을 알아봅시다.  


 
그림 6.3 Armv8 익셉션 레벨과 주요 실행 흐름

그림 6.3은 멀티 운영체제가 동시다발적으로 실행되는 하이퍼바이저와 트러스트존 기능이 구현된 아키텍처입니다. 각 익셉션 레벨에서 어떤 소프트웨어가 실행되는지 살펴봅시다.

먼저 그림의 가장 윗부분에 보이는 EL0부터 분석합시다. EL0는 EL0를 나타내며 유저 애플리케이션이 실행됩니다. 이어서 EL0의 아랫부분에 있는 EL1을 보겠습니다. EL1에는 운영체제의 커널이 구동되며, EL1은 PL1 권한으로 실행됩니다. EL1의 아래에는 EL2가 보입니다. 

EL2에는 2개 이상 게스트 OS를 제어하는 하이퍼바이저가 구동되며, EL2는 PL2 권한으로 실행됩니다. 그림에서는 게스트 OS1과 게스트 OS2가 존재하는데, 하이퍼바이저는 2개 이상의 게스트 OS가 동시다발적으로 실행되도록 게스트 OS의 리소스(예: VCPU, 가상 인터럽트)를 관리하는 역할을 수행합니다. 

마지막으로 EL2 아래에 있는 EL3를 봅시다. EL3는 가장 높은 특권 레벨(highest privilege) 권한, 즉 시스템의 모든 기능(레지스터, 메모리, 캐시)을 설정할 수 있는 가장 높은 권한으로 실행됩니다. 보통 부팅 과정에서 실행되는 부트로더에서 EL3로 설정한 다음에 시스템의 여러 리소스(메모리, 시스템 레지스터)를 설정합니다. 
 
여기까지 Armv8 아키텍처의 근간인 익셉션 레벨을 살펴봤습니다. 이번 절에서 소개한 익셉션 레벨은 Armv8 아키텍처에서 가장 중요한 내용이니 잘 기억해 둡시다. 이어지는 절에서는 익셉션 레벨로 어떻게 진입하는지 살펴보겠습니다.

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>
 
 

 

 

익셉션 레벨은 ‘익셉션’과 ‘레벨’의 합성어로 익셉션의 레벨로 해석할 수 있습니다. 그런데 ‘익셉션 레벨’은 ‘익셉션의 레벨’과는 조금 다른 의미를 지니는 용어입니다. 먼저 익셉션 레벨을 소개합니다.

다음과 같은 Arm 스펙 문서의 내용을 보면서 익셉션 레벨에 대해 알아봅시다.

출처: Arm Architecture Reference Manual Armv8, for A-profile architecture https://developer.arm.com/documentation/ddi0487/gb 
D1.1 Exception levels

The ARMv8-A architecture defines a set of Exception levels, EL0 to EL3, where:
• If ELn is the Exception level, increased values of n indicate increased software
   execution privilege.
  • Execution at EL0 is called unprivileged execution.
  • EL2 provides support for virtualization of Non-secure operation.
• EL3 provides support for switching between two Security states, Secure state 
  and Non-secure state.

An implementation might not include all of the Exception levels. All implementations must include EL0 and EL1. EL2 and EL3 are optional.

스펙 문서의 내용은 다음과 같이 정리할 수 있습니다.

 

   
ARMv8-A 아키텍처는 EL0에서 EL3까지 익셉션 레벨을 다음과 같이 정의한다.

 ELn을 익셉션 레벨이라 하며, n은 PL(Privilege Level)을 뜻한다.
 EL0는 Unprivileged Execution이다.
 EL2는 논 시큐어 동작에서 가상화를 지원하기 위한 익셉션 레벨이다.
 EL3는 논 시큐어 상태와 시큐어 상태를 전환하는 시큐어 모니터가 실행되는 익셉션 레벨이다.
 (운영체제나 RTOS를 구현할 때) 모든 익셉션 레벨을 구현해야 할 필요는 없다. EL0와 EL1은 필수 구현 사항이고, EL2, EL3는 옵션으로 반드시 구현할 필요는 없다.

 



앞에서 소개한 스펙 문서의 내용은 명료하게 익셉션 레벨을 설명합니다. 하지만 입문자는 위 내용을 읽고 익셉션 레벨을 바로 이해하기 어렵습니다. 이제부터 익셉션 레벨에 대해 더 자세히 알아봅시다.

 

익셉션 레벨 추가 분석


앞에서도 다룬 바 있지만 복습하는 차원에서 설명하자면 먼저 EL은 익셉션 레벨(Exception Level)의 약자입니다. 사실 익셉션 레벨은 ELn 혹은 ELx로 명시하며, 'EL'의 접미사로 있는 n이나 x는 익셉션 레벨의 단계를 뜻합니다. 여기서 n이나 x 값의 범위는 0 ~ 3입니다.

EL0는 익셉션 레벨 0, EL1은 익셉션 레벨 1이라고 부르며 익셉션 레벨은 EL0, EL1, EL2, 그리고 EL3까지 존재합니다.   

n이나 x 값의 범위는 0과 3 사이인데, 이 정숫값은 PL(Privilege Level)과 비례합니다. EL0는 PL0이고 EL1은 PL1입니다. EL0는 비특권 레벨(Unprivileged Level) 혹은 PL0로 실행되는 익셉션 레벨입니다. PL0 혹은 비특권 레벨에서는 시스템 메모리에 직접 접근할 수 없고, MMU나 인터럽트와 같은 시스템 설정은 할 수 없는 권한을 지닌 익셉션 레벨입니다. 

또한 스펙 문서에서 "EL0와 EL1은 필수 구현 사항이고, EL2, EL3는 반드시 구현할 필요는 없다"라고 명시한 부분이 있습니다. 일반적으로 하나의 운영체제만 실행되는 시스템에서는 유저 애플리케이션이 구동되는 EL0와 운영체제 커널이 구동되는 EL1만 구현하면 됩니다.

게스트 OS 혹은 가상 머신을 실행하는 하이퍼바이저에서는 EL2는 구현해야 하고, 트러스트존을 활용해 시큐어 운영체제(Secure OS)를 사용하는 경우에만 EL3를 구현해야 합니다.

 


[정보] 트러스트존과 시큐어 운영체제
트러스트존은 CPU 내에 신뢰할 수 있는 실행 영역을 확보하기 위한 보안 아키텍처입니다. 시큐어 운영체제는 트러스트존에서 정의된 시큐어 월드에서 실행되는 보안 소프트웨어입니다. 자세한 내용은 15장 ‘트러스트존’을 참고하세요.
달리 보면 EL2와 EL3는 추가로 구현되는 옵션으로 볼 수 있습니다. 그런데 "EL2, EL3는 반드시 구현할 필요는 없다"라는 내용을 읽으면 다음과 같은 의문이 생깁니다.

 


여기서 EL2와 EL3를 구현해야 한다는 것은 무엇을 의미할까?

먼저 익셉션 레벨을 운영체제나 RTOS에서 사용하려면 ELx(익셉션 레벨)별로 시스템 설정을 해야 합니다. 이를 위해 기본적으로 다음과 같은 기능을 구현해야 합니다.

 각 익셉션 레벨별로 익셉션 핸들러를 구현
 각 익셉션 레벨별로 사용되는 스택을 설정
 익셉션 핸들러의 베이스 주소를 VBAR_ELx 레지스터를 통해 설정
 관련 시스템 레지스터 및 MMU 설정

이 같은 기본 기능을 구현해야 EL1에서 EL2나 EL3로 실행 흐름이 이어집니다.

운영체제에서는 익셉션 레벨이 어떻게 활용될까?

이어서 익셉션 레벨이 운영체제에서 어떻게 활용되는지 다음 Arm 스펙 문서를 보면서 알아 봅시다.

 



출처: Arm Architecture Reference Manual Armv8, for A-profile architecture https://developer.arm.com/documentation/ddi0487/gb 
D1.1.1 Typical Exception level usage model

The architecture does not specify what software uses which Exception level. Such choices are outside the scope of the architecture. However, the following is a common usage model for the Exception levels:

 EL0: Applications.
 EL1: OS kernel and associated functions that are typically described as privileged.
 EL2: Hypervisor.
 EL3: Secure monitor.

 



스펙 문서의 내용은 다음과 같이 요약할 수 있습니다.


Arm 아키텍처는 SW(운영체제)에서 어떤 익셉션 레벨을 사용할지 명시하지는 않는다. 이런 결정은 아키텍처의 범위를 범어선다. 하지만 대부분의 소프트웨어는 다음과 같은 익셉션 레벨에서 실행된다. 

 EL0: 유저 애플리케이션 
 EL1: 운영체제 커널(특권 권한으로 실행되는 함수) 
 EL2: 하이퍼바이저
 EL3: 시큐어 모니터 

 



두 개 이상의 게스트 OS를 실행하는 하이퍼바이저를 구현하지 않는 경우 EL2는 필수로 구현할 필요가 없습니다. 여기서 시큐어 모니터는 Armv8 아키텍처에서 도입된 실행 영역으로 EL3에서 실행됩니다. 시큐어 모니터는 논 시큐어 월드와 시큐어 월드 사이를 이동하기 위한 용도(Gatekeeping)로 도입된 실행 영역으로, 트러스트존 기능을 활성화해 보안 환경을 구축하려는 시스템에서만 사용됩니다.

EL2와 EL3는 필수 구현 항목은 아니므로 일단 ‘EL0에서는 유저 애플리케이션이 실행되고, EL1에서는 운영체제 커널이 실행된다’라는 정도로 기억해 둡시다. 

 

[중요] 익셉션 레벨와 Armv7의 동작 모드

익셉션 레벨에 대한 이해를 돕기 위해 Armv7 아키텍처의 동작 모드를 어떤 방식으로 활용하는지 함께 알아봅시다.

Armv7 아키텍처에서 유저 애플리케이션은 유저 모드에서 실행됩니다. 마찬가지로 Armv8 아키텍처에서는 EL0에서 유저 애플리케이션이 구동됩니다. 또한 Armv7 아키텍처에서 운영체제 커널은 슈퍼바이저 모드, Armv8 아키텍처에서는 EL1에서 운영체제 커널이 실행됩니다.

 

 


익셉션 레벨과 기능 정리

다음 표를 보면서 익셉션 레벨에 대해 배운 내용을 정리합시다.

 


 
어셈블리 명령어를 보면 ELR_EL1 혹은 SP_EL2와 같이 레지스터의 접미사가 많이 보입니다. 익셉션 레벨과 같이 정의된 이 같은 레지스터를 보면 위 표에서 소개한 익셉션 레벨이 떠올랐으면 좋겠습니다.

 

 

< '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>
 
 
이번 포스팅에서는 ARMv8 아키텍처를 파악하다가 만나는 가장 큰 걸림돌인 '익셉션 레벨'에 대해 설명합니다.
 
대부분 개발자들은 기존 ARM 아키텍처의 User, Supervisor, IRQ, FIQ, 모드에 익숙한 상태입니다. User 모드에서 소프웨어 인터럽트를 발생하면 Supervisor 모드로 진입한다"라는 내용에 익숙해 있죠. 그런데 ARMv8 아키텍처를 파악하면 가장 먼저 '익셉션 레벨'이란 용어를 만나게 됩니다. 문제는 ARMv8 아키텍처를 구성하는 주요 개념을 익셉션 레벨이란 용어를 사용해 설명을 합니다.
 
낯선 '익셉션 레벨(EL)'이란 용어를 사용해 ARMv8 아키텍처를 설명하니 스팩 문서를 읽다가 포기하는 경우가 많습니다.
 
EL와 익셉션 레벨이란
 
익셉션 레벨은 ARM 사에서는 EL(Exception Level)이라고 명시합니다. ARM 사에서 제공하는 스팩 문서에서 EL이 보이면 익셉션 레벨이라고 간주하면 됩니다. 그런데 EL 레벨은 몇 가지가 있을까요? EL은 0~3까지 있습니다. 즉, EL0, EL1, EL2, EL3까지 있는 것입니다.
 
한 가지 다음과 같은 예시를 들어 보겠습니다.
 
12.6. Translations at EL2 and EL3
 
위 사이트에 접근하면 '12.6. Translations at EL2 and EL3' 이란 제목의 글이 보일 겁니다. 여기서 EL2과 EL3는 무엇을 의미할까요?
바로 익셉션 레벨2(Exception Level2)와 익셉션 레벨3(Exception Level3)를 의미합니다.
 
이제부터 이 포스팅에서도 익셉션 레벨을 EL로 명시하겠습니다. 익셉션 레벨과 관련된 용어에 먼저 익숙해질 필요가 있거든요. 
 
EL0 = ARM의 User 모드
 
64비트의 ARM 아키텍쳐(ARMv8)의 익셉션 레벨에 대해 간단히 살펴보고자 합니다.
 
먼저 EL0입니다. EL0은 ARMv7 아키텍처의 User모드에 대응합니다. EL0은 그냥 EL0(유저 모드: User Mode)라고 명시해도 됩니다.
EL0(User 모드)에서는 '카카오톡', 'T-map' 그리고 '크래시 오브 클랜'과 같은 애플리케이션이 구동됩니다.
 
EL0은 다음과 같이 이야기할 수 있습니다.
 
   * EL0은 User 모드이다.
   * EL0은 유저 애플리케이션이 돌고 있는 모드이다.
 
EL0에 대해서 조금 정리가 됐나요? 이어서 EL1에 대해서 살펴보겠습니다. 
 
 
EL1 = ARM의 Supervisor 모드(커널 코드 실행)
 
 
EL1은 ARMv7 아키텍처 기준으로 Supervisor 모드에 해당합니다. 리눅스 관점으로는 리눅스 커널 코드가 동작하는 모드에 해당합니다. EL1을 쉽게 다음과 같이 설명할 수 있습니다.
 
   * EL1은 Supervisor 모드이며 커널 모드라고도 부른다.
   * EL1은 리눅스 커널 코드가 실행 중인 모드이다.
 
이번에는 EL2에 대해서 살펴보겠습니다.
 
EL2 = 하이퍼바이저 모드(하이퍼바이저 실행)
 
대부분 개발자분들은 EL0~EL1의 의미만 제대로 알아도 개발하는데 큰 지장이 없습니다.
그런데 EL2부터 좀 이해하기 어려운데 하이퍼바이저의 개념을 알아야 하기 때문입니다.
 
EL2는 하이퍼바이저 모드라고 부르며 '하이퍼바이저'의 코드가 실행되는 모드입니다.
그럼 하이퍼바이저는 무엇일까요? 하이퍼바이저를 쉽게 이해하려면 Vmware를 떠올리면 됩니다.
하나의 운영체제에서 상이한 운영체제를 구동시키기 위한 소프트웨어 아키텍처라고 볼 수 있죠.
 
알기 쉽게 한 가지 예를 들겠습니다. 여러분의 PC를 구동하는 운영체제가 멀틱스이고 두 개의 Vmware를 통해 각각 윈도우와 리눅스를 사용할 수 있다고 가정하겠습니다. 'Ctl + Tab' 키를 누르면 Vmware 프로그램을 이동해 사용하고 싶은 운영체제로 이동할 수 있는 상태죠.
 
여기서 멀틱스를 하이퍼바이저 그리고 2개의 Vmware에서 돌고 있는 윈도우와 리눅스를 각각 게스트 운영체제(Guest OS) 라고 볼 수 있습니다. 그런데 각각 운영체제의 커널 코드는 EL1에서 구동됩니다. 예를 들어 2개의 Vmware에서 구동 중인 게스트 운영체제의 커널 코드는 EL1 그리고 유저 애플리케이션은 EL0로 구동 중이 것입니다.
 
따라서 각각 Guest OS를 제어할 수 있는 모드가 필요한데 이 모드를 하이퍼바이저 모드라고 하며 ARMv8 아키텍처에서는 이를 EL2라고 명시합니다.
 
EL2를 다음과 같이 설명할 수 있겠네요.
 
   * EL2는 하이퍼바이저 모드이다.
   * EL2에서 구동 중인 하이퍼바이저는 Guest OS를 제어하는 역할을 수행한다.
 
EL2는 하이퍼바이저를 위해 설계한 모드라 볼 수 있습니다.
 
EL3 = Secure 모드(트러스트 존 실행)
 
이번에는 마지막으로 EL3 모드에 대해 소개합니다. EL3는 ARMv7 아키텍처 기준으로 Secure 모드에 해당하며 트러스트 존이 실행됩니다. EL3는 다음과 같이 말할 수 있습니다.
 
   * EL3는 ARMv7 아키텍처의 안전(Secure) 모드이다.
   * EL3에서는 트러스트 존(TrustZone)이 실행한다.
 
 
EL의 특징
 
EL에 대해 소개를 했으니 각 Exception Level(EL) 특징을 다음과 같이 정리해 봅시다.
1> EL0 -> EL1 -> EL2 -> EL3로 갈수록 execution privilege가 증가해요. 볼 수 있는 코드나 파일에 대한 Permission이 더 있다는 거죠. 
2> EL0는 유일한 unprivileged 특성을 가져요.
3> EL2는 Non-secure 모드에서 가상화를 구현하기 위해서 사용되곤 하는데 자주 쓰지는 않아요.
4> EL3는 secure 와 Non-secure 모드 전환을 위해서 사용되죠.
5> ARMv8에서 EL0, EL1은 필수 구현 사항이며 나머지는 Option이에요.
 
즉 ARMv8을 탑재한 SoC 업체나 벤더에서 특정 요구 사항에 따라 구현을 안할 수도 있다는 거죠.
만약에 TrustZone을 안 쓰는 자동차 텔레메틱스 용 임베디드 장비를 개발할 때는 구지 EL3까지 구현할 필요는 없어요.
EL의 이동
 
 
Exception Level 이동에 대해서 잠깐 적으면요.
1> EL1: Supervisor Call(SVC)로 EL0 -> EL1로 이동하며 System Call 개념과 동일해요.
      ARM32 아키텍쳐의 Supervisor Mode와 비슷하다고 보면 되는데요, 보통 리눅스 커널이 구동될 때의 모드죠.   
 
2> EL2:  Hypervisor Call(HVC)로  EL1 -> EL2이 이동해요.
3> EL3: Secure Monitor Call(SMC)로 레벨 이동을 할 수 있는데요.  EL1 -> EL3, EL2 -> EL3 방향으로 이동 가능하죠.
여기서 중요한 점은 낮은 EL에서 높은 EL로 Shift를 하고 싶을 때는 반드시 Exception을 Trigger해줘야 해요.
그럼 ARM 아키텍쳐(ARMv8)는 Exception을 발생시키면 스택을 푸쉬하는 등 백업을 위한 추가 동작을 해줘야 하고
Exception 발생 시 preempt_count를 보고 스케쥴을 할 수 있기 때문에 시스템 설계 시 고려해야 할 사항이 늘어나요.
 
반대로 높은 EL는 execution privilege이 낮은 EL보다 높기 때문에 구지 Exception을 Trigger할 필요가 없어요.
ARM32 아키텍쳐에서 말 많이 들어봤잖아요. 커널 모드에서는 모든 파일 접근이 가능하다. (유저 모드 보다)
 
그래서 부트 Sequence에서 커널 실행 직전 익셉션 레벨을 EL3로 설정해놓는 동작이 사실 상 표준으로 잡혀 있어요.
SoC업체에서 TrustZone과 커널을 넘아들면서 부팅 초기화를 해주고 싶을 경우가 많다고 하네요.
 
각 Exception Level 사용 예를 들면 아래와 같아요.
EL0 : Applications (ex. 카카오톡, T-map)
EL1 : OS Kernel (ex. Android, Linux, QNX)
EL2 : Hypervisor (ex. XEN, KVM)
EL3 : Secure monitor (ex. 솔라시아, Trustonic사의 secure Firmware)
 
EL 벡터 코드 분석
 
그럼 각 Exception Level Vector는 어디에 구현되어 있냐구요?
아래 코드를 눈 크게 뜨고 보세요. 각 모드별로 Exception Vector가 정의되어 있죠.
#!/bin/sh
[arch/arm64/kernel/entry.S]
ENTRY(vectors)
ventry el1_sync_invalid // Synchronous EL1t
ventry el1_irq_invalid // IRQ EL1t
ventry el1_fiq_invalid // FIQ EL1t
ventry el1_error_invalid // Error EL1t
 
ventry el1_sync // Synchronous EL1h
ventry el1_irq // IRQ EL1h
ventry el1_fiq_invalid // FIQ EL1h
ventry el1_error_invalid // Error EL1h
 
ventry el0_sync // Synchronous 64-bit EL0
ventry el0_irq // IRQ 64-bit EL0
ventry el0_fiq_invalid // FIQ 64-bit EL0
ventry el0_error_invalid // Error 64-bit EL0
 
#ifdef CONFIG_COMPAT
ventry el0_sync_compat // Synchronous 32-bit EL0
ventry el0_irq_compat // IRQ 32-bit EL0
ventry el0_fiq_invalid_compat // FIQ 32-bit EL0
ventry el0_error_invalid_compat // Error 32-bit EL0
#else
ventry el0_sync_invalid // Synchronous 32-bit EL0
ventry el0_irq_invalid // IRQ 32-bit EL0
ventry el0_fiq_invalid // FIQ 32-bit EL0
ventry el0_error_invalid // Error 32-bit EL0
#endif
END(vectors)
 
위 벡터 중에 el0_sync_compat를 좀 살펴볼께요.
EL0 -> EL1으로는 주로 시스템 콜로 레벨이 바뀐다고 했잖아요.
 
el0_sync_compat -> el0_svc_compat ->el0_svc_naked -> el0_svc_naked 순서로 호출이 되는데.
아래 코드 조각을 잠깐 보면 "syscall handling" 이란 주석도 보이네요.
el0_svc_compat:
/*
 * AArch32 syscall handling
 */
adrp stbl, compat_sys_call_table // load compat syscall table pointer
uxtw scno, w7 // syscall number in w7 (r7)
mov     sc_nr, #__NR_compat_syscalls
b el0_svc_naked
 
el0_svc_naked 함수에서 시스템 콜 번호를 찾아서 등록된 시스템 콜 함수를 호출해요.
el0_svc_naked: // compat entry point
stp x0, scno, [sp, #S_ORIG_X0] // save the original x0 and syscall number
enable_dbg_and_irq
ct_user_exit 1
 
ldr x16, [tsk, #TI_FLAGS] // check for syscall hooks
tst x16, #_TIF_SYSCALL_WORK
b.ne __sys_trace
cmp     scno, sc_nr                     // check upper syscall limit
b.hs ni_sys
ldr x16, [stbl, scno, lsl #3] // address in the syscall table
blr x16 // call sys_* routine
b ret_fast_syscall
ni_sys:
mov x0, sp
bl do_ni_syscall
b ret_fast_syscall
ENDPROC(el0_svc)
 
위 코드가 머리가 들어오지 않는다구요.
아래 콜스택을 잠깐 볼까요? 아래는 sys_sendto란 시스템 콜로 해당 드라이버 코드가 수행된 다음에, 커널 패닉으로 시스템이 돌아가시는 동작인데요. sys_sendto란 시스템 콜을 호출하기 전에 el0_svc_naked(asm) 심볼이 보이네요.
-000|panic(?)
-001|oops_end(inline)
-001|die(?, regs = 0xFFFFFFE4DE6A78D0, err = -563462144)
-002|__do_kernel_fault.part.6(mm = 0xFFFFFFE55A7E4F00, addr = 18446743524274601936, esr = 2516582407, regs = 0xFFFFFFE4DE6A78D0)
-003|__do_kernel_fault(inline)
-003|do_page_fault(addr = 18446743524274601936, esr = 2516582407, regs = 0xFFFFFFE4DE6A78D0)
-004|do_mem_abort(addr = 18446743524274601936, esr = 2516582407, regs = 0xFFFFFFE4DE6A78D0)
-005|el1_da(asm)
 -->|exception
-006|ch_pop_remote_rx_intent(?, size = 40, riid_ptr = 0xFFFFFFE4DE6A7B1C, intent_size = 0xFFFFFFE4DE6A7B20, cookie = 0xFFFFFFE4DE6A7B28)
-007|tx_common(handle = 0xFFFFFFE5775ED200, ?, data = 0x0, iovec = 0xFFFFFFE5791C1580, size = 40, ?, ?, tx_flags = 1)
-008|txv(handle = 0xFFFFFFE5775ED200, ?, iovec = 0xFFFFFFE5791C1580, size = 40, vbuf_provider = 0xFFFFFF9F99B36144, pbuf_provider = 0x0, ?)
-009|ipc_router_glink_xprt_write(?, ?, xprt = 0xFFFFFFE4F4F7A4D0)
-010|ipc_router_write_pkt(inline)
-010|ipc_router_send_to(src = 0xFFFFFFE54FC67000, ?, ?, ?)
-011|ipc_router_sendmsg(?, ?, total_len = 22)
-012|sock_sendmsg_nosec(inline)
-012|sock_sendmsg(sock = 0xFFFFFFE4DC856A80, ?)
-013|SYSC_sendto(inline)
-013|sys_sendto(fd = -116527563208, buff = 531225458560, ?, ?, addr = 531233627424, addr_len = 20)
-014|el0_svc_naked(asm)
 -->|exception
-015|NUX:0x7BB2AFB7C4(asm)
 ---|end of frame




 < '시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리' 저자>
 
 

+ Recent posts