요약
이번 프로젝트는 ARM 아키텍처에서 운영되는 간단한 커널을 Rust로 제작하는 것이다. 이를 통해 ARM 아키텍처에 대해서 학습하고, 운영체제 전반의 구조를 직접 제작해보며 운영체제에 대한 이해도를 향상시킬 것을 목적으로 한다. 더불어, 가급적 memory safe한 rust의 특징을 살려 잘못된 메모리 참조나 메모리 누수가 일어나지 않는 안전한 커널을 만들 수 있을 것으로 기대해본다.
개요
프로젝트와 QEMU 세팅, Bootloading, Interrupt Handling 의 순으로 커널을 제작하였다. 초기에 프로젝트를 제작할 때에, X86기반의 커널을 제작하는 튜토리얼을 참고하였지만, ARM 아키텍처에 적용하기에는 아키텍처 의존성 때문에 어려움이 있어 이미 제작되어 있는 다른 ARM 아키텍처의 커널들을 참고하며 개발을 진행하게 되었다. 그 중, 대표적으로 hermit-os의 kernel과 bootloader에서 kernel의 부팅절차를 확인하였으며, raspberry pi용 os 제작 튜토리얼에서 주로 ARM의 어셈블리 코드를 참조하였다. 가급적 Linux의 방식을 모사하고 싶었지만, 커널의 규모가 너무 방대하여 최소한의 기능만 갖춘 위의 커널들을 주로 참고하였다.
Booting
X86의 부팅과정과 크게 다르지 않을 것이라고 생각하고 BIOS로 부팅하게 될 경우 16bit real mode에서 32bit protected mode로, 32bit protected mode에서 64bit long mode로 직접 transition 해주는 과정을 구현해야 될 것 같아 UEFI로 구현하려 하였다. 다음은 UEFI와 BIOS의 주요한 차이에 관한 내용이다.
- UEFI
- Unified Extensible Firmware Interface
- Requires EFI System Partition (ESP) to store
.efi
file .efi
file includes all device initialization & startup codes.. (could be kernel itself)- Uses GPT Partitioning scheme
- BIOS
- Basic Input Output System
- Uses MBR Partitioning Scheme
- Legacy Way
- Requires 16bit-32bit-64bit transition since it starts on 16bit mode (x86)
사실 ARM 아키텍쳐에서는 이러한 mode를 transition 하는 과정이 필요없다고 한다. UEFI 앱으로 빌드하여 개발을 하던 중, Interrupt 구현과정에서 어셈블리와 관련한 사소한 문제가 생겨 EFI 앱이 아닌 ELF앱으로 빌드하여 Bios로 부팅하기로 하였다. 결국, 기존의 구현하였던 UEFI 부분을 걷어낸 뒤, 일반적인 커널의 형태를 최대한 모사하기 위해서 Bootloader를 제작을 진행하려 하였고, 메모리에 로드된 Kernel ELF 의 주소를 직접 찾고, 매직넘버를 검사하여 올바른 ELF를 찾으면 이를 로드하는 방식으로 부팅하였다.
하지만, 메모리에서 Kernel ELF를 찾은 뒤, 로딩하는 과정에서 어려움이 있어 Bootloader가 통합된 하나의 Kernel ELF로 제작하기로 하였다. 이에 따라, 기존의 uefi로 부팅하던 qemu 세팅을 bios (기본값)으로 부팅하도록 세팅을 바꾸고, hermit-loader가 커널을 찾아서 로딩하는 부분과 hermit-kernel이 loader에서 넘겨받아 커널이 켜지는 부분을 역으로 참조하며 부팅을 진행할 수 있도록 하였다.
Hermit의 경우, linker script를 이용하여 entry point와 각 메모리들의 section을 명시해주었고, 초기 검증과정 등을 거친 후에 rust 코드로 branch하는 방식으로 개발되어 있었다. 본인 역시 이와 유사하게 진행하였으나, 추후 Exception Level 1으로 Transition하여 Exception Vector Table을 등록해야 되어, 이와 관련된 처리도 추가적으로 해주었다.
부팅이 완료된 후, stdout을 활성화시키기 위해서 0x40000000에 로드되어 있는 Device Tree를 참조하여 이를 담당하는 pl011 디바이스의 주소(0x09000000)를 찾아내고, 초기화 시켜주어 logging을 할 수 있었다.
Interrupt
ARM에서는 X86 아키텍처와는 다르게 Interrupt를 Exception의 일종으로 받아들인다. 따라서, X86의 Interrupt Descriptor Table이 존재하지 않고, Interrupt Request (IRQ) 혹은 Fast Interrupt Request (FIQ) 이 일어났을 때, Exception Vector Table를 참조하여 인터럽트에 대한 핸들러로 브랜치하고, 해당 핸들러에서 어떤 인터럽트인지 acknowledge를 한 이후, 이에 맞는 핸들러로 브랜치 시키는 방식을 이용한다. 이때 이용되는 Exception Vector Table은 다음과 같은 구조를 가지고 있다.
Exception Vector Table
AArch64 Exception Vector Table
ARMv7 Exception Vector Table
Offset Vector Mode 0x00 Reset Supervisor 0x04 Undefined Instruction Undefined 0x08 Supervisor Call Supervisor 0x0C Prefetch Abort Abort 0x10 Data Abort Abort 0x14 Not Used NA 0x18 IRQ Interrupt IRQ 0x1C FIQ Interrupt FIQ ARMv8 Exception Vector Table
Link to original
Address Exception Type Description VBAR_ELn +0x000 Synchronous Current EL with SP0 +0x080 IRQ/vIRQ ^ +0x100 FIQ/vFIQ ^ +0x180 SError/vSError ^ +0x200 Synchronous Current EL with SPx +0x280 IRQ/vIRQ ^ +0x300 FIQ/vFIQ ^ +0x380 SError/vSError ^ +0x400 Synchronous Lower EL using AArch64 +0x480 IRQ/vIRQ ^ +0x500 FIQ/vFIQ ^ +0x580 SError/vSError ^ +0x600 Synchronous Lower EL using AArch32 +0x680 IRQ/vIRQ ^ +0x700 FIQ/vFIQ ^ +0x780 SError/vSError ^
이와 같이 armv7의 exception vector와 armv8의 exception vector가 다른 구조로 구성되어 있다. cortex-a76 CPU에서 동작할 것을 상정하고 제작하기에, armv8의 exception vector table구조를 따랐다.
위의 Exception 이 들어왔을 때, 기존 프로그램의 실행 상태를 저장하고 handling을 처리하도록 하였으며, 여기에서 올바르지 않은 레지스터에 등록하였는지 오류가 발생하였지만 곧이내 해결할 수 있었다. Exception Vector Table의 각 field에서는 rust 코드로 branch하여 interrupt id를 확인하고 이에 맞는 핸들러를 불러와 수행하도록 하였다.
Exception Vector Table을 등록하기 위해서는 ARM의 VBAR 레지스터에 등록해야 하는데, 올바른 Exception Level의 VBAR 레지스터에 등록해야 해당 레벨에서의 익셉션을 처리할 수 있다. Exception의 종류는 다음과 같다.
Exception Levels
Exception Levels
- EL3
- Highest privilege level is typically used for so called
Secure Monitor
- EL3 firmware typically implements the Power State Coordination Interface (PSCI) for the lower ELs to use
- EL3 firmware typically involved into trusted boot
- EL2
- Targets the virtualization use-case
- EL at which hypervisors normally use for virtualization purposes.
- EL1
- Privileged parts of the OS kernels use
- EL0
- Most unprivileged level
- Runs most unprivileged codes (userspace application, userspace drivers, etc).
Reference
Link to original
- https://developer.arm.com/documentation/102412/0103/Privilege-and-Exception-levels/Exception-levels
- https://krinkinmu.github.io/2021/01/04/aarch64-exception-levels.html
- https://medium.com/@om.nara/aarch64-exception-levels-60d3a74280e6
현재로써는 가상화를 생각하지 않고 커널의 권한만 필요하기에 EL1으로 부팅하였다. (부팅할 때에는 EL2로 부팅되지만, EL1으로 Transition하는 과정을 추가하였다.)
추가적으로, ARM에서는 GIC를 이용하여 Software Interrupt를 발생시키거나, Interrupt ID를 acknowledge한다. GIC를 사용하기 위해서 Device Tree에서 GICD, GICC의 주소를 읽어와 디바이스를 초기화시켜주었다.
Memory
QEMU에서는 ARM아키텍처를 에뮬레이션하기 위해서 virt라는 가상의 보드를 이용한다. Virt board의 메모리 레이아웃을 확인하기 위해서 qemu의 소스코드를 확인하여 본 결과 다음과 같은 레이아웃을 가지고 있었다.
NAME | START_ADDR | END_ADDR | LEN |
---|---|---|---|
Boot Rom RESERVED (0 to 0x08000000) | < | < | < |
VIRT_FLASH | 0x00000000 | 0x08000000 | 0x08000000 |
VIRT_CPUPERIPHS | 0x08000000 | 0x08020000 | 0x00020000 |
GICD & GICC sits inside CPU Peripheral space | < | < | < |
VIRT_GIC_DIST | 0x08000000 | 0x08010000 | 0x00010000 |
VIRT_GIC_CPU | 0x08010000 | 0x08020000 | 0x00010000 |
VIRT_GIC_V2M | 0x08020000 | 0x08021000 | 0x00001000 |
VIRT_GIC_HYP | 0x08030000 | 0x08040000 | 0x00010000 |
VIRT_GIC_VCPU | 0x08040000 | 0x08050000 | 0x00010000 |
The space in between here is reserved for GICv3 CPU/vCPU/HYP | < | < | < |
VIRT_GIC_ITS | 0x08080000 | 0x080A0000 | 0x00020000 |
This redistributor space allows up to 264kB123 CPUs | < | < | < |
VIRT_GIC_REDIST | 0x080A0000 | 0x09000000 | 0x00F60000 |
VIRT_UART | 0x09000000 | 0x09001000 | 0x00001000 |
VIRT_RTC | 0x09010000 | 0x09011000 | 0x00001000 |
VIRT_FW_CFG | 0x09020000 | 0x09020018 | 0x00000018 |
VIRT_GPIO | 0x09030000 | 0x09031000 | 0x00001000 |
VIRT_SECURE_UART | 0x09040000 | 0x09041000 | 0x00001000 |
VIRT_SMMU | 0x09050000 | 0x09070000 | 0x00020000 |
VIRT_PCDIMM_ACPI | 0x09070000 | 0x09070018 | 0x00000018 |
VIRT_ACPI_GED | 0x09080000 | 0x09080004 | 0x00000004 |
VIRT_NVDIMM_ACPI | 0x09090000 | 0x09090004 | 0x00000004 |
VIRT_PVTIME | 0x090A0000 | 0x090B0000 | 0x00010000 |
VIRT_SECURE_GPIO | 0x090B0000 | 0x090B1000 | 0x00001000 |
VIRT_MMIO | 0x0A000000 | 0x0A000200 | 0x00000200 |
VIRT_PLATFORM_BUS | 0x0C000000 | 0x0E000000 | 0x02000000 |
VIRT_SECURE_MEM | 0x0E000000 | 0x01000000 | 0x01000000 |
VIRT_PCIE_MMIO | 0x10000000 | 0x3EFF0000 | 0x2eff0000 |
VIRT_PCIE_PIO | 0x3EFF0000 | 0x3F000000 | 0x00010000 |
VIRT_PCIE_ECAM | 0x3F000000 | 0x40000000 | 0x01000000 |
VIRT_MEM | 0x40000000 | 0x4040000000 | 0x4000000000 |
Customizable Area | < | < | < |
QEMU_DTB | 0x40000000 | 0x40200000 | 0x00200000 |
COSMOS_RAM_START | 0x40200000 |
여기에서, 램의 시작 주소는 0x40000000 인데, 해당 위치에는 qemu가 dtb를 로드해 두었기 때문에, 여유있게 0x40200000부터 메모리 공간을 이용하도록 linker script를 구성하였다. 0x40200000부터의 섹션들은 다음과 같다.
섹션 | 역할 | 권한 |
---|---|---|
.text | 커널의 실행할 코드가 위치할 메모리 영역 | r-x |
.rodata | 컴파일 시에 값이 정해지고 변경되지 않는 전역 상수 데이터 | r— |
.got | Relocation 관련.. 아직 사용하지 않음 | r— |
.data | 컴파일 시에 값이 정해지는 전역 변수 데이터 | rw- |
.bss | 컴파일 시에 초기값이 할당되지 않은 전역변수 | rw- |
Heap | 동적으로 할당할 수 있는 메모리의 영역..아직 구현되지 않음 | |
Stack | 프로세스의 스택이 위치하는 메모리 영역 |
아직 heap 영역에 대해서는 구현되지 않았으며, 스택 역시 주소공간만 충분히 할당해 주었다. 이는 Virtual Memory와 관련된 구현을 할 때 함께 진행할 계획이다.
References
Philipp Oppermann’s blog
https://os.phil-opp.com/freestanding-rust-binary/
Running a full arm64 system stack under QEMU
https://cdn.kernel.org/pub/linux/kernel/people/will/docs/qemu/qemu-arm64-howto.html
UEFI vs BIOS
https://phoenixnap.com/kb/uefi-vs-bios
Devicetree in QEMU
https://docs.u-boot.org/en/latest/develop/devicetree/dt_qemu.html
DeviceTree Specification
https://devicetree-specification.readthedocs.io/en/stable/index.html
Flattened Device Tree Format
https://devicetree-specification.readthedocs.io/en/v0.3/flattened-format.html
https://velog.io/@coral2cola/ARM-Interrupts-1
https://velog.io/@coral2cola/ARM-Interrupts-2
https://grasslab.github.io/osdi/en/labs/lab3.html
Linux - ARM architected timer:
https://kernel.org/doc/Documentation/devicetree/bindings/arm/arch_timer.txt
Linux - ARM Generic Interrupt Controller, version 3
https://www.kernel.org/doc/Documentation/devicetree/bindings/interrupt-controller/arm%2Cgic-v3.txt
Hermit Kernel: https://hermit-os.org/kernel
Hermit Loader: http://hermit-os.org/loader
rust-raspberrypi os tutorial: https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials
ALIGN in Linker Scripts
https://stackoverflow.com/questions/8458084/align-in-linker-scripts
Linux Memory Layout
https://hackyboiz.github.io/2022/01/14/poosic/linux-memory-layout/
QEMU