Armv7 아키텍처의 익셉션과 비교했을 때, Armv8 아키텍처에서는 익셉션을 분류하는 체계와 익셉션 벡터 테이블이 약간 다릅니다. 그 특징에 대해서 더 자세히 알아봅시다.
첫째, Armv8 아키텍처에서는 익셉션의 종류를 계층 구조로 재정의했습니다. 먼저 익셉션을 Synchronous와 Asynchronous와 같이 큰 카테고리로 분류하고, 하부 카테고리로 익셉션 클래스를 정의했습니다. Armv7 의 익셉션의 종류는 익셉션 클래스에서 확인할 수 있습니다.
표 9.1 Armv8 아키텍처에서 익셉션의 분류 체계
먼저 익셉션을 Synchronous와 Asynchronous와 같이 큰 카테고리로 분류합니다. Arm 코어가 명령어를 실행하다가 유발하는 익셉션을 Synchronous, 외부 인터럽트나 외부 메모리 어보트와 같이 외부에서 비동기적으로 유발되는 익셉션을 Asynchronous로 분류합니다. 이처럼 기존 Armv7 아키텍처의 익셉션과 비교해 익셉션을 분류하는 방식이 다릅니다.
둘째, 익셉션 레벨(EL)이란 개념을 도입해 이 기준으로 익셉션을 처리합니다. 익셉션이 유발된 익셉션 레벨 별로 익셉션 벡터 주소가 존재하며, 익셉션 레벨 별로 익셉션 링크 레지스터를 정의합니다.
다음 그림은 Armv8 아키텍처에서 익셉션 레벨 별로 익셉션 처리되는 구조를 나타냅니다.
그림 9.1 Armv8 아키텍처에서 익셉션 레벨 별로 익셉션이 처리되는 흐름
그림의 윗 부분을 먼저 보겠습니다. 유저 애플리케이션이 구동되는 EL0에서 익셉션이 발생하면 다음 순서로 처리됩니다.
이처럼 Arm코어는 익셉션이 유발되면 익셉션이 발생한 익셉션 레벨 별로 지정된 익셉션 벡터로 프로그램 카운터를 브랜치합니다.
[정보] EL0과 EL1은 각각 Armv7 아키텍처의 User 모드, Supervisor 모드에 대응됩니다. Armv7 아키텍처의 User 모드에서는 유저 애플리케이션이 구동하고, Supervisor 모드에서는 운영체제의 커널이 동작합니다. 마찬가지로, Armv8 아키텍처의 EL0에서는 유저 애플리케이션이 실행되고, EL1에서는 운영체제의 커널이 동작합니다.
Armv7 아키텍처에서는 어떤 Arm 동작 모드에서 익셉션이 유발돼도 익셉션 종류 별로 지정된 익셉션 벡터로 프로그램 카운터가 브랜치됩니다. 예를 들어 유저 애플리케이션이 실행되는 User 모드나 운영체제의 커널이 구동되는 슈퍼바이저 모드에서 데이터 어보트가 유발되면, 데이터 어보트 익셉션에 해당되는 익셉션 벡터로 프로그램 카운터가 브랜치됩니다.
그래서 익셉션 핸들러에서 익셉션이 유발된 시점의 Arm 동작 모드를 읽어서 이를 세분화하는 명령어를 입력해야 합니다. 예를 들어, 리눅스 커널인 경우 다음 코드와 같이 Arm 동작 모드를 읽어서 세분화해 처리하는 동작을 확인할 수 있습니다.
다음은 Armv7 아키텍처 기반 리눅스 커널에서 구현된 Undefined Instruction 익셉션 핸들러의 구현부입니다.
메모리 어보트 타입 익셉션이 발생하면 익셉션 핸들러에서 디버깅 정보를 출력하고 시스템을 리셋시킵니다. 소프트웨어적으로 시스템이 치명적인 오류가 있는 상태로 판단하기 때문입니다.
다음 그림을 보면서 메모리 어보트 타입 익셉션의 전체 실행 흐름을 알아봅시다.
그림 8.2 메모리 어보트 타입 익셉션이 발생할 때 전체 흐름
익셉션의 전체 실행 흐름도는 4단계로 분류할 수 있는데, 각 단계 별로 어떤 동작을 수행하는지 알아봅시다.
1단계: 프로세스가 실행하는 도중에 익셉션을 유발하는 명령어 실행
먼저 ①로 표시된 부분을 눈으로 따라가 봅시다. 프로세스가 메모리 어보트를 유발하는 명령어를 실행하는 부분입니다.
2단계: ARM 프로세서가 익셉션을 감지
② 로 표시된 부분은 ARM 프로세서가 익셉션을 감지하는 동작입니다. 1단계에서 메모리 어보트를 유발하는 명령어를 ARM 코어가 실행하면 다음과 같은 익셉션을 유발합니다.
❑ Undefined Instruction ❑ 프리패치 어보트 ❑ 데이터 어보트
②~③으로 표기된 부분의 외곽에 보이는 테두리는 ARM 프로세서가 하드웨어적으로 처리되는 부분입니다. 이 동작은 소스 코드 형태로 확인하기 어렵습니다.
3단계: ARM 프로세서가 익셉션에 대한 세부 처리
③으로 표기된 박스를 보겠습니다. 익셉션을 감지한 ARM 코어는 다음과 같은 동작을 처리합니다.
❑ 익셉션이 발생한 시점의 ARM 동작 모드를 나타내는 CPSR 레지스터를 spsr_<mode> 레지스터에 저장한다. ❑ CPSR 레지스터를 변경해 ARM의 동작 모드를 변경한다. ❑ 익셉션의 종류에 따라 이미 지정된 주소로 프로그램 카운터를 브랜치한다.
많은 SW 개발자들은 위 동작을 보고 "소스 코드로 보고 싶다"란 생각이 들지도 모르겠지만, 이 동작은 "하드웨어적으로" ARM 프로세서가 처리되는 부분입니다.
4단계: 익셉션 핸들러 실행 후 시스템 콜 핸들러 호출
④으로 표기된 박스는 SW적으로 처리되는 부분입니다. 3단계에서 익셉션의 종류에 따라 이미 지정된 주소로 프로그램 카운터를 브랜치하면, 익셉션의 종류에 따라 이미 지정된 주소에 위치한 명령어가 실행됩니다. 익셉션의 종류에 따라 이미 지정된 주소를 익셉션 벡터 주소라고 하며, 익셉션의 종류에 따라 이미 지정된 주소에 위치한 코드를 익셉션 벡터 핸들러라고 합니다. 익셉션 벡터로 구성된 익셉션 벡터 테이블은 다음 절에서 자세히 다룰 예정입니다.
처음에 ARM 아키텍처의 익셉션을 공부할 때 익셉션 벡터 테이블을 이해하는데만 집중하는 분들이 많습니다. "ARMv7 아키텍처의 익셉션 동작 원리를 파악하려면 익셉션 벡터 테이블만 제대로 이해하면 된다"라고 생각하기 때문입니다. 하지만 익셉션 벡터 테이블의 내용만 익히면 배운 내용을 실전 프로젝트에 활용하기 어렵습니다.
왜냐면, 익셉션 벡터 테이블의 내용보다도 다음과 같은 사실을 파악하는게 더 중요하기 때문입니다.
❑ 익셉션은 소프웨어적으로 어떤 명령어가 실행될 때 유발될까? ❑ ARM 프로세서가 익셉션을 감지한 후 소프트웨어적으로 어떤 처리를 수행할까?
위에서 소개한 질문에 답을 하려면 다음 그림에서 소개한 익셉션을 구성하는 주요 개념에 대해 파악할 필요가 있습니다.
그림 8.1 ARMv7 익셉션을 구성하는 주요 개념
그림 8.1에서 먼저 ‘유발 요인’으로 표기된 원의 내용부터 살펴봅시다.
먼저 그림을 보면 가운데 부분에 선이 보입니다. 이 선의 윗 부분은 ARM 코어가 익셉션을 감지하면 이를 하드웨어적으로 처리하는 과정을 나타내고, 아랫 부분은 소프트웨어적으로 익셉션을 처리하는 부분을 나타냅니다.
유발요인
익셉션이 동작하는 원리의 전체 맥락과 흐름을 파악하려면 익셉션을 유발하는 원인을 구체적으로 파악해야 합니다. 먼저 그림의 왼쪽 아랫 부분에 있는 원은 익셉션을 유발하는 요인을 나타내는데, 그 항목은 다음과 같습니다.
❑ 메모리 어보트: 데이터 어보트(Data Abort), 프리페치 어보트(Prefetch Abort), Undefined Instruction ❑ 외부 인터럽트(IRQ/FIQ) ❑ SW 인터럽트
익셉션을 유발하는 첫 번째 요인은 메모리 어보트를 유발하는 명령어를 실행했을 때입니다. ARM 코어는 명령어를 세부 단계로 나눠서 처리하는데, 이 과정에서 오류를 확인하면 데이터 어보트(Data Abort), 프리페치 어보트(Prefetch Abort), Undefined Instructiond와 같은 메모리 어보트 타입 익셉션을 유발합니다.
익셉션을 유발하는 두 번째 요인은 외부 하드웨어에서 전달되는 인터럽트입니다. ARM 코어가 외부 하드웨어에서 IRQ 인터럽트나 FIQ(Fast Interrupt Request) 인터럽트를 감지하면 익셉션을 유발합니다.
[정보] 여기서 말하는 외부 하드웨어는 컴퓨터에서 사용하는 키보드나 마우스 그리고 휴대폰의 센서와 같은 디바이스를 의미합니다.
마지막 익셉션의 유발 요인은 소프트웨어 인터럽트입니다. 용어 그대로 소프트웨어으로 유발되는 인터럽트인데, 'svc' 명령어를 실행하면 ARM 프로세서는 익셉션을 유발합니다.
IRQ와 같은 인터럽트는 외부 하드웨어에서 비동기적으로 인터럽트가 발생할 때 유발되는 익셉션인데, 소프트웨어적으로 'svc' 명령어를 실행하면 ARM 코어는 이를 '인터럽트' 익셉션을 유발하므로 이를 소프트웨어 인터럽트라고도 부릅니다.
운영체제에서 유저 애플리케이션이 실행되는 User 모드에서 'svc' 명령어를 실행해 커널 함수가 실행되는 슈퍼바이저 모드로 진입합니다. 이 과정을 운영체제에서는 시스템 콜이라고 부릅니다.
레지스터 업데이트
ARM 코어는 익셉션을 감지하면 특정 ARM의 작동 모드(Operation Mode)에서만 접근하는 레지스터를 다음과 같이 변경합니다.
❑ 익셉션이 발생한 시점의 CPSR 레지스터를 변경되는 모드의 spsr_<mode> 레지스터에 백업 ❑ 익셉션이 발생한 순간에 실행된 주소 기준으로, 익셉션이 유발된 다음 명령어로 복귀할 주소를 R14_<mode> 레지스터에 백업
이와 같이 익셉션이 발생한 순간의 정보를 레지스터에 저장하는 이유는, 익셉션의 타입에 따라 익셉션이 발생하기 직전의 모드와 주소로 복귀하는 동작을 지원하기 위해서입니다.
ARM 동작 모드 변경
'ARM의 동작 모드'는 익셉션을 구성하는 주요 개념 중 하나입니다. ARM 코어에서 익셉션을 유발하면 익셉션에 대응되는 ARM의 동작 모드(슈퍼바이저 모드, IRQ 모드)가 변경되므로 'ARM의 동작 모드'에 대해서도 잘 알고 있어야 합니다.
ARM 코어는 익셉션의 종류를 감지한 다음, 익셉션의 종류 별로 처리하는 동작 모드를 변경하는데, 익셉션의 종류 별로 처리되는 동작 모드의 목록은 다음 표와 같습니다.
ARM 코어는 CPSR 레지스터의 [4:0] 비트를 업데이트해, 익셉션의 종류 별로 처리할 ARM의 동작 모드를 변경합니다. 각 익셉션의 종류 별로 변경되는 각각의 모드가 있는데, 프리패치 어보트와, 데이터 어보트와 같은 익셉션을 감지하면 공통으로 '어보트 모드'로 변경합니다.
ARM 코어는 익셉션의 종류 별로 ARM 모드를 지정해 처리하는데, 익셉션의 종류에 따른 실행 흐름 구분해 처리할 수 있습니다.
익셉션 벡터 테이블
익셉션 벡터 테이블을 설명하기 전에 먼저 관련 용어를 먼저 소개하겠습니다. 먼저 익셉션 벡터 테이블에 대해 알아봅시다. 익셉션 벡터 테이블은 용어 그대로 익셉션 벡터로 구성된 테이블이라 볼 수 있습니다.
그렇다면 익셉션 벡터란 무엇일까요? ARM 코어는 익셉션을 감지해 익셉션의 종류를 식별한 다음에, 익셉션의 종류 별로 지정된 주소로 프로그램 카운터를 브랜치합니다. 쉽게 설명하면, 프로그램 카운터에 익셉션의 종류 별로 지정된 주소를 넣어주는 동작입니다. 여기서 말하는 익셉션의 종류 별로 지정된 주소를 익셉션 벡터라고 합니다.
익셉션 벡터 테이블은 익셉션 벡터로 구성된 테이블인데, 2가지 내용으로 구성돼 있습니다.
❑ 익셉션의 종류 ❑ 익셉션의 종류 별 오프셋 주소
익셉션이 발생하면 ARM 아키텍처에서 정한 규칙에 따라 익셉션 벡터로 프로그램 카운터가 브랜치되는데, 이 내용을 익셉션 벡터 테이블이 담고 있습니다.
익셉션 벡터와 익셉션 벡터 테이블이란 용어를 같이 사용해 익셉션의 동작 원리를 다음과 같이 설명할 수 있습니다.
“익셉션이 발생하면 익셉션 벡터 테이블에 명시된 익셉션 벡터로 프로그램 카운터가 브랜치한다”
이어서 ARM 스팩 문서를 보면서, 익셉션이 유발될 때 ARM 프로세서는 어떤 방식으로 익셉션 벡터를 프로그램 카운터로 브랜치하는지 알아봅시다. ARM 사에서 배포한 스팩 문서 'DDI0403E_d_armv7m_arm'를 보면 익셉션이 발생하면 익셉션 벡터로 프로그램 카운터를 브랜치하는 동작을 다음과 같은 슈도 코드로 표기합니다.
// Branch to Undefined Instruction vector. BranchTo(ExcVectorBase() + vect_offset);
위 코드에서 ExcVectorBase() 함수는 익셉션 벡터 테이블의 베이스 주소, vect_offset는 익셉션 벡터 테이블 주소 기준의 오프셋을 의미합니다. 'ExcVectorBase() + vect_offset' 구문은 익셉션 벡터 테이블의 베이스 주소에서 오프셋을 더한 주소인데, 이 인자를 적용해 BranchTo() 함수를 호출합니다. BranchTo() 함수는 인자를 프로그램 카운터로 브랜치하는 동작을 나타내는 슈도 코드 함수입니다.
정리하면, 위 슈도 코드는 다음과 같은 동작을 나타냅니다.
“익셉션이 발생하면 익셉션 벡터로 프로그램 카운터를 브랜치한다”
이제 앞으로 계속 사용될 익셉션 벡터 테이블과 관련된 용어를 간단히 정리해봅시다.
❑ 익셉션 벡터: 익셉션의 종류 별로 ARM 프로세서가 프로그램 카운터로 브랜치하는 주소 ❑ 익셉션 벡터 테이블: 익셉션 벡터로 구성된 일종의 테이블 ❑ 익셉션 벡터 베이스 주소: 익셉션 벡터 테이블의 시작 주소를 의미하면 익셉션 벡터 주소를 정할 때의 기준이 되는 주소
익셉션 벡터 테이블의 자세한 내용은 다음 포스트에서 살펴볼 예정입니다.
이어서 익셉션 벡터로 프로그램 카운터를 이동하면 익셉션 벡터 주소에 위치한 익셉션 핸들러가 처리되는 과정을 살펴봅시다.
익셉션 핸들러
ARM 코어가 익셉션 벡터 주소로 프로그램 카운터를 브랜치하면, 익셉션 벡터 주소에 위치한 명령어가 실행됩니다. 이 때 익셉션 핸들러가 실행되는데, 익셉션의 종류에 따라 각기 다른 방식으로 처리합니다.
❑ 메모리 어보트 타입 익셉션: 시스템 리셋 ❑ IRQ 익셉션: 인터럽트를 처리하는 인터럽트 서비스 루틴 실행 ❑ SW 인터럽트 익셉션: 시스템 콜 핸들러 실행
첫 번째로, 프리패치 어보트, 데이터 어보트 그리고 Undefined Instruction과 같은 메모리 어보트 타입 익셉션의 경우, 해당 익셉션 핸들러에서 레지스터와 같은 디버깅 정보를 출력합니다. 메모리 어보트 타입 익셉션은 소프트웨어적으로 치명적인 오류가 있을 때 유발되는데, 프로그램의 유형에 따라 달리 처리합니다.
❑ 유저 애플리케이션: 프로세스를 종료 ❑ 운영체제의 커널이나 커널 드라이버: 시스템을 리셋
[정보] 리눅스 운영체제에서 커널 패닉이나 커널 크래시가 발생할 경우 메모리 어보트 타입 익셉션이 유발됩니다.
둘째, IRQ 익셉션은 메모리 어보트 타입 익셉션과 처리 방식이 다릅니다. IRQ 익셉션은 외부 하드웨어와 소프트웨어 간의 인터페이스와 같은 기능이므로, IRQ 익셉션이 발생하면 실행되는 익셉션 핸들러에서 인터럽트 서비스 루틴을 실행합니다. 인터럽트는 하드웨어의 변화를 알리기 위한 인터페이스로 동작하므로, 인터럽트 서비스 루틴에서 인터럽트 핸들러이 호출되며, 인터럽트 핸들러에서 인터럽트에 대한 처리를 수행합니다.
셋째, 'svc'과 같은 명령어를 실행해 처리되는 소프트웨어 인터럽트의 경우, 해당 익셉션 핸들러에서 시스템 콜 핸들러로 분기하는 동작을 수행합니다. 운영체제 커널에서는 시스템 콜 핸들러의 정보를 포함하는 시스템 콜 테이블이 존재하는데, 시스템 콜 테이블의 정보를 참고해 시스템 콜 핸들러가 호출됩니다. 소프트웨어 인터럽트 익셉션도 IRQ 익셉션과 마찬가지로 운영체제의 시스템 콜을 지원하는 기능으로 동작합니다.
여기까지 익셉션을 이루는 주요 기능에 대해 살펴봤습니다. 여러분이 익셉션과 관련된 코드나 스팩 문서를 볼 때 이번 절에 소개된 내용을 머릿 속으로 떠올리면 전체 실행 흐름을 더 쉽게 이해할 수 있습니다.
익셉션 핸들러의 코드를 분석하기 전에 "'익셉션 벡터 베이스 주소 + 오프셋 주소' 규칙으로 프로그램 카운터가 분기된다"라는 사실을 염두에 둡시다. 또한 VBAR_EL2 레지스터는 익셉션 핸들러의 시작 주소를 저장한다는 사실을 기억합시다. 이번 절에서 소개한 익셉션 핸들러의 베이스 주소와 오프셋 정보는 다음과 같습니다.
만약 EL2에서 Synchronous 익셉션이 유발되면 다음과 같은 공식에 따라 프로그램 카운터가 0x26aa00 주소로 분기됩니다.
0x26aa00 = 0x26a800(VBAR_EL2) + 0x200(오프셋)
이와 마찬가지로 EL2에서 'IRQ Interrupt' 익셉션이 유발되면 프로그램 카운터가 0x26aa80 주소로 분기됩니다. 규칙은 다음과 같습니다.
0x26aa80 = 0x26a800(VBAR_EL2) + 0x280(오프셋)
이처럼 XEN 하이퍼바이저의 익셉션 벡터 테이블은 Armv8 아키텍처에서 명시된 익셉션 벡터 테이블의 스펙에 따라 구현됐다는 점을 알 수 있습니다.
XEN 하이퍼바이저 관점에서 익셉션 핸들러 구조 정리
다음은 앞에서 설명한 익셉션 핸들러 코드에서 익셉션 종류별로 분기되는 프로그램 카운터의 정보를 정리한 표입니다.
익셉션 핸들러의 코드를 제대로 이해하려면 Arm 아키텍처에서 정의된 익셉션 벡터 테이블을 먼저 숙지해야 합니다.
익셉션 핸들러 코드 분석
XEN 하이퍼바이저의 익셉션 핸들러를 해석하는 방법과 전체 구조를 소개했으니 익셉션 핸들러 코드를 분석하겠습니다.
익셉션 핸들러의 앞부분에 있는 02 ~ 14번째 줄은 익셉션 레벨과 상관없이 같은 스택을 사용하도록 설정한 경우에 동작합니다. XEN 하이퍼바이저는 이 유형의 익셉션을 지원하지 않으므로 해당 익셉션 핸들러의 서브루틴에서는 디버깅 정보를 출력하고 크래시를 유발하는 루틴이 실행됩니다.
여기서 EL2의 Synchronous 익셉션은 어떻게 유발될까요? 이 질문에는 다음과 같이 답할 수 있습니다.
EL2에서 실행되는 하이퍼바이저에서 메모리 어보트를 유발하는 명령어가 실행된 경우에 Synchronous 익셉션이 유발된다.
Arm 아키텍처 관점에서 분석하면 EL2에서 Synchronous 익셉션이 유발될 때 14번째 줄에 보이는 0x26aa00 주소로 프로그램 카운터가 분기됩니다. 14번째 줄에 "b hyp_sync"라는 명령어가 있는데, 이 명령어가 실행되면 hyp_sync 레이블로 분기합니다.
hyp_sync 레이블의 서브루틴에서는 디버깅 정보를 출력하고 크래시를 유발하는 루틴이 처리됩니다.
하이퍼바이저가 구동되는 EL2에서 'FIQ 인터럽트'가 유발되면 20번째 줄에 보이는 0x26ab00 주소로 프로그램 카운터가 분기됩니다. 그런데 XEN 하이퍼바이저는 FIQ를 지원하지 않으므로 hyp_fiq_invalid라는 레이블로 분기되며, 이후 크래시를 유발하는 코드가 실행됩니다.
--- [정보] FIQ는 어디서 처리할까? 대부분의 운영체제에서 FIQ는 시큐어 월드의 Trusted 커널에서 받아서 처리하도록 시스템을 설정합니다. ---
이어서 SError 익셉션이 유발되면 실행되는 23 ~ 25번째 줄 코드를 분석하겠습니다.
게스트 OS가 실행되는 EL1에서 HVC, WFI, WFE 명령어를 실행했을 때 26번째 줄에 있는 0x26ac00 주소로 프로그램 카운터가 분기됩니다. 이런 동작을 "게스트 Exit"라고 합니다. 소프트웨어적으로 EL1에서 EL2로 진입하는 시작점이 26번째 줄이라고 볼 수 있으며, 하이퍼바이저를 분석할 때 가장 많이 리뷰하는 루틴이 게스트 Exit로 실행되는 익셉션 핸들러입니다.
11번째 줄은 익셉션 클래스의 정보를 담고 있는 esr_el2(익셉션 신드롬 레지스터)의 값을 x1 레지스터에 로딩하는 동작입니다. 11번째 줄에서 esr_el2 레지스터의 값을 x1 레지스터에 로딩하는 이유는 무엇일까요? esr_el2 레지스터의 [31:26] 비트에 익셉션이 유발된 세부 원인을 나타내는 익셉션 클래스 비트 정보가 저장돼 있기 때문입니다.
12번째 줄은 x1 레지스터의 값을 #HSR_EC_SHIFT만큼 오른쪽으로 비트 시프트 연산한 결과를 x1 레지스터에 저장합니다. 여기서 HSR_EC_SHIFT는 다음 매크로 선언부와 같이 26입니다.
13 cmp x1, #HSR_EC_HVC64 14 b.ne guest_sync_slowpath /* Not a HVC skip fastpath. */
13번째 줄은 x1 레지스터가 담고 있는 익셉션 클래스가 #HSR_EC_HVC64인지 비교하는 명령어입니다. 13번째 줄 명령어의 실행 결과는 14번째 줄에 영향을 주는데, x1 레지스터의 값이 #HSR_EC_HVC64가 아니면 guest_sync_slowpath 함수로 분기합니다.
--- [정보] HSR_EC_HVC64 매크로의 정체는?
여기서 HSR_EC_HVC64 매크로의 정체는 무엇일까요? 다음 코드와 같이 0x16입니다.
그렇다면 HSR_EC_HVC64를 0x16으로 설정한 근거는 무엇일까요? 의문을 풀기 위해서는 Armv8 아키텍처의 스펙 문서를 볼 필요가 있습니다. 다음은 익셉션 클래스와 관련된 Arm 스펙 문서의 내용입니다.
출처: Arm Architecture Reference Manual Armv8, for A-profile architecture https://developer.arm.com/documentation/ddi0487/gb D1.10.4 Exception classes and the ESR_ELx syndrome registers 010110 HVC instruction execution in AArch64 state, when HVC is not disabled
위에 명시된 0b010110(이진수)은 익셉션 클래스를 나타내는 비트 정보로써 16진수로는 0x16입니다. 이처럼 Arm 스펙 문서에서 명시된 정보를 기반으로 어셈블리 명령어를 구현합니다. ---
여기까지 EL2에서 처리되는 익셉션 핸들러의 전반적인 구조를 알아봤습니다. EL2에 존재하는 익셉션 핸들러도 Arm 아키텍처에서 정의된 익셉션 벡터 테이블에 따라 구현됐다는 사실을 알 수 있습니다.
이전 절에서 익셉션 벡터 테이블의 스펙을 간단히 리뷰했습니다. 이어서 EL2 관점에서 익셉션 벡터 테이블을 자세히 분석하겠습니다.
EL2 관점에서 분석한 익셉션 벡터 테이블
다음은 하이퍼바이저가 실행되는 EL2 기준에서 본 익셉션 벡터 테이블을 나타낸 표입니다.
먼저 'EL2 with SP_EL0' 행의 내용을 분석합시다. 'EL2 with SP_EL0'는 익셉션 레벨에 상관없이 스택을 설정하는 조건에서 실행되는 익셉션 종류별 오프셋을 나타냅니다. 그런데 대부분의 하이퍼바이저는 이와 같은 익셉션은 지원하지 않습니다.
EL2에서 발생한 익셉션 분석
이어서 'EL2에서 발생'로 표시된 부분을 봅시다. 0x200, 0x280, 0x300, 0x380은 하이퍼바이저가 구동되는 EL2에서 익셉션이 발생했을 때 익셉션의 종류별로 분기되는 오프셋 주소를 나타냅니다.
이 내용에 대해 조금 더 자세히 알아볼까요? 만약 EL2에서 실행되는 하이퍼바이저에서 Synchronous 익셉션이 유발되면 다음과 같이 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x200 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다.
Synchronous 익셉션 벡터 주소: VBAR_EL2 + 0x200
만약 EL2에서 'IRQ 인터럽트' 익셉션이 유발되면 익셉션 벡터 베이스 주소인 VBAR_EL2에 +0x280 오프셋을 더한 주소로 프로그램 카운터가 다음과 같이 분기됩니다.
IRQ 인터럽트 익셉션 벡터 주소: VBAR_EL2 + 0x280
또한 EL2에서 FIQ와 SError 익셉션이 유발되면 같은 방식으로 다음과 같은 주소로 프로그램 카운터가 분기됩니다.
FIQ 익셉션 벡터 주소: VBAR_EL2 + 0x300 SError 익셉션 벡터 주소: VBAR_EL2 + 0x380
--- [정보] 하이퍼바이저와 FIQ의 관계
대부분의 하이퍼바이저에서는 FIQ 익셉션을 지원하지 않습니다. FIQ는 시큐어 월드에서 실행되는 트러스트 커널에서 받아 처리하도록 시스템을 구성합니다. ---
게스트 OS가 실행되는 EL1에서 발생한 익셉션 분석
이어서 'EL1에서 발생(Aarch64)'로 표시된 부분은 게스트 OS가 구동되는 EL1에서 익셉션이 유발되면 분기되는 오프셋 주소를 나타냅니다.
EL1에서 실행되는 게스트 OS에서 HVC, WFE, WFI 명령어를 실행하면 Synchronous 익셉션이 유발됩니다. 이때 다음과 같이 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x400 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다.
Synchronous 익셉션 벡터 주소: VBAR_EL2 + 0x400
이 같은 동작을 “게스트 Exit 혹은 하이퍼바이저로 트랩된다”라고 설명할 수 있습니다.
또한 게스트 OS가 실행되는 EL1에서 'IRQ 인터럽트' 익셉션이 유발되면 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x480 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다.
IRQ 인터럽트 익셉션 벡터 주소: VBAR_EL2 + 0x480
일반적으로 EL1에서 설정한 IRQ 인터럽트는 EL1에 존재하는 익셉션 핸들러에서 처리합니다. 하지만 HCR_EL2.IMO를 1로 설정하면 해당 인터럽트를 EL1에 있는 익셉션 핸들러가 아닌 EL2에 존재하는 익셉션 핸들러에서 받아 처리할 수 있습니다. 물리적인 인터럽트를 EL2가 먼저 받아 이를 EL1에서 실행되는 게스트 OS에 알려야 한다면 가상 인터럽트를 생성해 게스트 OS에 통지할 수 있습니다.
다음으로 게스트 OS가 실행되는 EL1에서 'FIQ 인터럽트' 익셉션이 유발되면 분기되는 주소를 알아봅시다. 익셉션 벡터 베이스 주소(VBAR_EL2)에 +0x500 오프셋을 더한 주소로 다음과 같이 프로그램 카운터가 분기됩니다.
FIQ 인터럽트 익셉션 벡터 주소: VBAR_EL2 + 0x500
이전에 설명했지만 대부분의 하이퍼바이저는 FIQ를 지원하지 않습니다. +0x500 오프셋을 적용한 주소에는 크래시를 유발하는 *_invalid와 같은 레이블이 실행되며, 서브루틴에서 크래시를 유발하는 코드가 실행됩니다.
마지막으로 게스트 OS가 실행되는 EL1에서 'SError 인터럽트' 익셉션이 유발되면 익셉션 벡터 베이스 주소인 VBAR_EL2를 기준으로 +0x580 오프셋을 더한 주소로 프로그램 카운터가 분기됩니다. 규칙은 다음과 같습니다.
SError 익셉션 벡터 주소: VBAR_EL2 + 0x580
일반적으로 EL1에서 설정한 'SError 인터럽트'는 EL1에서 처리되나 HCR_EL2 레지스터를 설정하면 해당 'SError 인터럽트' 익셉션을 EL2에서 받아 처리할 수 있습니다.
'EL1에서 발생(Aarch32)' 행은 EL1에서 실행되는 게스트 OS가 32비트 환경에서 실행될 때 처리되는 익셉션입니다. 최근 대부분의 게스트 OS(예: 리눅스 커널)는 64비트 모드로 실행되므로 'EL1에서 발생(Aarch32)' 행에서 정의된 익셉션은 "하위 호환성을 위해 정의됐다"라는 정도로 알아둡시다.
HCR_EL2 레지스터를 구성하는 비트 필드 정보를 파악하면 "이 비트를 어떻게 설정할 수 있을까?"라는 의문이 생깁니다. Armv8 아키텍처에서는 HCR_EL2 레지스터의 값을 읽고 쓸 수 있는 MRS, MSR 명령어를 제공합니다.
HCR_EL2 레지스터는 읽는 MRS 명령어
먼저 HCR_EL2 레지스터의 값을 읽기 위해서는 다음과 같은 형식의 MRS 명령어를 실행하면 됩니다.
그림 15.7 HCR_EL2 레지스터를 읽는 MRS 명령어
그림 15.7은 HCR_EL2 레지스터의 값을 Xt 레지스터로 로딩하는 명령어입니다. 위 그림에서 보이는 <Xt>는 Armv8 아키텍처에서 정의된 범용 레지스터를 뜻하며, X0 ~ X30 레지스터 중 하나가 지정됩니다. MRS 명령어를 사용하면 HCR_EL2 레지스터의 어떤 비트가 활성화돼 있는지 알 수 있습니다.
--- [중요] MRS 명령어를 해석하는 방법
MRS 명령어를 만나면 그림 15.7의 왼쪽 윗부분에 있는 화살표와 같이 명령어의 가장 오른쪽에서 왼쪽 방향으로 분석할 필요가 있습니다. MRS 명령어의 ‘S’는 시스템 컨트롤 레지스터, ‘R’은 범용 레지스터라고 이해하면 됩니다. 가장 오른쪽에 있는 ‘S’에서 바로 왼쪽에 ‘R’이 있으니 "시스템 컨트롤 레지스터의 값을 범용 레지스터에 로딩한다"라고 해석할 수 있습니다. ---
HCR_EL2 레지스터에 값을 쓰는 MRS 명령어
이어서 HCR_EL2 레지스터에 지정된 값을 쓰는 MSR 명령어를 배워 봅시다.
그림 15.8 HCR_EL2 레지스터에 값을 써주는 MSR 명령어
그림 15.8과 같이 MSR은 <Xt> 레지스터의 값을 HCR_EL2 레지스터에 쓰는 명령어입니다. 위 그림에서 보이는 <Xt>는 Armv8 아키텍처에서 정의된 범용 레지스터를 뜻하며, X0 ~ X30 레지스터가 지정될 수 있습니다. 이 명령어로 HCR_EL2 레지스터를 구성하는 세부 비트를 설정할 수 있습니다.
--- [중요] MSR 명령어를 해석하는 방법
MSR 명령어는 그림 15.8의 왼쪽 윗부분에 있는 화살표와 같이 명령어의 가장 오른쪽에서 왼쪽 방향으로 분석할 필요가 있습니다. MSR 명령어의 ‘R’을 범용 레지스터, 바로 왼쪽에 있는 ‘S’를 시스템 컨트롤 레지스터라고 이해하면 됩니다. 가장 오른쪽에 ‘R’이 있고, 왼쪽에 ‘S’가 있으니 "범용 레지스터의 값을 시스템 컨트롤 레지스터에 써준다"라고 해석할 수 있습니다.
HCR_EL2는 ‘Hypervisor Configuration Register, EL2’의 약자로서 하이퍼바이저의 세부 동작을 설정하는 가장 중요한 레지스터 중 하나입니다. HCR_EL2는 EL1에서 EL2로 진입(트랩)하는 방식을 설정하는 비트 필드로 구성돼 있습니다. HCR_EL2 레지스터의 접미사에 있는 EL2는 이 레지스터에 접근할 수 있는 최소 익셉션 레벨이므로 EL2 혹은 EL3에서만 접근할 수 있습니다.
다음 그림은 HCR_EL2 레지스터의 비트 필드 정보입니다.
그림 15.6 HCR_EL2 레지스터의 비트 필드
다른 레지스터와 같이 레지스터를 구성하는 비트를 1로 설정하면 해당 비트 필드와 관련된 동작이 수행됩니다. HCR_EL2 레지스터를 구성하는 비트 필드 중에 중요한 내용을 소개하겠습니다.
FMO, bit [3]
물리적인 FIQ 라우팅 방식을 설정하는 비트 필드입니다. 각 비트에 따라 다음과 같이 설정됩니다.
0b0: 가상 FIQ 인터럽트를 비활성화하므로 물리적인 FIQ가 EL2로 라우팅되지 않음 0b1: 물리적인 FIQ가 유발되면 EL2로 라우팅됨
일반적으로 FIQ는 시큐어 인터럽트로 처리되므로 EL3 모니터 혹은 시큐어 EL1에서 받아 처리합니다.
IMO, bit [4]
IMO는 물리적인 IRQ 라우팅 방식을 설정하는 비트 필드입니다. 각 비트에 따라 다음과 같이 설정됩니다.
0b0: 가상 IRQ 인터럽트를 비활성화하므로 물리적인 IRQ가 EL2로 라우팅되지 않음 0b1: 물리적인 IRQ가 유발되면 EL2로 라우팅됨. EL2나 EL1에서 인터럽트가 유발되면 EL2에 존재하는 익셉션 핸들러(VBAR_EL2)가 받아 처리
만약 SCR_EL3.IRQ 비트가 1로 설정되면 EL3에 존재하는 익셉션 핸들러(VAR_EL3)가 받아 인터럽트를 처리합니다. 만약 IMO 비트가 0으로 설정되면 EL2보다 낮은 익셉션 레벨에서 설정된 인터럽트를 EL2가 받아 처리하지 않습니다.
AMO, bit [5]
이 비트는 EL1에서 발생한 Asynchronous 익셉션을 EL2에 위치한 익셉션 핸들러가 받아 처리하기 위한 용도로 사용됩니다. AMO 비트가 1로 설정됐으면 EL2나 EL2보다 낮은 익셉션 레벨에서 Asynchronous 혹은 SError 인터럽트가 발생하면 EL2로 트랩됩니다.
TWI, bit [13]
WFI 명령어에 대한 트랩을 설정하는 비트 필드로, 각 비트에 따라 다음과 같이 설정됩니다.
0b0: WFI 명령어를 실행하면 EL2로 트랩되지 않고 Arm 코어가 저전력 모드로 진입 0b1: EL0 혹은 EL1에서 WFI 명령어를 실행하면 트랩이 발생하면서 EL2로 익셉션 레벨이 변경
TWE, bit [14]
TWI 비트와 비슷한 용도로 WFE 명령어에 대한 트랩을 설정하는 비트 필드입니다. 각 비트에 따라 다음과 같이 설정됩니다.
0b0: WFE 명령어를 실행하면 EL2로 트랩되지 않고 Arm 코어가 저전력 모드로 진입 0b1: EL0 혹은 EL1에서 WFE 명령어를 실행하면 트랩이 발생하면서 EL2로 익셉션 레벨이 변경
TSC, bit [19]
SMC 명령어와 관련된 트랩 방식을 설정하는 비트 필드입니다.
0b0: EL1에서 SMC 명령어를 실행하면 EL3에 존재하는 모니터 모드로 진입 0b1: EL1에서 SMC 명령어를 실행하면 EL2로 트랩
여러 게스트 OS에서 SMC 명령어를 실행하면 동시다발적으로 EL3에 진입할 수 있어 누군가 이를 받아 처리해야 합니다. 이때 TSC 비트 필드를 1로 설정하면 여러 게스트 OS에서 SMC 명령어를 통해 EL2에서 실행되는 하이퍼바이저에 존재하는 익셉션 핸들러로 트랩됩니다. 이를 SMC 명령어 후킹이라고 합니다.
TVM, bit [26]
가상 메모리를 제어하기 위한 트랩을 설정하는 비트입니다. TVM 비트가 1로 설정될 경우 EL1에서 다음과 같은 메모리를 제어하는 레지스터에 어떤 값을 쓰면 EL2로 트랩됩니다.
많은 개발자들은 “HVC 명령어를 통해서만 EL1에서 EL2로 진입할 수 있다”라고 알고 있습니다. 사실 그렇지는 않습니다. WFE와 WFI 명령어를 실행해도 EL1에서 실행되는 게스트 OS에서 EL2로 진입할 수 있습니다.
--- [정보] WFI와 WFE 명령어
WFI와 WFE 명령어는 Arm 아키텍처에서 지원하는 저전력 기능 중 하나입니다. 리눅스 커널이나 RTOS 관점에서 루틴을 분석하면 "함수가 동시다발적으로 아주 빠르게 실행된다"라고 느끼지만 이를 실행하는 Arm 코어 입장에서는 여러 함수에서 특정 루프(예: while)를 반복하면서 데이터가 특정 조건을 만족할 때까지 기다리는 동작을 자주 수행합니다.
이 상황에서 Arm 코어가 저전력 모드(스탠바이)에 진입하면 전력을 최적화해 Arm 코어를 실행할 수 있습니다. 시스템에 하나의 운영체제만 존재하는 환경에서는 WFE, WFI 명령어가 저전력 모드용으로 사용됩니다.
가상화 관점에서는 게스트 OS에서 WFI 혹은 WFE 명령어가 실행되면 익셉션이 유발되어 EL2(하이퍼바이저 구동)로 진입한다고 볼 수 있습니다. 하이퍼바이저와 같이 시스템에 2개 이상의 운영체제가 실행되는 환경에서 HCR_EL2 레지스터의 TWE 혹은 TWI 비트를 1로 설정한 다음에 WFE, WFI 명령어를 실행하면 HVC 명령어처럼 EL2로 진입합니다. ---
Arm 스펙 문서에서 WFE, WFI 명령어 알아보기
이어서 Arm 아키텍처 문서를 보면서 WFE, WFI 명령어에 대해 더 자세히 알아봅시다.
EL2 provides the following traps for WFE and WFI instructions:
• HCR_EL2.TWE: 1: Any attempt to execute a WFE instruction at Non-secure EL1 or EL0 is trapped to EL2, if the instruction would otherwise have caused the PE to enter a low-power state. 0: Non-secure EL1 or EL0 execution of WFE instructions is not trapped to EL2.
• HCR_EL2.TWI 1: Any attempt to execute a WFI instruction at Non-secure EL1 or EL0 is trapped to EL2, if the instruction would otherwise have caused the PE to enter a low-power state. 0: Non-secure EL1 or EL0 execution of WFI instructions is not trapped to EL2.
스펙 문서의 내용을 다음과 같이 해석할 수 있습니다.
EL2는 WFE와 WFI 명령어에 대한 트랩을 지원한다. HCR_EL2 레지스터의 TWE와 TWI 비트가 1로 설정된 경우 WFE 혹은 WFI 명령어를 실행하면 EL2로 트랩된다. 만약 HCR_EL2 레지스터의 TWE와 TWI 비트가 0으로 설정됐으면 기존의 저전력 모드로 진입하는 동작을 수행한다.
이어서 HVC, WFE, WFI 명령어를 실행하면 어떤 방식으로 EL2에 진입하는지 알아봅시다.
HVC, WFE, WFI 명령어와 Synchronization 익셉션
지금까지 설명한 내용을 정리하면 HCR_EL2.TWE와 HCR_EL2.TWE가 1인 조건에서 HVC, WFE, WFI와 같은 명령어를 실행하면 EL1에서 EL2로 진입됩니다. 이 부분까지 읽으면 다음과 같은 의문이 생길 수 있습니다.
"HVC, WFE, WFI와 같은 명령어를 실행하면 어떻게 EL2에 진입할까?"
HVC, WFE, WFI와 같은 명령어를 실행하면 Arm 코어는 Synchronous 익셉션을 유발하며, 다음과 같은 규칙에 따라 프로그램 카운터를 업데이트합니다.
프로그램 카운터: VBAR_EL2 + 0x400
여기서 VBAR_EL2는 EL2에 정의된 익셉션 벡터 테이블의 베이스 주소이고 0x400은 Synchronous 익셉션의 오프셋 정보입니다.
--- [정보] EL2 관점에서 해석한 익셉션 벡터 테이블
하이퍼바이저가 실행되는 EL2 관점에서 해석한 익셉션 벡터 테이블의 세부 내용은 15.4절 ‘가상화와 익셉션 벡터 테이블’을 참고하세요. ---
HVC, WFE, WFI 명령어와 익셉션 클래스
HVC, WFE, WFI 같은 명령어를 실행하면 EL2에 존재하는 같은 익셉션 핸들러에서 처리된다고 볼 수 있습니다. 그렇다면 EL1에서 HVC 명령어가 실행되어 EL2에 진입했는지 혹은 WFE/WFI와 같은 명령어를 실행해 EL2에 진입했는지 어떻게 알 수 있을까요?
Arm 코어는 HVC, WFE 와 WFI 같은 명령어를 실행하면 Synchronous 익셉션을 유발하면서 다음 표와 같은 익셉션 클래스를 ESR_EL2의 [36:21] 비트에 업데이트합니다.
표 15.1 HVC, WFE, WFI 명령어와 익셉션 클래스
HVC, WFE, WFI 명령어를 실행하면 분기되는 같은 익셉션 핸들러에서 표 15.1에서 소개한 익셉션 클래스 비트 정보는 ESR_EL2 레지스터의 [31:26] 비트에서 읽어 HVC, WFE, WFI 명령어에 따라 다른 방식으로 처리할 수 있습니다.
이어지는 절에서 Arm 아키텍처 관점에서 하이퍼바이저의 세부 동작을 설정할 수 있는 HCR_EL2 레지스터에 대해 알아봅시다.