책은 ARMv7과 ARMv8 양쪽 모두를 다루고 있습니다.
이미 이전 포스트들에서 ARMv7에 대해서 충분히 공부했기 때문에, 이번 포스트에서는 ARMv8에 집중해서 다룹니다.
ARMv7 관련 내용 중 일부는 해당 내용을 자세히 다룬 포스트를 주석으로 링크 달고 생략하도록 하겠습니다.
4. ARMv7의 동작모드
다른 포스트들(링크1, 링크2)에서 자세히 공부했던 내용이므로 중요한 내용만 집고 넘어갑니다.
4.1. 동작모드들 설명
ARMv7은 크게 특권모드와 비특권모드 2가지로 나뉩니다.
모드 | 심볼 | 동작모드 | CPSR [4:0] |
설명 |
비특권모드 | USR | 유저모드 | 0x10 | 일반 유저 애플리케이션이 실행되는 동작모드 |
특권모드 | FIQ | Fast 인터럽트 |
0x11 | 요청받은 인터럽트를 NVIC가 관리하고 처리하는 동작모드 |
IRQ | 인터럽트 | 0x12 | ||
SVC | 슈퍼바이저 | 0x13 | SVC 명령어로 요청받은 커널 및 OS의 자원이 실행되는 동작모드 | |
ABT | Abort | 0x17 | Data 및 Prefetch abort 발생 시 처리하는 동작모드 | |
UND | Undefined | 0x1B | 정의되지 않은 명령어, Undefined 익셉션 발생 시 처리하는 동작모드 | |
SYS | System | 0x1F | USR 모드의 레지스터 뷰를 공유하는 동작모드 |
ARMv7 아키텍처를 이용한 프로세서를 이용한다고 해서 모든 상황에서 모든 동작모드를 구현할 필요는 없습니다.
프로젝트의 usecase에 맞게 구현할 동작모드들을 선택하고 구현하면 됩니다.
- (상황 ①) 출력장치에 아스키코드를 출력하는 간단한 프로그램
= USR이든 SVC든 어떤 모드에서 동작하던지 상관없음 - (상황 ②) 블루투스 기능이 있는 소형 MP3, 블루투스 요청 때 인터럽트 걸림
= IRQ 모드 및 적절한 인터럽트 핸들러를 구현해야 함 - (상황 ③) 다양한 유저 애플리케이션이 커널 및 OS 위에서 동작함
= 위에서 언급한 모든 동작모드 및 핸들러를 상세히 구현해야 함
4.2. 동작모드 변경 방법
유저모드에서 특권모드로 이동할 때는 적절한 트리거가 있어야 합니다.
SVC 명령어를 호출하거나, 인터럽트가 걸리거나, 어보트가 발생하거나 하는 식으로요.
또한, 유저모드는 CPSR에 직접적으로 접근할 권한이 없습니다.
반면, 각 특권모드끼리 이동은 자유롭습니다.
동작모드를 변경하는 방법은 두 가지가 있습니다.
방법 ① - 직접 변경: CPSR 값 변경
MSR CPSR_c, #Mode_FIQ:OR:I_Bit:OR:F_Bit
MOV SP, R0
SUB R0, R0, #FIQ_Stack_Size
MSR CPSR_c, #Mode_IRQ:OR:I_Bit:OR:F_Bit
MOV SP, R0
MSR 명령어를 이용해서 직접 CPSR의 mode 필드를 변경하는 방법입니다.
위 코드를 보시면, 미리 정의된 매크로들을 이용해서 `CPSR_c`를 수정하는 것을 볼 수 있습니다. `CPSR_c`는 CPSR 레지스터의 하위 1바이트를 의미하며, IRQ/FIQ 활성화 필드와 mode 필드가 포함돼 있는 중요한 부분입니다.
방법 ② - 간접 변경: SPSR 값 변경 후 MOVS 명령어 사용
- SPSR_<mode> 레지스터의 [4:0] mode 필드를 바꿉니다.
- LR_<mode>(R14) 레지스터에 동작모드가 변경되며 분기할 주소를 넣습니다.
- `SUBS PC, LR, #4` 또는 `MOVS PC, LR` 명령어로 분기합니다. (동시에 동작모드도 바뀝니다.)
이전 포스트에서도 잠깐 설명했지만, S 접미사가 붙은 명령어는 연산결과의 특징 (캐리발생, 오버플로우 발생, 포화 발생 등)을 CPSR의 flag 필드에 업데이트 하는 용도로 사용합니다. 하지만, 피연산자가 PC일 때는 특수한 동작 - SPSR 값을 CPSR로 옮기는 동작을 합니다.
실제 리눅스 커널의 코드를 통해서도 동작모드를 변경하는 모습을 어렵지 않게 찾아볼 수 있습니다.
`cpsr_c` 필드를 MSR 명령어를 이용해서 변경하는 모습을 볼 수 있습니다.
`spsr_cxsf` 필드 (SPSR 레지스터의 하위 1바이트, [7:0])를 MSR 명령어를 이용해서 변경한 뒤
movs 명령어를 이용해서 분기 + 동작모드 변경을 동시에 수행하는 것을 볼 수 있습니다.
4.3. (★) 모든 익셉션은 슈퍼바이저 모드에서?
ARMv7에서 리눅스 커널이 동작할 때 신기한 특징이 있습니다.
익셉션도 인터럽트도 모두 SVC mode에서 처리된다는 점입니다.
조금 어이없는 내용입니다. 왜냐하면, 지금까지 배운 내용대로라면,
인터럽트는 IRQ/FIQ mode에서 처리하고,
data/prefetch abort는 DABT/PABT mode에서 처리되는건줄 알았는데 말입니다.
(리눅스 커널 기준) 익셉션/인터럽트 발생 시 해당하는 동작모드로 '진입'은 합니다만, '처리'는 SVC에서 맡아서 합니다.
즉, 유저 애플리케이션을 실행하다가 IRQ가 발생했다면 순서대로
{ ① USR → ② IRQ → ③ IRQ 익셉션 벡터 진입 → ④ SVC ⑤ 인터럽트 핸들러 (ISR) 실행 → ⑥ USR 복귀 }로 동작합니다.
즉, 시스템 자원에 접근할 수 있는 SVC mode에 모든 익셉의 처리를 집중해서 시스템을 간결하게 만든 것입니다.
바로 위에서 봤던 코드 (arch/arm/kernel/entry-armv.S)를 빌드하면 다음과 같은 어셈블리가 나옵니다.
<vector_irq>
@ 복귀주소 보정
0xFFFF1020: sub lr, lr, #4
0xFFFF1024: stm sp, {r0, lr}
@ Save r0, lr_<exception> (parent PC) and spsr_<exception> (parent CPSR)
0xFFFF1028: mrs lr, SPSR
0xFFFF102C: str lr, [sp, #8]
@ SVC mode로 변경할 준비
0xFFFF1030: mrs r0, CPSR
0xFFFF1034: eor r0, r0, #1 @ IRQ(0x12), 0번 bit set하면 SVC(0x13)
0xFFFF1038: msr SPSR_fsxc, r0 @ SPSR_fsxc에 저장, mode 필드 SVC로 갱신
0xFFFF103C: and lr, lr, #15
0xFFFF1040: mov r0, sp
0xFFFF1044: ldr lr, [pc, lr, lsl, #2]
0xFFFF1048: movs pc, lr @ SVC mode로 변경되면서 ISR 하러 이동
주석을 보면 이해하기 쉬울 것 같습니다.
IRQ 익셉션이 유발되면 HW적으로 자동으로 IRQ 모드로 변경되면서 `vector_irq` 레이블로 분기됩니다.
그리고 위와같이 R0, LR_irq, SPSR_irq를 스택에 백업해 둔 뒤 SVC 모드로 변경되면서 ISR을 호출하러 갑니다.
5. (★) ARMv8의 익셉션 레벨
거듭 반복해서 설명하지만, ARMv7에서 ARMv8로 넘어오면서 아키텍처 밑바닥부터 많은 부분이 바뀌었습니다. 가장 큰 차이점은 동작모드별로 동작하던 ARMv7에서 익셉션 레벨별로 독립된 실행환경을 가지며 동작하게 됐다는 점입니다.
5.1. 익셉션 레벨 정리
위 그림은 ARMv8의 익셉션레벨 아키텍처를 보여주는 아주 중요한 그림이며 눈에 익을 때까지 많이 보게 될 겁니다.
저번 포스트 (링크)의 2.3.2절에서 익셉션레벨이 무엇인지 아주 간단하게 정리했습니다.
'익셉션레벨'은 권한레벨이자 동작모드를 의미하는 서로 독립된 실행환경을 말합니다.
- 익셉션레벨 0 (EL0)에서는 ARMv7의 USR 모드와 같이 유저 애플리케이션이 동작합니다.
- Unprivileged 모드이므로 권한이 없어서 시스템 레지스터 및 시스템 자원에 접근할 수 없습니다.
- 즉, 메모리 접근에 일부 제약이 있고, IRQ, MMU, 캐시 같은 주요 기능을 제어할 수 없으며, TCR_EL1 같은 시스템 레지스터를 직접 설정할 수 없습니다.
- 익셉션레벨 1 (EL1)은 커널 및 OS가 동작하는 곳입니다.
- 평소에 EL0에서 동작하던 유저 애플리케이션은
① 시스템 자원에 접근하기 위해 SVC 명령어를 사용하거나
② 인터럽트 (IRQ)가 발생하거나
③ 메모리 어보트가 발생하면 EL1으로 진입 (트랩 됐다라고 표현)합니다. - 위 그림을 보면, 커널 위에서 다양한 애플리케이션이 실행되고 있습니다.
- 평소에 EL0에서 동작하던 유저 애플리케이션은
- 익셉션레벨 2 (EL2)는 하이퍼바이저 (Hypervisor)가 동작하는 곳입니다.
- 하이퍼바이저는 하나의 시스템 위에서 2개 이상의 OS를 동작할 때 다양한 커널 및 OS의 자원을 관리해 주는 역할을 합니다.
(하이퍼바이저에 대한 자세한 내용은 다음 포스트에서 배웁니다.) - 위 그림을 보면, 하이퍼바이저 위에서 서로 다른 OS 2개가 동작하고 있습니다.
- OS가 2개 이상 동작한다는 게 처음에는 잘 와닿지 않아서 이해하기 어려울 수 있습니다.
- 대표적인 하이퍼바이저 적용 예시로는 차량 내 인포테인먼트 시스템이 있습니다.
- 계기판은 보안성과 실시간성이 높은 RTOS가 위에서 동작, 내비게이션은 일반/범용 리눅스 커널 위에서 동작합니다.
- 하이퍼바이저는 하나의 시스템 위에서 2개 이상의 OS를 동작할 때 다양한 커널 및 OS의 자원을 관리해 주는 역할을 합니다.
- 익셉션레벨 3 (EL3)는 시큐어모니터 (Secure Monitor)가 동작하는 곳입니다.
- 위 그림을 보면 좌측 연파랑색으로 표시된 부분이 Non-secure 영역, 우측 군청색으로 표시된 부분이 Secure 영역입니다.
- 이렇게 보안이 중요한 영역과 그렇지 않은 영역을 분리한 구조를 트러스트존(TrustZone)이라고 합니다.
(트러스트존에 대해서는 다음 포스트에서 자세히 배웁니다.)- 굳이 '실행 영역'을 나눴다는 것이 처음에는 잘 와닿지 않아서 이해하기 어려울 수 있습니다.
- 정보누출이 돼서는 안 되는 민감한 정보를 다루거나, 중요한 자원에 접근할 때는 시큐어 영역에서 실행됩니다.
- 대표적인 트러스트존 예시로 카카오톡과 은행어플이 있습니다. 카카오톡은 논-시큐어 영역, 은행어플은 시큐어 영역에서 실행됩니다.
- Non-secure 영역에서 secure 영역으로 무언가를 처리해 달라고 요청하는 경우, '반드시' 시큐어모니터를 거치게 됩니다.
- ARMv7에서 반드시 모든 동작모드를 구현할 필요가 없던 것처럼,
시스템의 usecase와 스펙에 따라 EL0, EL1은 필수, EL2, EL3는 선택입니다.
5.2. 익셉션레벨 관련 레지스터
ARMv8의 PSTATE의 [4:0] 비트(`PSTATE.EL`)는 현재 동작하는 익셉션레벨을 나타냅니다. 하지만 우리는 지난 포스트에서 PSTATE는 특수 레지스터로 분류되므로 직접 접근할 수 없다고 배웠습니다. 따라서 `PSTATE.EL` 필드를 '읽을 수 있도록' 따로 'CurrentEL'이라는 레지스터가 있습니다. MRS 명령어를 이용해서 'CurrentEL' 레지스터를 읽으면, 특히 [3:2] 2-bit를 읽으면, 현재 동작중인 익셉션 레벨이 무엇인지 바로 확인할 수 있습니다. (`MRS <Xt>, CurrentEL`)
익셉션 레벨 (스택포인터) | PSTATE.M[3:0] | CurrentEL | 16진수 | 익셉션 레벨 |
EL0t | 0b00_00 | 0b00_00 | 0x0 | EL0 |
EL1t | 0b01_00 | 0b01_00 | 0x4 | EL1 |
EL1h | 0b01_01 | 0b01_01 | 0x5 | |
EL2t | 0b10_00 | 0b10_00 | 0x8 | EL2 |
EL2h | 0b10_01 | 0b10_01 | 0x9 | |
EL3t | 0b11_00 | 0b11_00 | 0xC | EL3 |
EL3h | 0b11_01 | 0b11_01 | 0xD |
5.2.1. 익셉션레벨별로 나뉜 스택공간과 스택포인터
상단의 표를 보면 익셉션레벨에 `-h` 접미사와 `-t` 접미사가 붙은 것을 확인할 수 있습니다.
- 앞서 익셉션레벨은 '서로 독립된' 실행공간이라고 언급했는데, 서로 완전히 독립되기 위해 스택과 스택포인터도 따로따로 가져야 합니다. (`-h`접미사)
- 즉, EL0, 1, 2, 3의 스택공간과 SP가 따로따로 있어야 합니다. 하지만, 설정값에 따라서 모든 익셉션 레벨이 같은 스택과 같은 스택포인터를 공유하도록 만들 수 있습니다. (`-t`접미사)
노란색으로 강조표시한 것처럼, 일반적으로는 익셉션레벨별로 스택과 스택포인터를 따로 가지며, 리눅스 커널도 이 방법을 채택합니다.
5.2.2. ELR_ELx 레지스터
ARMv8에는 복귀주소를 저장하는 LR(X30) 레지스터뿐만 아니라 익셉션 처리한 뒤 익셉션이 발생한 직후의 복귀주소를 저장할 ELR_ELx 레지스터도 준비하고 있습니다.
- ELR_EL1: EL1'으로' 익셉션이 유발됐을 때, 익셉션 처리 후 복귀할 주소 (복귀할 곳은 EL0 또는 EL1이겠지요?)
- ELR_EL2: EL2'으로' 익셉션이 유발됐을 때, 익셉션 처리 후 복귀할 주소 (복귀할 곳은 EL1, 2겠지요?)
- ELR_EL3: EL3'으로' 익셉션이 유발됐을 때, 익셉션 처리 후 복귀할 주소 (복귀할 곳은 EL1, 2, 3이겠지요?)
익셉션을 처리한 후 복귀할 때는 `ERET` 명령어를 이용하며, 이때 'SPSR_ELn', `ELR_ELn' 레지스터를 사용합니다.
악의적인 의도를 가지고 SPSR_ELn 레지스터의 익셉션레벨 필드와 ELR_ELn 레지스터를 변경한 뒤 ERET 명령어를 실행한다면, 낮은 익셉션레벨 (e.g. EL1)에서 높은 익셉션레벨로 접근할 수 있습니다. 하지만, ARM 코어는 보안을 위해 허용되지 않은 형태로 낮은 익셉션레벨 → 높은 익셉션레벨로 가려고 한다면 HW적으로 자동으로 PSTATE.IL 필드를 set 합니다. 이후 진행되는 모든 명령어를 invalid로 설정해 abort를 발생시킵니다.
오직 허용된 방법은
① 각 익셉션레벨 진입 트랩 명령어 (`SVC, HVC, SMC`)를 사용하거나,
② 익셉션 발생 (Synchronous 익셉션 or Asynchronous 익셉션) 뿐입니다.
실제 코드를 살펴봅시다.
리눅스 커널이 부팅할 때 실행하는 el2_setup 레이블입니다.
'CurrentEL' 레지스터를 읽고 현재 익셉션 레벨을 파악한 뒤 EL1인지 EL2인지에 따라 다르게 동작하는 모습입니다.
6. 익셉션 (Exception)
6.1. 익셉션 정의
익셉션에는 많은 것이 포함돼 있고, 그들 사이에서도 수행하는 동작도, 특징도 다르기에 익셉션을 명확히 정의 내리기란 어렵습니다.
따라서 ARM에서 내린 정의를 빌리자면, 익셉션은 '발생하면 종류별로 정해진 주소로 PC값을 바꾸는 이벤트'입니다.
익셉션이 발생하면 공통적으로 다음 과정을 거칩니다.
- ① 하던 일을 중지하고, 익셉션 유발 원인에 대한 다양한 정보를 레지스터에 저장
- ② 정해진 주소로 분기해 익셉션을 처리
익셉션은 '의도적인 것'과 '의도적이지 않은 것'으로 나뉩니다.
- 의도적인 것: 인터럽트 (IRQ), 시스템콜 (SVC, HVC, SMC)
- 의도적이지 않은 것 = 치명적인 버그: 메모리 어보트 (Prefetch abort, Data abort, Undefined Instruction)
6.2. ARMv7 익셉션
6.2.1. (★) 처리하는 과정
익셉션 | 동작모드 | CPSR[4:0] (16진수) | 벡터테이블 베이스주소 + offset |
Reset | Supervisor | 0b10011 (0x13) | +0x00 |
Undefined Instruction | UND | 0b11011 (0x1B) | +0x04 |
SVC call (SW 인터럽트) | SVC (Supervisor) | 0b10011 (0x13) | +0x08 |
Prefetch Abort | PABT | 0b10111 (0x17) | +0x0C |
Data Abort | DABT | 0b10111 (0x17) | +0x10 |
Not Used | Not Used | +0x14 | |
IRQ 인터럽트 | IRQ | 0b10010 (0x12) | +0x18 |
FIQ 인터럽트 | FIQ | 0b10011 (0x11) | +0x1C |
ARMv7에서 익셉션이 발생하면 위에서 봤던 그림과 비슷한 절차를 거쳐 익셉션을 처리합니다.
- 프로세스에서 하던 작업을 중단합니다.
- CPSR값을 SPSR_<mode>에 백업, 현재 PC값을 보정한 것(보통 PC-0x4)을 LR_<mode>에 백업
- PC값을 익셉션 벡터 테이블 주소로 변경, 분기합니다.
- 익셉션 벡터 테이블에서 익셉션 종류별로 레이블 진입 후 정해진 작업 수행합니다.
- 인터럽트일 경우 ISR로 분기한 후 인터럽트 처리한 후 복귀
- 시스템콜일 경우 시스템콜 처리한 후 복귀
- 메모리 어보트(UND, PABT, DABT) 일 경우 디버깅 정보를 흩뿌리고 시스템 리셋
위 2번, 3번 과정이 HW적으로 자동으로 발생하기 때문에 코드를 보면서 공부할 수 없습니다.
따라서 의사코드로 표현하면 다음과 같습니다.
(Prefetch Abort가 발생했다고 가정)
R14_abt = 0xC000_D000 - 0x4; // 복귀주소(보정 후) 저장
SPSR_abt = CPSR; // 현재 CPSR값 저장
CPSR.M = 0x17; // ABT mode로 변경
pc = 0xFFFF_0000 + 0xC // 벡터 테이블 base 주소 + offset
☆ 벡터 테이블의 base 주소는 어떻게 확인할까요? VBAR 레지스터에 저장돼 있습니다.
6.2.2. 복귀주소 보정
파이프라인 때문에 익셉션을 처리한 후 복귀할 때 복귀주소를 보정해야 줘야 합니다.
익셉션 종류마다 발생했을 때의 파이프라인 step 위치가 다르고, 복귀하는 위치가 다르기 때문에 복잡합니다.
익셉션 종류 | 발생했을 때 파이프라인 step | 명령어 | 복귀하는 위치 |
Undefined Instruction | Decode | `MOVS PC, R14_und` | 익셉션이 발생한 다음 명령어 |
SVC call | Decode | `MOVS PC, R14_svc` | 익셉션이 발생한 다음 명령어 |
Prefetch Abort | Fetch | `SUBS PC, R14_abt, #0x4` | 익셉션이 발생한 명령어 |
Data Abort | Execute | `SUBS, PC, R14_abt, #0x8` | 익셉션이 발생한 명령어 |
IRQ 인터럽트 | 알 수 없음, 'Execute'까진 끝냄 | `SUBS, PC, R14_irq, #0x4` | 익셉션이 발생한 다음 명령어 |
FIQ 인터럽트 | 알 수 없음, 'Execute'까진 끝냄 | `SUBS, PC, R14_fiq, #0x4` | 익셉션이 발생한 다음 명령어 |
6.2.3. 익셉션 관련 코드 분석
struct process_struct {
void* stack;
int state;
struct task_struct *pre_process;
};
int proc_funct_ptr(void* param) {
struct process_struct *task;
int process_state = 0;
task = (struct process_struct*)param;
process_state = task->state; // ★ 여기서 DABT 발생!
return process_state;
}
위와 같이 현재 프로세스의 상태를 반환하는 간단한 함수가 있다고 생각해 봅시다.
`proc_funct_ptr()` 함수의 void* 타입 매개변수 param으로 만일 NULL값이 들어왔다면 어떻게 될까요?
NULL값 참조로 Data abort가 발생할 것입니다. 파이프라인 상 'Execute' 단계에 들어서야 '아, 이 주소가 NULL이네?'를 알아차립니다.
익셉션이 발생했으니 다음 명령어 (위 `ldm sp, {fp, sp, pc}`)는 data abort를 처리하는 레이블로 분기합니다.
위와 같은 과정을 거친 뒤 개발자가 디버깅할 수 있도록 익셉션이 발생한 곳을 비롯해 여러 가지 정보를 출력하고 시스템을 리셋합니다.
이때, 중요한 점은 익셉션을 처리하기 위해 호출했던 일련의 핸들러들이 프로세스 스택 공간에서 처리된다는 점입니다.
6.3. ARMv8 익셉션
ARMv8은 하위호환성을 지원하기 때문에 거의 비슷한 내용이 이어지기 때문에 ARMv7의 익셉션 및 익셉션 처리 과정을 잘 이해했다면 훨씬 이해하기 쉽습니다.
6.3.1. 익셉션 종류
익셉션 타입 | 익셉션 종류 | 익셉션 유발 원인 (익셉션 클래스) |
Synchronous | System Call (SVC ...) | |
Data Abort | ||
Instruction Abort | ||
Misaligned Stack Pointer | ||
Unknown Reason | ||
Asynchronous | IRQ | 외부 인터럽트 (Non-secure) |
FIQ | 외부 인터럽트 (Secure) | |
SError | 외부 Memory data abort |
위 표는 ARMv8의 익셉션 종류를 나타내는 표입니다. (정말 중요하니 꼭 외웁시다)
ARMv7과 비교해서 조금 바뀐 점이 있습니다.
- 인터럽트(IRQ, FIQ)와 외부 메모리 어보트를 제외하면 모두 Synchronous 익셉션으로 분류됩니다.
- ARMv7 때는 DABT, PABT, UND 등 서로 다른 익셉션으로 여겨졌지만
- ARMv8에서는 모두 Synchronous 익셉션에 속하며 구체적인 유발 원인을 분류할 때만 구분됩니다.
- 즉, ARMv8 코어가 어셈블리 명령어를 실행하다 발생하는 익셉션은 전부 Synchronous 익셉션입니다. 심지어 SVC 같은 시스템콜조차도요.
- FIQ는 더 이상 fast 인터럽트가 아닙니다.
- ARMv7 때는 일반 범용 인터럽트를 IRQ, 그보다 더 빠르게 처리해야 하는 인터럽트를 Fast IRQ (FIQ)로 여겼지만
- ARMv8에서는 (트러스트존의) non-secure 영역에서 처리하는 인터럽트는 IRQ,
secure 영역에서 처리하는 인터럽트를 FIQ로 분류합니다.
- SError 익셉션이 새로 생겼습니다.
- 외부 메모리 버스에서 Abort를 유발하면 발생하는 익셉션입니다.
- MMU를 통해 메모리 접근권한 체크 후 물리주소로 변환하는데, 여기서 통과하더라도 버스 자체에서 오류가 감지됩니다.
Synchronous/ Asynchronous 단어가 잘 와닿지 않으실 수 있습니다.
- IRQ 같은 외부에서 걸리는 익셉션은 프로세서의 형편에 맞게 걸려주지 않고 비동기적으로 발생합니다.
- 시스템콜이든 어보트든 프로세서의 파이프라인 동작에 맞춰서 발생/처리되는 일종의 프로시저입니다.
- 이런 관점에서 생각하면 납득하는 데 도움 될 수 있을 것 같습니다.
6.3.2. (★) 익셉션 클래스와 익셉션 신드롬 레지스터 (ESR_ELx)
익셉션 클래스란, 익셉션의 원인, 익셉션을 발생시키는 유발인자를 의미합니다.
익셉션 클래스는 5가지 카테코리에 총 37개 있으며 각각 6-bit로 식별코드가 부여돼 있습니다.
익셉션이 발생했을 때, ARM 코어는 'ESR_ELx'라는 익셉션 신드롬 레지스터의 [31:26] bit에 세부 원인 (익셉션 클래스의 식별코드)을 기록합니다.
모든 익셉션 클래스에 대한 설명은 이 공식문서 (링크)의 EC bits [31:26] 부분을 참고하면 됩니다.
6.3.3. (★) 익셉션 벡터 테이블 진입 전
우리는 앞서 6.2.1절에서 ARMv7애서 익셉션이 발생했을 때 HW적으로 자동으로 처리되는 일련의 과정을 살펴봤습니다.
이번에는 ARMv8에서 익셉션 발생 시, 익셉션 벡터 테이블로 진입할 때까지 일어나는 과정을 살펴보겠습니다.
동작 | 설명 |
PSTATE를 SPSR_ELx에 백업 | 현재 PSTATE 정보를 SPSR_ELx에 백업해놔야 복귀할 수 있겠죠? |
PSTATE.EL 업데이트 | PSTATE.EL 필드를 이동할 익셉션 레벨값으로 갱신합니다. |
ELR_ELx, ESR_ELx 업데이트 | ELR_ELx에 복귀할 주소를, ESR_ELx에 익셉션 발생 원인을 기록합니다. |
PC를 익셉션 벡터 테이블 주소로 분기 | 익셉션 핸들러로 이동하기 위해 VBAL_ELx를 참조해 익셉션 벡터 테이블로 분기합니다. 이때, 현재 EL과 익셉션 종류에 따라 분기할 주소가 달라집니다. 아래 6.3.4절을 참고하세요. |
위 표를 의사코드로 표현하면 아래와 같습니다.
ELR_ELx = 복귀할주소
ESR_ELx = PSTATE[5:0]
if PSTATE.EL == EL1 then // 익셉션발생 EL = 현재 EL
offset = 0x200;
elif PSTATE.EL == EL0 then // 익셉션발생 EL < 현재 EL
offset = (if aarch32 then 0x600 elif 0x400);
elif PSTATE.SP == 1 then // SP_EL0 스택 통합 사용
offset = 0x0;
SPSR_ELx = PSTATE; // 복귀할 state 백업
PSTATE.EL = ELx; // 이동할 익셉션 레벨 갱신
BranchTo(VBAR_ELx + offset);
6.3.4. (★) 익셉션 벡터 테이블
익셉션이 발생한 EL | 익셉션 벡터 주소 계산 (VBAL_ELx + offset) | |||
Synchronous | IRQ | FIQ | SError | |
SP_EL0를 사용하는 EL (※ 리눅스커널/ XEN은 이거 사용 안 합니다 ) |
+0x000 | +0x080 | +0x100 | +0x180 |
현재 실행 중인 EL | +0x200 | +0x280 | +0x300 | +0x380 |
1단계 낮은 (lower) EL (AArch64) | +0x400 | +0x480 | +0x500 | +0x580 |
1단계 낮은 (lower) EL (AArch32) | +0x600 | +0x680 | +0x700 | +0x780 |
(◈ 각 익셉션 종류별로 +0x080 바이트만큼 떨어져 있습니다.)
계속 강조하지만, ARMv8의 익셉션 레벨은 독립된 실행환경입니다.
따라서 익셉션 벡터 테이블도 따로따로 갖고 있습니다.
ARMv7에서는 익셉션 벡터의 base 주소를 VBAR 레지스터가 갖고 있었죠?
각 익셉션 레벨의 익셉션 벡터 테이블은 VBAR_ELx 레지스터가 갖고 있습니다.
위 표도 중요하므로 꼭 머릿속에 넣읍시다.
표를 보는 방법은 다음과 같습니다.
- 현재 동작중인 익셉션 레벨을 기준으로 ① 통합 스택을 사용하는지 여부, ② 익셉션이 어디서 발생했는지로 나뉩니다.
- 통합스택을 사용하는 경우는 거의 없습니다. 사용한다면 가장 윗 행 'SP_EL0를 사용하는 EL'을 보면 됩니다.
- 익셉션이 현재 동작중인 익셉션 레벨 EL에서 발생했다면 2번째 행 '현재 실행 중인 EL'을 보면 됩니다.
- 익셉션이 낮은 레벨 (EL - 1)에서 발생했다면 3번째,
그중에서도 32-bit 애플리케이션/커널/OS라면 4번째 행을 보면 됩니다.
몇 가지 상황을 예로 들어서 확인합시다. (모두 독립된 스택 공간 이용)
- Q1. EL1에서 memory abort가 발생
- EL1에서 실행, EL1에서 abort 발생했으므로 2번째 행 '현재 실행 중인 EL'을 봅시다.
- Memory abort(Data abort + Instruction abort)는 Synchronous 익셉션으로 분류됩니다.
- 따라서 VBAL_EL1 + 0x200 주소를 보면, 적절한 익셉션 핸들러를 발견할 수 있습니다.
- Q2. 유저 애플리케이션(AArch64)이 동작하다 인터럽트 발생
- 유저 애플리케이션이 실행되는 익셉션 레벨은 EL0입니다.
- EL0에서 실행, EL0에서 인터럽트가 발생했으므로 2번째 행 '현재 실행 중인 EL'을 봅시다.
- 따라서 VBAL_EL1 + 0x280 주소를 보면, 적절한 인터럽트 핸들러를 발견할 수 있습니다.
- Q3. 시스템 자원을 얻기 위해 SVC 명령어 호출
- SVC 명령어를 호출했다는 것은, 익셉션 레벨이 EL0이었다는 뜻입니다.
- SVC 명령어 덕분에 현재 실행모드는 EL1이지만, 익셉션이 발생한 건 EL0이므로 3,4번째 행을 봅시다.
- SVC 시스템콜은 synchronous 익셉션으로 분류됩니다.
- 64-bit 애플리케이션이었다면 VBAL_EL1 + 0x400을
32-bit 애플리케이션이었다면 VBAL_EL1 + 0x600을 보면 됩니다.
이렇게 offset을 계산한 곳을 보면, 적절한 익셉션 핸들러로 분기하는 코드가 있습니다.
익셉션 핸들러에서 익셉션 처리한 후 복귀할 때는 ERET 명령어 실행하는 거 아직 안 까먹으셨죠?
'Embedded' 카테고리의 다른 글
【공부/정리】 시스템 소프트웨어 개발을 위한 ARM 아키텍처 구조와 원리 ④ 메모리 모델, 캐시, MMU (0) | 2025.03.03 |
---|---|
【공부/정리】 시스템 소프트웨어 개발을 위한 ARM 아키텍처 구조와 원리 ③ GIC, 트러스트존, 하이퍼바이저 (0) | 2025.03.01 |
【공부/정리】 시스템 소프트웨어 개발을 위한 ARM 아키텍처 구조와 원리 ① 레지스터와 명령어 (0) | 2025.02.26 |
Aarch64 Memory Management 요약 정리 (0) | 2025.02.25 |
[정리/요약] ARM Cortex-M 프로그래밍 (0) | 2025.02.11 |