요약

이번 프로젝트는 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

OffsetVectorMode
0x00ResetSupervisor
0x04Undefined InstructionUndefined
0x08Supervisor CallSupervisor
0x0CPrefetch AbortAbort
0x10Data AbortAbort
0x14Not UsedNA
0x18IRQ InterruptIRQ
0x1CFIQ InterruptFIQ

ARMv8 Exception Vector Table

AddressException TypeDescription
VBAR_ELn +0x000SynchronousCurrent EL with SP0
+0x080IRQ/vIRQ^
+0x100FIQ/vFIQ^
+0x180SError/vSError^
+0x200SynchronousCurrent EL with SPx
+0x280IRQ/vIRQ^
+0x300FIQ/vFIQ^
+0x380SError/vSError^
+0x400SynchronousLower EL using AArch64
+0x480IRQ/vIRQ^
+0x500FIQ/vFIQ^
+0x580SError/vSError^
+0x600SynchronousLower EL using AArch32
+0x680IRQ/vIRQ^
+0x700FIQ/vFIQ^
+0x780SError/vSError^
Link to original

이와 같이 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

현재로써는 가상화를 생각하지 않고 커널의 권한만 필요하기에 EL1으로 부팅하였다. (부팅할 때에는 EL2로 부팅되지만, EL1으로 Transition하는 과정을 추가하였다.)

추가적으로, ARM에서는 GIC를 이용하여 Software Interrupt를 발생시키거나, Interrupt ID를 acknowledge한다. GIC를 사용하기 위해서 Device Tree에서 GICD, GICC의 주소를 읽어와 디바이스를 초기화시켜주었다.

Memory

QEMU에서는 ARM아키텍처를 에뮬레이션하기 위해서 virt라는 가상의 보드를 이용한다. Virt board의 메모리 레이아웃을 확인하기 위해서 qemu의 소스코드를 확인하여 본 결과 다음과 같은 레이아웃을 가지고 있었다.

NAMESTART_ADDREND_ADDRLEN
Boot Rom RESERVED (0 to 0x08000000)<<<
VIRT_FLASH0x000000000x080000000x08000000
VIRT_CPUPERIPHS0x080000000x080200000x00020000
GICD & GICC sits inside CPU Peripheral space<<<
VIRT_GIC_DIST0x080000000x080100000x00010000
VIRT_GIC_CPU0x080100000x080200000x00010000
VIRT_GIC_V2M0x080200000x080210000x00001000
VIRT_GIC_HYP0x080300000x080400000x00010000
VIRT_GIC_VCPU0x080400000x080500000x00010000
The space in between here is reserved for GICv3 CPU/vCPU/HYP<<<
VIRT_GIC_ITS0x080800000x080A00000x00020000
This redistributor space allows up to 264kB123 CPUs<<<
VIRT_GIC_REDIST0x080A00000x090000000x00F60000
VIRT_UART0x090000000x090010000x00001000
VIRT_RTC0x090100000x090110000x00001000
VIRT_FW_CFG0x090200000x090200180x00000018
VIRT_GPIO0x090300000x090310000x00001000
VIRT_SECURE_UART0x090400000x090410000x00001000
VIRT_SMMU0x090500000x090700000x00020000
VIRT_PCDIMM_ACPI0x090700000x090700180x00000018
VIRT_ACPI_GED0x090800000x090800040x00000004
VIRT_NVDIMM_ACPI0x090900000x090900040x00000004
VIRT_PVTIME0x090A00000x090B00000x00010000
VIRT_SECURE_GPIO0x090B00000x090B10000x00001000
VIRT_MMIO0x0A0000000x0A0002000x00000200
VIRT_PLATFORM_BUS0x0C0000000x0E0000000x02000000
VIRT_SECURE_MEM0x0E0000000x010000000x01000000
VIRT_PCIE_MMIO0x100000000x3EFF00000x2eff0000
VIRT_PCIE_PIO0x3EFF00000x3F0000000x00010000
VIRT_PCIE_ECAM0x3F0000000x400000000x01000000
VIRT_MEM0x400000000x40400000000x4000000000
Customizable Area<<<
QEMU_DTB0x400000000x402000000x00200000
COSMOS_RAM_START0x40200000

여기에서, 램의 시작 주소는 0x40000000 인데, 해당 위치에는 qemu가 dtb를 로드해 두었기 때문에, 여유있게 0x40200000부터 메모리 공간을 이용하도록 linker script를 구성하였다. 0x40200000부터의 섹션들은 다음과 같다.

섹션역할권한
.text커널의 실행할 코드가 위치할 메모리 영역r-x
.rodata컴파일 시에 값이 정해지고 변경되지 않는 전역 상수 데이터r—
.gotRelocation 관련.. 아직 사용하지 않음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

https://github.com/qemu/qemu