본문 바로가기

시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리/15장: 가상화(Virtualization)

[가상화-virtualization] EL2:익셉션 핸들러 코드 소개

먼저 이번 절에서 분석할 XEN 하이퍼바이저에서 구현된 익셉션 핸들러의 전체 코드를 소개합니다. 

01  0x26a800 <hyp_traps_vector>:
02  0x26a800:    b    0x269800 <hyp_sync_invalid>
03  0x26a804:    nop
04  0x26a808:    nop
...
05  0x26a880:    b    0x26986c <hyp_irq_invalid>
06  0x26a884:    nop
07  0x26a888:    nop
...
08  0x26a900:    b    0x2698d8 <hyp_fiq_invalid>
09  0x26a904:    nop
10  0x26a908:    nop
...
11  0x26a980:    b    0x269944 <hyp_error_invalid>
12  0x26a984:    nop
13  0x26a988:    nop
...
14  0x26aa00:    b    0x269a1c <hyp_sync>
15  0x26aa04:    nop
16  0x26aa08:    nop
...
17  0x26aa80:    b    0x269a94 <hyp_irq>
18  0x26aa84:    nop
19  0x26aa88:    nop
...
20  0x26ab00:    b    0x2698d8 <hyp_fiq_invalid>
21  0x26ab04:    nop
22  0x26ab08:    nop
...
23  0x26ab80:    b    0x2699b0 <hyp_error>
24  0x26ab84:    nop
25  0x26ab88:    nop
...
26  0x26ac00:    b    0x0x269b14 <guest_sync>
27  0x26ac04:    nop
28  0x26ac08:    nop
...
29  0x26ac80:    b    0x269c78 <guest_irq>
30  0x26ac84:    nop
31  0x26ac88:    nop
...
32  0x26ad00:    b    0x269d4c <guest_fiq_invalid>
33  0x26ad04:    nop
34  0x26ad08:    nop
...
35  0x26ad80:    b    0x269de0 <guest_error>
36  0x26ad84:    nop
37  0x26ad88:    nop
...
38  0x26ae00:    b    0x269eb4 <guest_sync_compat>
39  0x26ae04:    nop
40  0x26ae08:    nop
...
41  0x26ae80:    b    0x269f90 <guest_irq_compat>
42  0x26ae84:    nop
43  0x26ae88:    nop
...
44  0x26af00:    b    0x26a06c <guest_fiq_invalid_compat>
45  0x26af04:    nop
46  0x26af08:    nop
...
47 0x26af80:    b    0x26a104 <guest_error_compat>

익셉션 핸들러의 코드를 분석하기 전에 "'익셉션 벡터 베이스 주소 + 오프셋 주소' 규칙으로 프로그램 카운터가 분기된다"라는 사실을 염두에 둡시다. 또한 VBAR_EL2 레지스터는 익셉션 핸들러의 시작 주소를 저장한다는 사실을 기억합시다. 이번 절에서 소개한 익셉션 핸들러의 베이스 주소와 오프셋 정보는 다음과 같습니다.

 익셉션 핸들러 베이스 주소: 0x26a800(VBAR_EL2)
 익셉션별 오프셋 주소의 간격: +0x80

EL2 익셉션 핸들러의 구조 파악

이해를 돕기 위해 다음 예제 코드를 보면서 설명하겠습니다.

01  0x26a800 <hyp_traps_vector>:
02  0x26a800:    17fffc00     b    0x269800 <hyp_sync_invalid>
03  0x26a804:    d503201f     nop
04  0x26a808:    d503201f     nop
...
05  0x26a880:    17fffbfb     b    0x26986c <hyp_irq_invalid>
06  0x26a884:    d503201f     nop
07  0x26a888:    d503201f     nop

'EL2 with SP_EL0'와 같이 스택 포인터를 익셉션 레벨과 상관없이 설정하는 조건에서 유발되는 익셉션의 오프셋 정보는 다음과 같습니다.

 Synchronous 익셉션: 0x0
 IRQ 익셉션: 0x80

만약 'EL2 with SP_EL0' 조건에서 Synchronous 익셉션이 유발되면 다음과 같은 규칙에 따라 프로그램 카운터가 0x26a800 주소로 분기됩니다.

 0x26a800 = 0x26a800(VBAR_EL2) + 0x0(오프셋)

그런데 'EL2 with SP_EL0' 조건에서 'IRQ Interrupt' 익셉션이 유발되면 프로그램 카운터가 0x26a880 주소로 분기됩니다. 프로그램 카운터가 바뀌는 공식은 다음과 같습니다.

 0x26a880 = 0x26a800(익셉션 벡터 베이스 주소) + 0x80(오프셋)

이번에는 하이퍼바이저가 실행되는 EL2에서 익셉션이 유발되면 프로그램 카운터가 어떻게 분기되는지 알아보겠습니다.

14  0x26aa00:    17fffc07     b    0x269a1c <hyp_sync>
15  0x26aa04:    d503201f     nop
16  0x26aa08:    d503201f     nop
...
17  0x26aa80:    17fffc05     b    0x269a94 <hyp_irq>
18  0x26aa84:    d503201f     nop
19  0x26aa88:    d503201f     nop

EL2에서 발생하는 익셉션의 오프셋 정보는 다음과 같습니다.

 Synchronous 익셉션: 0x200
 IRQ 익셉션: 0x280

만약 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에서 발생한 익셉션에 대한 처리 루틴

이어서 14 ~ 25번째 줄을 분석하겠습니다.

14  0x26aa00:    17fffc07     b    0x269a1c <hyp_sync>
15  0x26aa04:    d503201f     nop
16  0x26aa08:    d503201f     nop
...
17  0x26aa80:    17fffc05     b    0x269a94 <hyp_irq>
18  0x26aa84:    d503201f     nop
19  0x26aa88:    d503201f     nop
...
20  0x26ab00:    17fffb76     b    0x2698d8 <hyp_fiq_invalid>
21  0x26ab04:    d503201f     nop
22  0x26ab08:    d503201f     nop
...
23  0x26ab80:    17fffb8c     b    0x2699b0 <hyp_error>
24  0x26ab84:    d503201f     nop
25  0x26ab88:    d503201f     nop

14 ~ 25번째 줄은 하이퍼바이저의 코드가 실행 중인 EL2에서 익셉션이 유발됐을때 처리되는 익셉션 핸들러입니다.

이번에는 익셉션 종류별로 처리되는 세부 코드를 분석하겠습니다. 먼저 14 ~ 16번째 줄을 봅시다.

14  0x26aa00:    17fffc07     b    0x269a1c <hyp_sync>
15  0x26aa04:    d503201f     nop
16  0x26aa08:    d503201f     nop

여기서 EL2의 Synchronous 익셉션은 어떻게 유발될까요? 이 질문에는 다음과 같이 답할 수 있습니다.

EL2에서 실행되는 하이퍼바이저에서 메모리 어보트를 유발하는 명령어가 실행된 경우에 Synchronous 익셉션이 유발된다.

Arm 아키텍처 관점에서 분석하면 EL2에서 Synchronous 익셉션이 유발될 때 14번째 줄에 보이는 0x26aa00 주소로 프로그램 카운터가 분기됩니다. 14번째 줄에 "b hyp_sync"라는 명령어가 있는데, 이 명령어가 실행되면 hyp_sync 레이블로 분기합니다.

hyp_sync 레이블의 서브루틴에서는 디버깅 정보를 출력하고 크래시를 유발하는 루틴이 처리됩니다. 

이어서 17 ~ 19번째 줄을 보겠습니다.

17  0x26aa80:    17fffc05     b    0x269a94 <hyp_irq>
18  0x26aa84:    d503201f     nop
19  0x26aa88:    d503201f     nop

EL2에서 실행되는 하이퍼바이저에서 'IRQ 인터럽트'가 발생하면 17번째 줄에 보이는 0x26aa80 주소로 프로그램 카운터가 분기됩니다. 17번째 줄의 코드는 hyp_irq 레이블로 분기하는 명령어입니다.

이어서 20 ~ 22번째 줄을 보겠습니다.

20  0x26ab00:    17fffb76     b    0x2698d8 <hyp_fiq_invalid>
21  0x26ab04:    d503201f     nop
22  0x26ab08:    d503201f     nop

하이퍼바이저가 구동되는 EL2에서 'FIQ 인터럽트'가 유발되면 20번째 줄에 보이는 0x26ab00 주소로 프로그램 카운터가 분기됩니다. 그런데 XEN 하이퍼바이저는 FIQ를 지원하지 않으므로 hyp_fiq_invalid라는 레이블로 분기되며, 이후 크래시를 유발하는 코드가 실행됩니다.

 

---
[정보] FIQ는 어디서 처리할까?
대부분의 운영체제에서 FIQ는 시큐어 월드의 Trusted 커널에서 받아서 처리하도록 시스템을 설정합니다.
---

이어서 SError 익셉션이 유발되면 실행되는 23 ~ 25번째 줄 코드를 분석하겠습니다.

23  0x26ab80:    17fffb8c     b    0x2699b0 <hyp_error>
24  0x26ab84:    d503201f     nop
25  0x26ab88:    d503201f     nop

EL2에서 SError 익셉션이 유발되면 23번째 줄에 보이는 주소로 프로그램 카운터가 바뀝니다. 'b    0x2699b0 <hyp_error>' 명령어가 실행되면 hyp_error 레이블로 분기되는데, hyp_error 레이블에서는 크래시를 유발하는 루틴이 실행됩니다. 

게스트 OS가 실행되는 EL1에서 발생한 익셉션에 대한 처리 루틴

이번에는 XEN 하이퍼바이저의 익셉션 핸들러 코드 중에서 가장 중요한 루틴인 26 ~ 28번째 줄을 보겠습니다.

26  0x26ac00:    17fffbc5     b    0x269b14 <guest_sync>
27  0x26ac04:    d503201f     nop
28  0x26ac08:    d503201f     nop

게스트 OS가 실행되는 EL1에서 HVC, WFI, WFE 명령어를 실행했을 때 26번째 줄에 있는 0x26ac00 주소로 프로그램 카운터가 분기됩니다. 이런 동작을 "게스트 Exit"라고 합니다. 소프트웨어적으로 EL1에서 EL2로 진입하는 시작점이 26번째 줄이라고 볼 수 있으며, 하이퍼바이저를 분석할 때 가장 많이 리뷰하는 루틴이 게스트 Exit로 실행되는 익셉션 핸들러입니다.

이번에는 29 ~ 31번째 줄을 봅시다. 

29  0x26ac80:    17fffbfe     b    0x269c78 <guest_irq>
30  0x26ac84:    d503201f     nop
31  0x26ac88:    d503201f     nop

게스트 OS에서 ‘IRQ Interrupt’ 익셉션이 유발될 때 실행되는 코드입니다. 29번째 줄과 같이 guest_irq라는 레이블로 분기합니다.
 
EL1에서 실행되는 게스트 OS에서 설정된 인터럽트가 발생하면 일반적으로 EL1(VBAR_EL1)에서 받아 처리합니다. 만약 HCR_EL2 레지스터의 VI와 IMO 비트가 1로 설정된 경우에만 EL2(VBAR_EL2)가 EL1에서 설정된 인터럽트를 받아 처리합니다. 

이어서 EL1에서 FIQ 인터럽트 익셉션이 유발되면 실행되는 32 ~ 34번째 줄을 보겠습니다.

32  0x26ad00:    17fffc13     b    0x269d4c <guest_fiq_invalid>
33  0x26ad04:    d503201f     nop
34  0x26ad08:    d503201f     nop

32번째 줄과 같이 guest_fiq_invalid 레이블로 분기됩니다. XEN 하이퍼바이저는 FIQ를 지원하지 않으므로 guest_fiq_invalid 레이블에서는 크래시를 유발하는 루틴이 실행됩니다.
 
게스트 Exit을 처리하는 루틴 분석 

이어서 게스트 Exit가 실행되면 호출되는 guest_sync 레이블의 코드를 분석하겠습니다.

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/arch/arm/arm64/entry.S
01 guest_sync:
02        /*
03         * Save x0, x1 in advance
04         */
05        stp     x0, x1, [sp, #-(UREGS_kernel_sizeof - UREGS_X0)]
06
07        /*
08         * x1 is used because x0 may contain the function identifier.
09         * This avoids to restore x0 from the stack.
10         */
11        mrs     x1, esr_el2
12        lsr     x1, x1, #HSR_EC_SHIFT           /* x1 = ESR_EL2.EC */
13        cmp     x1, #HSR_EC_HVC64
14        b.ne    guest_sync_slowpath             /* Not a HVC skip fastpath. */
15
16        mrs     x1, esr_el2
17        and     x1, x1, #0xffff                 /* Check the immediate [0:16] */
18        cbnz    x1, guest_sync_slowpath         /* should be 0 for HVC #0 */

먼저 11 ~ 12번째 줄을 보겠습니다.

11        mrs     x1, esr_el2
12        lsr     x1, x1, #HSR_EC_SHIFT           /* x1 = ESR_EL2.EC */

11번째 줄은 익셉션 클래스의 정보를 담고 있는 esr_el2(익셉션 신드롬 레지스터)의 값을 x1 레지스터에 로딩하는 동작입니다. 11번째 줄에서 esr_el2 레지스터의 값을 x1 레지스터에 로딩하는 이유는 무엇일까요? esr_el2 레지스터의 [31:26] 비트에 익셉션이 유발된 세부 원인을 나타내는 익셉션 클래스 비트 정보가 저장돼 있기 때문입니다.

12번째 줄은 x1 레지스터의 값을 #HSR_EC_SHIFT만큼 오른쪽으로 비트 시프트 연산한 결과를 x1 레지스터에 저장합니다. 여기서 HSR_EC_SHIFT는 다음 매크로 선언부와 같이 26입니다.

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/include/asm-arm/processor.h
#define HSR_EC_SHIFT                26

11번째 줄에서 esr_el2 레지스터의 값을 x1 레지스터에 로딩한 다음에 오른쪽으로 26만큼 비트 시프트(12번째 줄) 연산을 수행합니다. 그 결과, 익셉션 클래스를 나타내는 정숫값을 x1 레지스터가 저장합니다.

11 ~ 12번째 줄의 동작은 12번째 줄에 보이는 주석처럼 다음과 같이 표기할 수 있습니다.

 'x1 = ESR_EL2.EC'

ESR_EL2는 EL2에서 액세스하는 익셉션 신드롬 레지스터이고, EC는 익셉션 클래스를 나타냅니다.
 
이어서 13 ~ 14번째 줄을 보겠습니다.
 
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입니다.

출처: https://github.com/xen-project/xen/blob/stable-4.15/xen/include/asm-arm/processor.h
#define 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 아키텍처에서 정의된 익셉션 벡터 테이블에 따라 구현됐다는 사실을 알 수 있습니다. 

 
<강의 영상>