일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
- 아두이노
- UART
- AVR
- USART
- QEMU
- 라즈베리파이
- Visual Studio
- bare metal
- STM32
- Linux
- Debug
- raspberrypi
- BeagleBone
- Raspberry
- yocto
- 리눅스
- vscode
- 디버깅
- GPIO
- platformio
- nucleo
- avr-gcc
- esp32
- atmel
- Debugging
- buildroot
- Visual Studio Code
- C++
- Arduino
- AArch64
- Today
- Total
임베디드를 좋아하는 조금 특이한 개발자?
[AArch64] Bare metal에서 EL2에서 EL1으로 모드 변경 방법 본문
- 개발 환경
개발 보드 : Raspberrypi 4
WSL2 (Ubuntu 22.04 LTS)
toolchain : aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
- 예제 코드
GitHub - MainForm/RaspberryPI4_Baremetal_Firmware
Contribute to MainForm/RaspberryPI4_Baremetal_Firmware development by creating an account on GitHub.
github.com
1. 서론
ARMv8에서는 현재 실행하려는 프로그램의 목적에 따라 모드(EL : Exception Level)를 나누어 관리하고 있습니다. 현재 모드에 따라 접근 할 수 있는 레지스터가 달라지며 프로그램 실행 전 목적에 적합한 EL로 전환하여야 합니다.
(단, 목적에 맞게 EL 변경을 권장한다는 것이지 강제는 아님)
출처 : Learn the architecture - AArch64 Exception Model
EL에 대해서 설명하는 것은 너무 내용이 방대하므로 다른 포스트에서 다루어 보도록 하겠습니다.
이번 포스트에서는 EL2에서 EL1으로 모드를 변경하는 경우에 대해서만 다루도록 하겠습니다. 현재 모드와 바꾸고 싶은 모드에 따라서 접근해야하는 레지스터가 다르고, 또 하위 EL(EL0)에서 상위 EL(EL1,2,3) 로 이동하는 방법 또한 다르기에 굳이 여기에 모든 방법을 다루지는 않겠습니다.
2. EL2에서 EL1으로 모드 변경
라즈베리파이4에서는 부팅시 default EL 모드가 EL2입니다. 저는 현재 OS를 개발하고 있으므로 EL2에서 EL1으로 전환하도록 하겠습니다. EL 모드를 전환하기 위해서는 몇가지 설정이 필요합니다.
위 과정을 천천히 살펴 보도록 하겠습니다.
2.1. EL1에서 AArch64 허용
라즈베리파이4는 AArch64 아키텍처 기반의 MCU를 사용하고 있습니다. 그래서 먼저 AArch64를 사용하도록 설정하도록 하겠습니다. 그러기 위해서 HCR_EL2의 31번 레지스터인 RW를 1로 설정해야합니다.
출처 : ARMv8, for ARMv8-A architecture profile
- 어셈블리 코드
1
2
3
4
5
|
// HCR_EL2: Hypervisor Configuration Register
// EL1에서 AArch64 모드 허용
MOV x0, #(1 << 31)
MSR hcr_el2, x0
ISB
|
cs |
2.2. EL1의 스택 포인터 설정
각 모드 별로 실행되는 프로그램이 다릅니다. 그말은 해당 모드별로 다른 스택 메모리를 사용한다는 말과 같습니다. 그러므로 EL1에서 사용할 스택메모리를 설정해 주어야 한다는 것입니다. ARMv8에서는 상위 모드에서 EL1의 스택포인터를 설정할 수 있는 특수한 레지스터를 제공합니다.
출처 : ARMv8, for ARMv8-A architecture profile
- 어셈블리 코드
1
2
3
4
|
// EL1에서 사용할 스택 포인터 메모리 설정
LDR x0, =__stack_top
MSR sp_el1, x0
ISB
|
cs |
여기서 __stack_top 은 링커 스크립트에 작성되어 있습니다.
2.3. 변경할 모드 설정
이제 현재 모드(EL2)에서 변경할 모드(EL1)으로 전환하기 위한 설정을 하여야 합니다. ARMv8에서는 SPSR_EL2 레지스터를 통해 해당 설정을 할 수 있습니다.
출처 : ARMv8, for ARMv8-A architecture profile
SPSR_EL2에서 M bits[3:0]를 설정함으로써 전환할 모드를 설정하게 됩니다. 여기서 M[3:2]가 변경할 모드를 의미하며 M[0]로 t, h 상태를 설정하여 전환할 모드에서 사용할 SP(스택 포인터)를 설정하게 됩니다. 간단히 t, h에 대한 상태를 설명 드리자면 t는 모드가 어떻든 무조건 EL0의 스택 포인터(SP_EL0)을 사용하겠다는 것이며, h는 는 각 모드에 해당하는 스택 포인터를 사용하겠다는 의미 입니다. 저는 해당 모드에 해당하는 SP를 사용할 것이므로 h로 설정하도록 하겠습니다.
- 어셈블리 코드
1
2
3
4
5
|
// SPSR_EL2: Saved Program Status Register for EL2
// EL1 모드로 진입하기 위해 EL2에서 SPSR_EL2 설정
MOV x0, #(0b0101)
MSR spsr_el2, x0
ISB
|
cs |
2.4. 모드 변경시 시작할 주소 설정
모드를 변경하게 된다면 해당 모드에서 실행할 프로그램이 있을 것입니다. 그러므로 해당 프로그램이 시작하는 주소를 설정하여 모드 전환시 자동적으로 해당 프로그램이 실행될 수 있도록 할 수 있습니다.
출처 : ARMv8, for ARMv8-A architecture profile
Purpose에서 Exception이 발생했다는 의미를 더 상세하게 말씀드리자면 Exception RETurn(대문자로 쓴 이유는 해당 명령어가 있기 때문입니다.)이 발생하면 위 설정 내용이 적용되어 EL1으로 전환한다는 의미입니다.
- 어셈블리 코드
1
2
3
4
|
// ELR_EL2: Exception Link
LDR x0, =prev_main
MSR elr_el2, x0
ISB
|
저는 바로 main 함수로 이동하는 것이 아닌 prev_main으로 이동하도록 설정하였습니다. 그 이유는 main 함수 진입전 EL1 모드에서 해야하는 설정이 있기 때문입니다.(BSS 섹션 초기화 및 VBAR 설정)
2.5. 모드 변경
이제 실질적으로 모드를 전환하기 위해서는 Exception RETurn를 발생해야 합니다. 다행스럽게도 ARMv8에서는 해당 Exception를 발생시키는 명령어를 제공합니다.
출처 : ARMv8, for ARMv8-A architecture profile
- 어셈블리 코드
1
2
|
// 위 설정한 내용을 토대로 EL2에서 EL1으로 전환
ERET
|
3. 변경된 모드 확인
현재 모드를 확인하기 위한 레지스터가 있습니다.
하지만 위 레지스터는 어셈블리로 접근해야하므로 다음과 같은 C언어 함수를 정의 하였습니다.
1
2
3
4
5
6
7
8
9
10
|
// 현재 EL 모드를 확인하기 위한 함수
uint32_t getCurrentEL(){
uint64_t el;
/* CurrentEL 레지스터를 읽어 el에 저장 */
asm volatile (
"mrs %0, CurrentEL\n"
: "=r" (el)
);
return (uint32_t)((el & 0b1100) >> 2);
}
|
이제 해당 함수를 사용하여 main함수에서 실제 EL1으로 변경되었음을 확인해보겠습니다.
1
2
3
4
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int main(void){
// UART 초기화
UART_Initialize(115200);
while(1){
switch(getCurrentEL()){
case 0: // EL0
UART_SendString("EL0\n");
break;
case 1: // EL1
UART_SendString("EL1\n");
break;
case 2: // EL2
UART_SendString("EL2\n");
break;
case 3: // EL3
UART_SendString("EL3\n");
break;
default:
UART_SendString("Unknown EL\n");
}
delay(0x400000);
}
return 0;
}
|
굳이 switch문을 사용한 이유는 아직 정수를 출력하는 itoa나 sprintf 함수가 없기에 간편히 만든 것입니다.
출력 결과를 확인해보면 정상적으로 EL1으로 전환된 것을 확인하였습니다.
'Embedded > ARM' 카테고리의 다른 글
[AArch64] Bare metal에서 BSS 섹션 초기화(전역 변수 쓰레기 값 문제) (1) | 2025.07.29 |
---|---|
[AArch64] FPU 및 SIMD 활성화 (실수 연산시 멈춤 현상 해결) (1) | 2025.07.28 |