본문 바로가기

시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리/6장: Armv8 - 익셉션 레벨

[ARMv8]ARM64 - 각 익셉션(Exception) 레벨 소개

이번 포스팅에서는 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 아키텍처의 구조와 원리' 저자>