2023년 12월 31일 일요일

AARCH64 bare metal 로 동작 시키기 - 2

디버깅:

BSP작성시 Trace32같은 장비를 사용하기도 하지만, qemu상에서 개발하므로 gdb를 사용하여 개발이 가능하다.  gdb사용을 위해 아래와 같이 qemu를 시작한다.

$ qemu-system-aarch64 -M virt,virtualization=on,gic-version=3 -m 4G -smp 2 -kernel my_bl3.elf -S -gdb tcp::1234,ipv4 -nographic

-S옵션을 주었으므로 virt machine은 시작되자마자 suspend상태로 gdb를 기다린다.  이제 다른 터미널에서 "gdb-multiarch"를 실행하여 gdb를 연결한다. 


$ gdb-multiarch -q ./my_bl33.elf

Reading symbols from ./my_bl33.elf...

(gdb) target remote:1234

Remote debugging using :1234

_start () at ./src/_start.s:10

10              mrs     x0, mpidr_el1       // get the CPU ID


참고 자료:

아래 책들을 참조하였다. 

1. ARM 64-Bit Assembly Language by Pyeatt Ph.D., Larry D (amazon.com)

이 책에서 부팅 후 초기 ASM code 와 PL011 UART 기기를 사용한  입출력 방법에 관한 정보를 얻을 수 있었다. 

하지만, 이 책에서는 ARM RaspberryPI를 기준으로 설명하므로 GIC를 사용한 SPI/PPI 처리 방법에 대한 정보가 없었고, generic timer 처리 관련 정보 또한 얻을 수 없었다. 

2. 임베디드 OS 개발 프로젝트 | 이만우 - 교보문고 (kyobobook.co.kr)

내가 구현하고자 하는 것과 거의 동일한 작업을 armv7 에서 구현하고 설명한 책으로 개발 진행에 큰 도움이 되었다.  특히 printf 구현 관련 부분은 나의  주된 관심사가 아니었지만 개발 진행에 꼭 필요한 부분이었기 때문에 재구현하지 않고 이만우님의 코드를 사용하여 개발을 진행 하였다. 

3. 시스템 소프트웨어 개발을 위한 Arm 아키텍처의 구조와 원리 | 김동현 - 교보문고 (kyobobook.co.kr)

Armv8 및 GICv3 관련 정보를 얻을 수 있었다. 

4. ARM 공식 문서

GICv2 : ARM Generic Interrupt Controller Architecture Specification - Version 2.0 (B)
GICv3 : Arm A-profile Architecture Registers


Exception Vector table:

1. aarch64에서는 exception vector table주소가 고정되어 있지 않고, VBAR_ELx 레지스터에 설정하면 된다.  다만 이 레지스터들은 Bit 63:11 만 사용되므로, 이 주소에 맞도록 시작 위치가 align만 되면 된다. 

         .align      11 

2. exception vector는 EL별로 존재하는데, 우리는 EL1만 관심이 있으므로 EL1용 만 작성한다. 

3. exception vector는 아래와 같이 4가지 부분으로 나뉜다. 

- exception from sp0 : 사용 안함.
- exception from current el : 현재 el에서 exception 발생 시 사용.
- exception from lower el(aarch64) : 현재 el보다 낮은 el에서 64bit모드로 실행 중 exception발생 시 사용
- exception from lower el(aarch32) : 현재 el보다 낮은 el에서 32bit모드로 실행 중 excepton발생 시 사용. 

boot loader를 항상 el1에서 동작하도록 작성한다면, "exception from current el" 부분에만 실제로 의미 있는 코드를 작성하면 된다. 

4. exception은 아래와 같이 4가지 종류로 나뉜다

- sync
- irq
- fiq
- serror

일반적으로 fiq는 secure world에서 사용한다. 우리의 목표를 구현하기 위해서는 irq만 처리하면 된다. 


프로그램 시작 주소

qemu실행 시 -M virt, virtualization=on,dumpdtb=my_bl.dtb ... 와 같이 dumpdtb를 지정하여 virt machine의 dtb파일을 얻을 수 있음. 

- dtb파일을 text형식인 dts로 변환할 수 있다. 

$ dtb -I dtb -O dts -o my_bl.dts ./my_bl.dtb

아래와 같이 메모리 주소가 0x40000000으로 표시됨. 

 18 »   memory@40000000 {
 19 »   »   reg = <0x00 0x40000000 0x01 0x00>;
 20 »   »   device_type = "memory";
 21 »   };


따라서 작성하는 프로그램을 0x40000000 이후에 로드하면 실행 가능함.  이때 해당 프로그램의 link script에 로드할 주소를 지정하여야 함. 

  6 SECTION
  7 {
  8  . = 0x40400000;
  9  . = ALIGN(8);

위와 같이 링커 스크립트 파일을 설정하여 빌드한 후 qemu에서 elf포맷으로 로드하면 (-kernel my_bl3.elf ) qemu가 알아서 해당 위치에 로드시켜 준다. 

- u-boot의 경우 0x40000000에 dtb를 로드하고 그 뒷부분에 u-boot코드가 로드되며 u-boot가 실행하는 프로그램은 0x40400000 이후에 로드해야만 실행 가능하다.(go addr)  추후 u-boot 를 사용하는 경우를 고려하여 0x40000000대신 0x40400000으로 시작 주소를 설정하였다. 

ARM GIC interrupts:

SGI : S/W generated interrrupt (INTID #0 ~ #15) : IPI

PPI : Private Peripheral interrupt (INTID #16 ~ #31) : 특정 core용

SPI : Shared Peripheral interrupt (INTID #32 ~ #1019) : 일반 interrupts

LPI : Local Peripheral interrupt (INTID #8192 ~) - MSI : for PCI device

 

GIC 설정:

매뉴얼을 보면 너무 많은 register들이 있어 어떤 순서로 어떤 레지스터를 설정하면 좋을지 알기 어렵다.  하지만 virt machine 동작에 꼭 필요한 레지스터들은 많지 않았다. 다만 실제 HW에서는 추가 설정이 필요할 수도 있을 것 같다. 

설정 순서는 gicd->gicr->gicc로 하였다. 

1. Distributor / gicd설정

a. Disable GICD_CTRL

b. (GICv3 only) SPI에 대해 Secure/Non-secure group을 Non-secure group 1로 설정

    group status=1 GICD_IGROUPR

    group modifier=0, GICD_IGRPMODR

c. Route SPI interrupts to CPU0 ( GICD_ITARGETSR )

d. Interrpt trigger type설정 ( GICD_ICFGR ) 

    : SPI는 모두 level trigger로 설정, PPI는 모두 edge trigger로 설정

e. Enable GICD_CTRL


2. Redistributor / gicr설정(GICv3 or later only) 

a. SGI/PPI에 대해 Secure/Non-secure group을 Non-secure group1로 설정

b. Enable SGI/PPI : GICR_ISENABLER0

c. PPI Interrup trigger type설정 (GICR_ICFGR1)

Redistributor는 GICv3에서 도입되었기 때문에 GICv2에서는 설정이 필요하지 않다. 어떤 CPU로도 라우팅이 가능한 SPI는 gicd에서 설정하고, 실행 core가 결정된 SGI/PPI는 gicr에서 설정하는 것으로 보인다.  

3. CPU interface / gicc설정

a. GICC_PMR for GICv2, ICC_PMR_EL1 for GICv3  : Priority Mask설정

b. GICC_CTRL = 1 for GICv2,  ICC_IGRPEN1_EL1 = 1 for GICv3

- Priority Mask로 허용할 최저 interrupt 우선순위를 설정한다. 우선순위는 0~255인데 0이 가장 높다. 0xFF로 설정하면 대부분의 우선순위를 가지는 인터럽트가 모두 허용된다. 

- CPU interface register라고 불리는 gicc 레지스터는 GICv2에서는 memory mapped레지스터였지만, GICv3에서는 system register가 되어 access 하는 방법이 달라졌다. (mrs/msr사용)

- gcc compiler에서는 aarch64의 cpu interface register이름을 해석하지 못하므로 이를 적절히 해석해주는 매크로가 필요하다. 

ex)
#define ICC_IGRPEN1_EL1     S3_0_C12_C12_7


Timer Interrupt

- ARM generic timer의 INTID = 30이다. (PPI)
- Trigger type을 edge로 설정해주어야 동작함 - Redistributor설정 시 일괄 설정함

아래는 gic-version=3 옵션을 사용한 virt machine의 dts파일의 일부이다. 

385 »   timer {
386 »   »   interrupts = <0x01 0x0d 0x04 0x01 0x0e 0x04 0x01 0x0b 0x04 0x01 0x0a 0x04>;
387 »   »   always-on;
388 »   »   compatible = "arm,armv8-timer\0arm,armv7-timer";
389 »   };

- 매뉴얼 및 dtb파일에는 level trigger(0x4)로 표시되어 있음.  확인 필요. 
- Interrupt가 0x0d, 0x0e, 0x0b, 0x0a 4개가 표시되어 있는데, 이 번호가 PPI시작 주소인 16을 더해주면 29,30,27,26 이 된다. generic timer의 INTID는 30인데, 왜 DTS에는 4개 의 interrupt가 표시되는지 확인 필요. 

구현은 아래 내용을 참조하였다.
https://lowenware.com/blog/aarch64-gic-and-timer-interrupt

UART interrupt:

dtb파일에서  UART장치가 pl011이라는 것과 시작 주소가 0x9000000이라는 것,  INTID가 33이라는 것을 알 수 있음.   

308 »   pl011@9000000 {
309 »   »   clock-names = "uartclk\0apb_pclk";
310 »   »   clocks = <0x8000 0x8000>;
311 »   »   interrupts = <0x00 0x01 0x04>;
312 »   »   reg = <0x00 0x9000000 0x00 0x1000>;
313 »   »   compatible = "arm,pl011\0arm,primecell";
314 »   };

- dtb파일의 interrupt번호는 SPI의 번호이므로 INTID로 변환을 위해 32를 더해주어야 함.( 1 + 32 = 33(INTID)
- interrupt는 level trigger방식임. (0x04)


그 외 필요해 보이는 추가 설정:

gicd:

GICD_ICENABLER

GICD_ICPENDR

GICD_PRIORITY

GICD_CTLR( GICD_CTLR_ARE_NS | GICD_CTLR_EN_GR1_NS )

GICD_I ROUTER


gicr:

GICR_TYPER

GICR_IPRIORITY0

GICR_WAKER

GICR_ICPENDR0

GICR_ICENABLER0




AARCH64 bare metal 로 동작 시키기 - 1

2023년 연말을 최대한 유의미하게 보내고자 아래와 같이 목표를 세우고 달성하고자 노력하였다. 

목표: 

aarch64 시스템을 최소 설정으로 OS없이 동작하도록 초기화. 

  1. System 동작 확인
  2. GIC를 사용한 (SPI, PPI) 설정법 확인
  3. UART를 사용한 입출력 - interrupt로 처리(SPI)
  4. timer interrupt처리 (PPI)

대부분의 경우 새로운 BSP는 기존 BSP를 수정하여 만들고, u-boot등 기성 boot loader를 사용하기 때문에 주의 깊게 살펴볼 기회가 없었던 aarch64 시스템 초기화 방법 및 GIC 사용법을 정리하고자 한다. 


준비: 

( ubuntu or WSL/Windows )

1. cross-compiler설치

$ sudo apt-get update
$ sudo apt-get install gcc-aarch64-linux-gnu

2. qemu설치

$ sudo apt-get install qemu-system-aarch64

우선 테스트를 위한 HW를 선정해야 한다.  rpi4 등 실제 HW에서도 진행할 수 있지만,  qemu환경을 사용하면, 실제 HW없이 구현 및 테스트가 가능하므로 일단 qemu의 aarch64환경에서 진행하기로 결정하였다. 

qemu는 여러가지 HW의 emulation을 지원하는데, 그중 "virt" machine을 선정하였다. 

‘virt’ generic virtual platform (virt) — QEMU documentation

- 2023년 말 기준 qemu정식 버전은 rpi4를 지원하지 않지만, rpi4 emulation을 지원하는 qemu를 쉽게 구할 수 있었고, 빌드도 쉽게 가능하였지만 일단 "virt"에서 먼저 구현하기로 하였다.  rpi는 ARM의 GIC interrupt controller가 아닌 다른 interrupt controller를 사용하기 때문에 study의 목적에 부합하지 못하였다. 

- qemu virt machine은 GICv2를 지원하지만, 옵션을 사용하여 GICv3를 선택할 수 도 있다.  먼저 GICv2에서 동작을 확인한 후 GICv3까지 동작시키는 것을 목표로 하였다. 


aarch64 부팅 과정 고찰:

aarch64는 아래와 같은 복잡한 부팅 과정을 가진다.  이 과정을 이해해야 어떤 부분부터 직접 작성할지 결정할 수 있었다. 

BL1 -> BL2  -> BL31

                -> BL32

                -> BL33

BL1 : 1st stage boot loader

BL2 : 2ed stage boot loader

BL3x: 3rd stage boot loader

BL31 : EL3 -runtime f/w (Secure monitor)
BL32 : Secure EL1 f/w : TEE OS
BL33 : Non-Secure EL1 Non-trusted f/w ( u-boot etc) 

최종적으로 일반 OS를 구동 시키고자 한다면, BL33에 해당하는 부분을 작성하는 것을 목표로 잡을 수 있었다. 즉 기존의 u-boot를 대신하여 실행할 수 있는 f/w작성하고자 한다. 


qemu virt machine을 사용해서 일단 u-boot를 실행해 보자. 

$ qemu-system-aarch64 -M virt,virtualization=on -m 4G -smp 2 -kernel u-boot -nographic


위에서 "u-boot"는 elf 형식의 u-boot 파일명이다. 

이 경우  BL1/BL2/BL31/BL32가 사용되지 않았다. BL33(여기서는 u-boot)만 사용하여 동작이 가능하도록 qemu가 emulation해준다.  따라서 다른 BL을 신경 쓰지 않고 작업 진행이 가능하다. 


만약 굳이 BL1, BL3, BL31, BL32, BL33을 사용해서 동작 시킨다면  아래와 같이 할 수 있다. 

$ qemu-system-aarch64 -M virt,secure=on -smp 2 -m 4G -bios bl1.bin -d unimp -semihosting-config enable=on -nographic

단 이때 실행 디렉토리에 bl1.bin, bl2.bin, bl31.bin bl33.bin이 있어야 한다. 

bl1.bin, bl2.bin, bl31.bin은 아래에서 구할 수 있었다. 

GitHub - ARM-software/arm-trusted-firmware: Read-only mirror of Trusted Firmware-A

bl33.bin은 u-boot.bin의 symbolic link로 만들었다. 

실제 하드웨어와 유사하게 u-boot를 실행한 상태에서 내가 작성한 프로그램을 메모리에 로드하여 go addr 명령으로 실행하는 것이 가능하지만, 굳이 그렇게 할 필요는 없다.  또한 bl1이나 bl2, bl31등의 개발에 관심이 있는 것이 아니라면 굳이 secure=on을 사용하여 EL3를 사용할 이유도 없다 (일반적으로 HW 업체에서 자신들의 CPU에 맞는 BL파일들을 제공한다).  따라서 이후에서는 virtualization=on을 사용하여 EL2에서 시스템을 시작 시키며, u-boot 대신 직접 작성하는 바이너리를 올려서 진행하기로 하였다. 

 

빌드

aarch64-linux-gnu-as -march=armv8-a -mcpu=cortex-a72 -g -c ...
aarch64-linux-gnu-gcc -DGICV3 -march=armv8-a -mcpu=cortex-a72 -g -I. -fno-stack-protector -mgeneral-regs-only -c ...
aarch64-linux-gnu-ld -n ... -T virt.ld -o my_bl33.elf

개발 중 unexpected sync. exception이 발생 하는 문제가 있었다.  다행이 최근에 회사 동료가 동일한 문제를 해결한 적이 있어 해결책을 알고 있었다.  -mgeneral-regs-only 추가. 


- 계속 -