Exception -7- (ARM64 Vector)

<kernel v5.4>

ARM64 Exception

ARM64 Exception 종류

ARM64 Exception 벡터에 포함된 exception 종류는 다음과 같다.  Synchronous를 제외하고 나머지 3개는 비동기이다.

  • Synchronous
    • Instruction Abort
      • MMU를 통해 명령 fetch 에러
        • 예) Execute Never로 마킹된 매핑 주소로 접근하는 경우 발생한다.
    • Data Abort
      • MMU를 통해 데이터 fetch 에러
        • 예) 권한 실패 및 SP 또는 PC alignment 체크에 의해 발생한다.
    • Exception Generating 명령들
      • SVC(Supervisor Call)
        • 유저 모드에서 OS 서비스 요청 (syscall, 동기 exception)
      • HVC
        • Guest OS에서 Hypervisor 요청 (동기 exception)
      • SMC
        • normal(non-secure) 월드에서 secure 월드 서비스 요청 (동기 exception)
  • IRQ
    • 디바이스에서 인터럽트 서비스를 요청하는 경우 발생한다.
    • irq 처리 중 fiq 요청에 대한 preemption은 가능하지만 irq 재진입(irq preemption)은 허용하지 않는다.
    • ARM32 아키텍처는 irq preemption을 허용하지만 ARM 리눅스 커널이 이를 허용하지 않는 설계로 구현되어 있다.
    • Pesudo-NMI를 지원하는 ARM64 아키텍처와 ARM64 커널이 사용될 때 nmi 관련 디바이스에 한해 irq preemption을 허용한다.
  • FIQ
    • IRQ보다 더 빠른 인터럽트 서비스를 요청하는 경우 발생한다.
    • 리눅스 커널의 fiq 핸들러 함수인 handle_fiq_as_nmi()에 처리 코드를 추가하여 사용하여야 한다.
      • 디폴트 처리로 아무것도 하지 않는다. (nop)
      • 예) rpi2의 경우 usb 호스트 드라이버(dwc_otg)에서 사용한다.
    • irq 보다 높은 우선 순위를 갖고, irq 처리 중에도 preemption될 수 있다.
    • 일반적으로 secure 펌웨어에서 fiq를 처리하고, 리눅스 커널에서는 fiq를 처리하지 않는다.
  • SError (System Error)
    • 비동기 Data Abort
      • 예) 캐시 라인을 시스템 메모리에 writeback하느 과정에서 중단되는 경우 발생한다.

 

Reset

최고 레벨의 특별한 exception 이다. 리셋되는 주소는 IMPLEMENTATION DEFINED이고, RVBAR_ELn 레지스터로 지정할 수 있다.

 

Exception 레벨별 벡터 테이블

다음 그림은 ARMv8 아키텍처에서 운영되는 각 Exception 레벨에서 운영중인 벡터들을 보여준다.

  • 파란색 글씨는 ARM64 커널이 디폴트로 지원하는 핸들러들이다. (KVM 포함)

 

리눅스 커널의 EL2 부팅

예) rpi4가 부팅하면 Hyper 모드로 호스트 OS가 동작하도록 EL2로 부팅 후 EL2용 벡터와 핸들러들을 설치한다. 그 후 EL1용 벡터와 핸들러들을 설치한다. , Exception 발생 시 일부 코드(현재 HVC 서비스 3개)만 EL2에서 동작시키고, 나머지 커널 코드의 실행은 EL1에서 수행한다. 즉 처음 부팅된 리눅스 커널이 하이퍼바이저 역할을 일부 수행하는 것이다. 그 후 Qemu/KVM을 사용하여 동작시키는 Guest OS들은 EL1에 벡터와 핸들러들을 설치하고, 커널 코드 운영도 EL1에서 수행한다.

 

Exception 벡터 테이블 주소

벡터 테이블의 주소는 AArch64 및 AArch32에 대해 다음과 같은 레지스터를 사용하여 지정한다.  리눅스 커널의 경우 boot cpu는 __primary_switched: 레이블에서 EL1용 벡터 위치를 VBAR_EL1에 지정하고, 나머지 cpu들은 __secondary_switched: 레이블에서 지정하였다. 하이퍼모드로 운영되는 경우 install_el2_stub: 레이블에서 EL2용 벡터 위치를 VBAR_EL2에 지정한다.

  • for AArch64
    • VBAR_EL3,  VBAR_EL2,  VBAR_EL1
  • for AArch32
    •  HVBAR

 

Vector 선언

EL1 벡터

vectors

arch/arm64/kernel/entry.S

/*
 * Exception vectors.
 */
        .pushsection ".entry.text", "ax"

        .align  11
ENTRY(vectors)
        kernel_ventry   1, sync_invalid                 // Synchronous EL1t
        kernel_ventry   1, irq_invalid                  // IRQ EL1t
        kernel_ventry   1, fiq_invalid                  // FIQ EL1t
        kernel_ventry   1, error_invalid                // Error EL1t

        kernel_ventry   1, sync                         // Synchronous EL1h
        kernel_ventry   1, irq                          // IRQ EL1h
        kernel_ventry   1, fiq_invalid                  // FIQ EL1h
        kernel_ventry   1, error                        // Error EL1h

        kernel_ventry   0, sync                         // Synchronous 64-bit EL0
        kernel_ventry   0, irq                          // IRQ 64-bit EL0
        kernel_ventry   0, fiq_invalid                  // FIQ 64-bit EL0
        kernel_ventry   0, error                        // Error 64-bit EL0

#ifdef CONFIG_COMPAT
        kernel_ventry   0, sync_compat, 32              // Synchronous 32-bit EL0
        kernel_ventry   0, irq_compat, 32               // IRQ 32-bit EL0
        kernel_ventry   0, fiq_invalid_compat, 32       // FIQ 32-bit EL0
        kernel_ventry   0, error_compat, 32             // Error 32-bit EL0
#else
        kernel_ventry   0, sync_invalid, 32             // Synchronous 32-bit EL0
        kernel_ventry   0, irq_invalid, 32              // IRQ 32-bit EL0
        kernel_ventry   0, fiq_invalid, 32              // FIQ 32-bit EL0
        kernel_ventry   0, error_invalid, 32            // Error 32-bit EL0
#endif
END(vectors)
  • 총 16개의 벡터 엔트리들로 구성되며, 각각의 엔트리에 0x80 바이트를 제공한다.
    • ARMv7이 엔트리당 4바이트의 공간만을 제공한 것에 비해 ARMv8은 충분한 크기를 제공하는 것을 알 수 있다.
  • 16개의 벡터 엔트리들을 4개씩 전체 4 세트로 묶는 경우 각 세트들은 다음과  같은 특징을 가지고있다.
    • 첫 번째, 현재 코드가 수행 중인 exception 레벨과 동일한 exception 레벨에서 excetion이 발생하였고, SP0를 사용하고 있었다.
      • 리눅스 커널은 이에 해당하는 핸들러들을 처리하지 않도록 invalid 핸들러들로 연결되어 있다.
    • 두 번째, 현재 코드가 수행 중인 exception 레벨과 동일한 exception 레벨에서 excetion이 발생하였고, SPn(n>0)을 사용하고 있었다.
      • 커널(EL1)에서 exception이 발생하였고, 이 때 SP1을 사용 중인 경우이다.
      • 예) sync exception이 발생하는 경우 el1_sync: 레이블로 이동하여 sync 관련 루틴을 수행한다.
    • 세 번째, 현재 코드가 수행 중인 exception 레벨보다 하위 exception 레벨이며 AArch64 수행 중이다.
      • AArch64 유저(EL0)에서 exception이 발생한 경우이다.
    • 네 번째, 현재 코드가 수행 중인 exception 레벨보다 하위 exception 레벨이며 AArch32 수행 중이다.
      • AArch32 유저(EL0)에서 exception이 발생한 경우이다.

 

EL2 벡터

__kvm_hyp_vector

arch/arm64/kernel/entry.S

ENTRY(__kvm_hyp_vector)
        invalid_vect    el2t_sync_invalid       // Synchronous EL2t
        invalid_vect    el2t_irq_invalid        // IRQ EL2t
        invalid_vect    el2t_fiq_invalid        // FIQ EL2t
        invalid_vect    el2t_error_invalid      // Error EL2t

        valid_vect      el2_sync                // Synchronous EL2h
        invalid_vect    el2h_irq_invalid        // IRQ EL2h
        invalid_vect    el2h_fiq_invalid        // FIQ EL2h
        valid_vect      el2_error               // Error EL2h

        valid_vect      el1_sync                // Synchronous 64-bit EL1
        valid_vect      el1_irq                 // IRQ 64-bit EL1
        invalid_vect    el1_fiq_invalid         // FIQ 64-bit EL1
        valid_vect      el1_error               // Error 64-bit EL1

        valid_vect      el1_sync                // Synchronous 32-bit EL1
        valid_vect      el1_irq                 // IRQ 32-bit EL1
        invalid_vect    el1_fiq_invalid         // FIQ 32-bit EL1
        valid_vect      el1_error               // Error 32-bit EL1
ENDPROC(__kvm_hyp_vector)

Guest OS에서 exception이 발생한 경우 el1_sync, el1_irq, el1_error 레이블로 이동한다.

 

다음 그림은 exception이 발생하였을 때 호출되는 exception 벡터 엔트리 레이블들을 가리킨다.

 

Vector가 저장되는 섹션 위치

ENTRY_TEXT

include/asm-generic/vmlinux.lds.h

#define ENTRY_TEXT                                                      \
                ALIGN_FUNCTION();                                       \
                __entry_text_start = .;                                 \
                *(.entry.text)                                          \
                __entry_text_end = .;

.extry.text 섹션에 벡터코드가 포함되며 ENTRY_TEXT로 정의되어 있다.

 

arch/arm64/kernel/vmlinux.lds.S

.       .head.text : {
                _text = .;
                HEAD_TEXT
        }
        .text : {                       /* Real text segment            */
                _stext = .;             /* Text and read-only data      */
                        __exception_text_start = .;
                        *(.exception.text)
                        __exception_text_end = .;
                        IRQENTRY_TEXT
                        SOFTIRQENTRY_TEXT
                        ENTRY_TEXT
                        TEXT_TEXT
                        SCHED_TEXT
                        CPUIDLE_TEXT
                        LOCK_TEXT
                        KPROBES_TEXT
                        HYPERVISOR_TEXT
                        IDMAP_TEXT
                        HIBERNATE_TEXT
                        TRAMP_TEXT
                        *(.fixup)
                        *(.gnu.warning)
                . = ALIGN(16);
                *(.got)                 /* Global offset table          */

벡터코드가 포함된 ENTRY_TEXT는 .text 영역에 포함되어 있다.

 

kernel_ventry 매크로

arch/arm64/kernel/entry.S

        .macro kernel_ventry, el, label, regsize = 64
        .align 7
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
alternative_if ARM64_UNMAP_KERNEL_AT_EL0
        .if     \el == 0
        .if     \regsize == 64
        mrs     x30, tpidrro_el0
        msr     tpidrro_el0, xzr
        .else
        mov     x30, xzr
        .endif
        .endif
alternative_else_nop_endif
#endif

        sub     sp, sp, #S_FRAME_SIZE
#ifdef CONFIG_VMAP_STACK
        /*
         * Test whether the SP has overflowed, without corrupting a GPR.
         * Task and IRQ stacks are aligned to (1 << THREAD_SHIFT).
         */
        add     sp, sp, x0                      // sp' = sp + x0
        sub     x0, sp, x0                      // x0' = sp' - x0 = (sp + x0) - x0 = sp
        tbnz    x0, #THREAD_SHIFT, 0f
        sub     x0, sp, x0                      // x0'' = sp' - x0' = (sp + x0) - sp = x0
        sub     sp, sp, x0                      // sp'' = sp' - x0 = (sp + x0) - x0 = sp
        b       el\()\el\()_\label

0:
        /*
         * Either we've just detected an overflow, or we've taken an exception
         * while on the overflow stack. Either way, we won't return to
         * userspace, and can clobber EL0 registers to free up GPRs.
         */

        /* Stash the original SP (minus S_FRAME_SIZE) in tpidr_el0. */
        msr     tpidr_el0, x0

        /* Recover the original x0 value and stash it in tpidrro_el0 */
        sub     x0, sp, x0
        msr     tpidrro_el0, x0

        /* Switch to the overflow stack */
        adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0

        /*
         * Check whether we were already on the overflow stack. This may happen
         * after panic() re-enables interrupts.
         */
        mrs     x0, tpidr_el0                   // sp of interrupted context
        sub     x0, sp, x0                      // delta with top of overflow stack
        tst     x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
        b.ne    __bad_stack                     // no? -> bad stack pointer

        /* We were already on the overflow stack. Restore sp/x0 and carry on. */
        sub     sp, sp, x0
        mrs     x0, tpidrro_el0
#endif
        b       el\()\el\()_\label
        .endm

Exception이 발생되면 16개의 exception vector 엔트리 중 해당하는 exception 위치로 jump 되어 수행한다.

  • 코드 라인 1에서 인자 @el 은 exception 레벨을 의미하고, @label은 jump할 라벨, 그리고 @regsize는 시스템 레지스터의 사이즈를 의미한다.
  • 코드 라인 2에서 .align 7을 지정하여 벡터간 128바이트 단위로 정렬하도록 한다.
  • 코드 라인 3에서 고성능 아키텍처에서 문제가 되었던 추측 공격(Speculation 공격, side 채널 공격)을 통해 유저 영역에서 MMU 권한 체크를 우회하여 커널 액세스가 가능해지는 버그가 있다. 이러한 커널 액세스를 방지하기 위해 유저 영역에서 커널 영역을 완전히 분리하여 액세스를 방지하는 CONFIG_UNMAP_KERNEL_AT_EL0 커널 옵션이며 이를 설정하여 사용하는 경우 성능을 약간 희생한다.
    • 참고로 exception 페이지는 trampoline 페이지를 통해 매핑을 허용한다.
  • 코드 라인 4에서 위 보안 기능을 사용하기 위해 fake cpu 기능(capability) 하나를 추가하였다. 이 ARM64_UNMAP_KERNEL_AT_EL0 capability를 지원하는 시스템에서만 코드가 동작하게 제한한다.
  • 코드 라인 5~12에서 EL0 AArch64(64bit user application)에서 exception이 발생한 경우 TPIDRRO_EL0 레지스터 값을 x30 레지스터에 백업하고, 0으로 기록한다. 만일 EL0 AArch32(32bit user application)에서 exception이 발생한 경우 x30을 0으로 기록한다.
  • 코드 라인 16에서 context 전환 목적으로 현재 레지스터들을 백업하기 위해 pt_regs 구조체 사이즈(S_FRAME_SIZE)만큼  스택을 증가시킨다. (growth down)
  • 코드 라인 17에서 CONFIG_VMAP_STACK 커널 옵션을 사용하면 스택을 생성할 때 vmalloc 공간을 이용하여 생성한다. 스택은 물리적으로 연속된 공간이 필요 없고, 가상 주소 공간만 연속되면 문제 없으므로 vmalloc 공간에 리니어 매핑하여 사용하는 것이 메모리의 파편화 방지에 도움이 된다.
  • 코드 라인 22~23에서 sp 레지스터와 x0 레지스터 값을 교환할 목적으로먼저 x0 <- sp를 수행한다.이때 sp에는 sp + x0 값이 잠시 담겨있다.
    • 두 레지스터 간의 교환이 필요할 때 DRAM 메모리를 사용할 수 있다. 그러나 exception 루틴에서 DRAM에 접근하면 수백 사이클의 대기 시간이 소요되어 매우 느려지므로 수학적으로 가감 연산만을 사용하여 두 레지스터를 교환할 수 있다.
  • 코드 라인 24에서 sp 레지스터를 범용 레지스터인 x0 레지스터로 옮겨야 tbnz 명령을 사용하여 특정 비트가 설정되었는지 여부를 확인할 수 있다. 여기서 스택 사이즈 또는 태스크 사이즈로 정렬되었는지 여부를 테스트하여 정렬되지 않은 경우 0: 레이블로 이동한다.
    • 스택 레지스터의 THREAD_SHIFT 위치의 비트가 설정된 경우 스택 정렬되지 않은 것으로 판단한다.
  • 코드 라인 25~26에서 x0 레지스터와 sp 레지스터 값을 다시 원래대로 복귀시킨다.
  • 코드 라인 27에서 이 매크로에 전달된 @el 값과 @label을 연결하여 만든 레이블로 점프한다.
    • 문자열과 매크로 인자 값을 연결하는 \() 문자는 접착재 같이 사용되므로 제거한다.
    • 예) \el=1, \label=irq
      • b el\()\el\()_\label -> “b el1_irq
  • 코드 라인 29에서 스택이 정렬되지 않은채로 호출되어 이동해온 0 레이블이다. 이 레이블에서는 깨진 스택 대신 static하게 만들어진 per-cpu 오버플로우용 스택을 사용하는 코드로 진행된다.
  • 코드 라인 37에서 x0 레지스터에는 sp 레지스터 값과 바뀐채로 이동해왔다. 이 값을 잠시 tpidr_el0에 저장해둔다.
    • tpidr_el0 <- sp
  • 코드 라인 40~41에서 원래 x0 레지스터 값으로 복원하여 잠시 tpidrro_el0에 저장해둔다.
    • tpidrro_el0 <- x0
  • 코드 라인 44에서 현재 cpu에 해당하는 오버 플로우용 스택을 지정한다.
    • 스택은 growth down으로 동작하므로 스택의 높은 주소를 지정한다.
    • 정적 per-cpu 선언된 overflow_stack은 4K 크기로 사용된다.
  • 코드 라인 50~53에서 백업해둔 스택 값을 x0 레지스터로 읽어와서 현재 스택 범위 이내에 있는지 비교하여 범위 밖인 경우 __bad_stack 레이블로 이동한다.
  • 코드 라인 56~59에서 x0 레지스터와 스택을 다시 복구하고 이 매크로에 전달된 @el 값과 @label을 연결하여 만든 레이블로 점프한다.

 

adr_this_cpu 매크로

arch/arm64/include/asm/assembler.h

.       /*
         * @dst: Result of per_cpu(sym, smp_processor_id()) (can be SP)
         * @sym: The name of the per-cpu variable
         * @tmp: scratch register
         */
        .macro adr_this_cpu, dst, sym, tmp
        adrp    \tmp, \sym
        add     \dst, \tmp, #:lo12:\sym
alternative_if_not ARM64_HAS_VIRT_HOST_EXTN
        mrs     \tmp, tpidr_el1
alternative_else
        mrs     \tmp, tpidr_el2
alternative_endif
        add     \dst, \dst, \tmp
        .endm

현재 cpu가 접근할 per-cpu 변수 @sym의 주소를 @dst에 담아 반환한다. (예: @dst <- 현재 cpu로 접근할 per-cpu @sym) 이 매크로는 스크래치 레지스터 @tmp가 하나 소모된다.

  • 코드 라인 2에서 인자로 전달받은 per-cpu 변수 @sym에 대한 페이지 단위로 절삭된 주소를 @tmp에 담는다.
    • 현재 cpu의 pc 주소로 부터 4G 이내의 상대 주소(relative addressing)를 사용하여 알아온다.
    • 예) @sym(overflow_stack 주소 0x1234_5678)이 현재 cpu의 주소로부터 +-4G 이내인 조건에서 페이지 단위로 절삭된 0x1234_5000 값을 @tmp에 대입한다.
  • 코드 라인 3에서 @sym의 하위 12비트를 @tmp에 더해 @dst에 산출한다.
    • 예) @dst(0x1234_5678) = @tmp(0x1234_5000) + 하위 12비트 @sym(0x678)
  • 코드 라인 4~8에서 현재 cpu에 대한 per-cpu offset 값이 담겨있는 tpidr_el1(하이퍼 바이저 지원시 tpidr_el2) 값을 @tmp에 읽어온다.
  • 코드 라인 9에서 per-cpu offset 값이 적용된 per-cpu 변수 overflow_stack 주소를 @dst에 담아 반환한다.

 

adr vs adrp

  • adr 명령은 심볼의 주소를 상대 주소(relative addressing)를 사용하여 알아온다. 20비트(추가 1비트는 부호) 상대 주소 기법을 사용하므로 현재 cpu pc 주소로부터 심볼 위치까지 지원하는 범위가 +-1M 주소로 제한된다.
  • adrp 명령은 adr과 유사하지만 더 넓은 범위를 지원하기 위해 심볼 주소의 하위 12비트를 사용하지 않는 20비트(추가 1비트는 부호, 12비트 좌쉬프트) 상대 주소 기법을 사용하므로 더욱 넓은 범위인 +-4G 주소 범위에 한하여 페이지 단위로 절삭된 주소를 알아온다.

 

TPID 레지스터 관련

TPIDR_EL0 (Thread ID Register EL0)
  • 유저(EL0) 영역에서 TLS(Thread Location Storage)를 사용하기 위해 스레드 데이터가 위치한 베이스 주소를 저장한다.
  • Exception이 발생할 때 vmap 스택에서 overflow가 발생하여 overflow용 스택을 사용하는 경우 이 레지스터를 임시 저장 영역으로 사용된다.
  • TPIDR_EL0의 경우 커널 영역(EL1) 및 유저 영역(EL0)에서 읽고 쓰기가 가능하다.

 

TPIDRRO_EL0 (Thread ID Register EL0 with User Read Only)
  • TPIDR_EL0와 같은 목적으로 유저(EL0) 영역에서 TLS(Thread Location Storage)를 사용하기 위해 스레드 데이터가 위치한 베이스 주소를 저장한다.
  • Exception이 발생할 때 vmap 스택에서 overflow가 발생하여 overflow용 스택을 사용하는 경우 이 레지스터도 임시 저장 영역으로 사용된다.
  • 커널 영역(EL1)에서 읽고 쓰기가 가능하며, 유저 영역(EL0)에서는 읽기만 가능하다.

 

TPIDR_EL1 (Thread ID Register EL1)
  • per-cpu 베이스 주소를 기억하기 위해 사용한다.
  • 커널(EL1) 영역에서 읽고 쓸 수 있다.

 

참고

 

 

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다