7 분 소요

2.1: Processor initialization

이번 레슨에서는 ARM 프로세서를 더 상세하게 다뤄 볼 것입니다. OS가 활용할 수 있는 필수적인 기능들을 ARM 프로세서는 가집니다. 그러한 기능 중 첫번째는 “Exception levels”입니다.

Exception levels

ARM.v8 아키텍처를 지원하는 각 ARM 프로세서는 4개의 예외 레벨(exception level)을 가집니다. 모든 동작(operation)과 레지스터의 일부만 가능한 프로세서 실행 모드로 예외 레벨(짧게는 EL)을 생각해도 됩니다. 최소 권한 EL은 level 0입니다. 프로세서가 이 레벨에서 동작할 때 오로지 일반 레지스터(general purpose X0 - X30)와 스택 포인터 레지스터(SP)만을 사용합니다. EL0는 메모리에 데이터를 저장하거나 로드하기 위해 STR, LDR 명령어를 사용하는 것도 가능하고 보통 유저 프로그램에서 사용되는 몇 개의 다른 명령어가 가능합니다.

운영체제는 프로세스 고립(process isolation)를 구현하기 위해서 EL을 다뤄야 합니다. 유저 프로세스는 다른 프로세스의 데이터를 접근하면 안됩니다. 이러한 것을 이루기 위해서 운영체제는 각 유저 프로세스를 EL0에서 항상 구동시킵니다. 이 예외 레벨에서 한 프로세스를 구동시키는 것은 오로지 자신의 가상 메모리만을 사용할 수 있고 가상 메모리 설정을 바꾸는 어떤 명령어도 접근할 수 없습니다. 따라서 프로세스 고립를 보장하기 위해서 운영체제는 각 프로세스 마다 별도의 가상 메모리 매핑을 준비해야 하고 유저 프로세스로 실행을 넘기기 전에 프로세서를 EL0 상태로 만들어야 합니다.

운영체제 자체는 보통 EL1에서 작동합니다. 이 예외 레벨에서 구동하면서 프로세서는 가상 메모리 설정을 수정하는 레지스터로 접근을 허용할 뿐만 아니라 몇가지 시스템 레지스터를 접근할 수도 있습니다. RPi 운영체제도 EL1을 사용할 것입니다.

EL2와 3은 사용하지 않을 예정이지만 간단하게 설명해서 왜 이것들이 필요한지 알 수 있기를 바랍니다.

EL2는 하이퍼바이저를 사용할 때 사용됩니다. 이러한 경우 호스트 OS는 EL2 수준에서 동작하고 게스트 OS가 EL1만을 사용합니다. 호스트 OS가 유저프로세스를 고립하는 방법과 같은 방식으로 게스트 OS들을 격리 할 수 있게 해줍니다.

EL3는 ARM “Secure World”에서 “Insecure world”로 변경할 때 사용됩니다. 이러한 추상화는 다른 “worlds”에서 구동되는 소프트웨어 간 완전한 하드웨어 고립을 제공하기 위해서 존재합니다. “Insecure world”에서의 앱은 “Secure worlds”에 속한 정보를 접근하거나 수정할 수 있는 방법이 없습니다. 그리고 이러한 제한은 하드웨어 수준에서 강제됩니다.

Debugging the kernel

다음으로 하고 싶은 것은 어떤 EL을 현재 사용하고 있는지 확인하는 것입니다. 그러나 확인을 하려 할 때 커널이 오로지 스크린에 어떤 상수 문자열만을 출력할 수 있다는 것을 깨달았습니다. 하지만 필요한거는 printf 함수의 어떤 아날로그 입니다. printf를 가지고 레지스터와 변수들의 값을 쉽게 보여줄 수 있습니다. 다른 디버거 지원이 없고 printf가 프로그램내에서 무슨 일이 발생하는지 알아낼 유일한 수단이 되기 때문에 이러한 기능은 커널 개발에서 중요합니다.

RPi OS를 위해서는 새로 개발하지 않고 기존의 printf를 사용하기로 결정했습니다. 이 함수는 커널 개발자 입장에서 딱히 흥미롭지 않고 문자열 다루는 것을 위주로 구성되 있습니다. 사용되는 구현은 매우 작고 외부 의존성이 없기에 쉽게 커널에 통합됩니다. 해야하는 유일한 일은 스크린에 하나의 단일 문자를 보낼 수 있는 putc 함수를 정의하는 것입니다. 이러한 함수는 여기에 정의 되 있고 이미 존재하는 uart_send 함수를 사용했습니다. 또한 printf 라이브러리를 초기화하고 putc 함수의 위치를 특정지어야 합니다. 이러한 작업은 한줄의 코드로 수행됩니다.

Finding current Exception level

이제 printf 함수가 갖춰졌다면 원래의 작업: OS가 부팅시 어떤 예외 레벨에 있는지 확인하는, 을 완료할 수 있습니다. 이 질문을 답할 수 있는 한 작은 함수는 여기에 정의되 있고 아래와 유사합니다.

.globl get_el
get_el:
    mrs x0, CurrentEL
    lsr x0, x0, #2
    ret

여기서 mrs 명령어를 사용하여 CurrentEL 시스템 레지스터에서 x0 레지스터로 값을 읽어 드립니다. 그 후 이 값을 오른쪽을 2 bit 이동 시킵니다(CurrentEL 레지스터안에 첫 2 비트가 예약되 있고 항상 0을 가지기 때문에 이러한 비트 이동을 해야합니다). 그리고 최종적으로 x0 레지스터에 현재 예외 레벨을 나타내는 정수 값을 가집니다. 이제 남은 일은 아래와 같이 이 값을 보여주는 것 뿐입니다.

int el = get_el();
    printf("Exception level: %d \r\n", el);

만약 이 과정을 재생산한다면 Exception level: 3을 스크린에서 봐야만 합니다.

Changing current exception level

ARM 아키텍처에서는 이미 상위 레벨에서 구동하는 소프트웨어의 참여 없이 프로그램이 자체 예외 레벨을 상승시킬 방법은 없습니다. 이것은 완벽히 말이 됩니다. 아니라면 어떤 프로그램이든 할당된 EL을 탈출할 수 있고 다른 프로그램의 데이터를 접근할 수 있을 것입니다. 현재 EL은 예외(exception)가 발생할 때만 변경됩니다. 프로그램이 어떤 비정상적인(illegal) 명령어를 실행할 때 이러한 경우가 발생가능합니다.(예를 들어서 존재하지 않는 주소로 메모리 접근을 시도하거나 0으로 나누기를 시도할 때) 또한 의도적으로 예외를 발생하기 위해 프로그램이 svc 명령어를 실행할 수 있습니다. 하드웨어가 생성한 인터럽트는 또한 특별한 타입의 예외로 다뤄집니다. 예외가 발생할 때 마다 다음의 일련의 과정이 발생합니다.(설명에서 EL n에서 n dms 1, 2, 또는 3이라고 가정합니다.)

  1. 현재 명령어의 주소를 ELR_ELn 레지스터에 저장합니다.(이 레지스터는 Exception link register라고 불립니다.)
  2. 현재 프로세서 상태는 SPSR_ELn 레지스터에 저장됩니다.(Saved Program Status Register)
  3. 예외 핸들러(exception handler)가 실행되고 수행해야 할 일을 수행합니다.
  4. 예외 핸들러는 eret 명령어를 호출합니다. 이 명령어는 SPSR_ELn으로 부터 프로세서 상태를 복원하고 ELR_ELn 레지스터에 저장된 주소로 부터 실행 시작을 복원합니다.

실제로는 예외 핸들러 또한 모든 범용 레지스터(general purpose register)의 상태를 저장하고 돌아올 때 복원을 해야하기 때문에 좀 더 복잡합니다. 그러나 다음 레슨에서 이러한 과정을 좀 더 자세하게 논의할 것입니다. 지금은 일반적으로 과정을 이해해야하고 ELR_ELnSPSR_ELn 레지스터의 의미를 기억해야 합니다.

알아야 할 한 가지 중요한 점은 예외 핸들러는 예외가 발생한 같은 위치로 돌아갈 의무가 있지는 않다는 것입니다. ELR_ELmSPSR_ELn은 수정가능 하고 예외 핸들러는 원하면 둘다 수정할 수 있습니다. 우리 코드에서 EL3 에서 EL1으로 바꾸려고 할 때 이러한 기술 이점으로 사용할 것입니다.

Switching to EL1

엄밀히 말하자면 우리 운영체제는 EL1 으로 바꿀 의무는 없지만 EL1은 모든 일상적인 운영체제 작업을 수행하기 위한 권한의 적절한 집합을 가지기 때문에 당연한 선택입니다. 예외 레벨 교체가 어떻게 이뤄지는지 확인하는 것은 흥미로울 것입니다. 이를 하는 소스 코드를 확인해봅시다.

master:
    ldr    x0, =SCTLR_VALUE_MMU_DISABLED
    msr    sctlr_el1, x0        

    ldr    x0, =HCR_VALUE
    msr    hcr_el2, x0

    ldr    x0, =SCR_VALUE
    msr    scr_el3, x0

    ldr    x0, =SPSR_VALUE
    msr    spsr_el3, x0

    adr    x0, el1_entry        
    msr    elr_el3, x0

    eret       

보이듯이 이 코드는 몇 개의 시스템 레지스터를 설정하는 코드로 이뤄져있습니다. 이제 이 레지스터들을하나씩 확인할 것입니다. 그러기 위해서는 AArch64-Reference-Manual을 다운로드 받아야 합니다. 이 문서는 ARM.v8 아키텍처의 자세한 명세를 포함합니다.

SCTLR_EL1, System Control Register(EL1), Page 2654 of AArch64-Reference-Manual.

    ldr    x0, =SCTLR_VALUE_MMU_DISABLED
    msr    sctlr_el1, x0       

여기서 sctlr_el1 시스템 레지스터의 값을 설정합니다. sctlr_el1은 프로세서가 EL1에서 작동할 때 프로세서의 다른 매개변수들을 설정하는 역할을 한다. 예를 들어서 캐시가 활성화 되는지 조절하고 우리에게 중요한 MMU(Memory Mapping Unit)이 켜져 있는지 결정할 수 있습니다. sctlr_el1은 EL1 보다 높거나 같은 모든 예외 레벨에서 접근가능합니다.(이러한 것은 _el1 접미어로 추측할 수 있습니다)

SCTLR_VALUE_MMU_DISABLED 상수는 여기 정의되 있고 각각의 비트 값이 아래와 같이 정의되 있다.

  • #define SCTLR_RESERVED (3 << 28) | (3 << 22) | (1 << 20) | (1 << 11) sctlr_el1 레지스터의 설며에서 몇 비트들은 RES1으로 마크됩니다. 이러한 비트는 차후 사용을 위해 예약되 있고 1로 초기화 되어야 합니다.
  • #define SCTLR_EE_LITTLE_ENDIAN (0 << 25) 예외 엔디안. 이 필드는 EL1에서 명시적 데이터 접근의 엔디안을 제어합니다. 프로세서를 little-endian 포맷에서만 작동하게 설정합니다.
  • #define SCTRL_EOE_LITTLE_ENDIAN (0 << 24) 이전 필드와 유사하지만 이번 거는 EL1이 아닌 EL0에서 명시적 데이터 접근의 엔디언을 제어합니다.
  • #define SCTLR_I_CACHE_DISABLED (0 << 12) 명령어 캐시를 비활성화 합니다. 단순함을 위해 모든 캐시를 비활성화 할 것입니다. 데이터와 명령어 캐시에 대해 여기서 더 찾아보실 수 있습니다.
  • #define SCTLR_D_CACHE_DISABLED (0 << 2) 데이터 캐시를 비활성화합니다.
  • #define SCTLR_MMU_DISABABLED (0 << 0) MMU를 비활성화 합니다. MMU는 레슨 6 전까지 비활성화 되있어야 합니다. 레슨 6에서는 페이지 테이블을 준비하고 가상 메모리를 다룰예정입니다.

HCR_EL2, Hypervisor Configuration Register (EL2), Page 2487 of AArch64-Reference-Manual.

    ldr    x0, =HCR_VALUE
    msr    hcr_el2, x0

자체 하이퍼바이저를 구현하지 않을 예정입니다. 하지만 다른 설정들 간에 하이퍼바이저가 EL1에서 실행상태를 컨트롤 하기 때문에 사용해야 합니다. 실행 상태는 AArch64여야 합니다 AArch32가 아니라. 여기서 설정됩니다.

SCR_EL3, Secure Configuration Register (EL3), Page 2648 of AArch64-Reference-Manual.

    ldr    x0, =SCR_VALUE
    msr    scr_el3, x0

이 레지스터는 보안 설정을 수행하는 역할을 합니다. 예를 들어 모든 하위 레벨이 “secure” 상태 또는 “nonsecure” 상태에서 구동되는지 제어할 수 있습니다. EL2에서 실행 상태 또한 제어합니다. 여기서 EL2가 AArch64 상태에서 실행되게 설정하고 모든 하위 예외 레벨은 “nonsecure”입니다.

SPSR_EL3, Saved Program Status Register (EL3), Page 389 of AArch64-Reference-Manual.

    ldr    x0, =SPSR_VALUE
    msr    spsr_el3, x0

이 레지스터는 이미 여러분에게 익숙할 것입니다. - 예외 레벨을 변경하는 과정에서 논의 됬었습니다. spsr_el3은 프로세서 상태를 포함합니다. 프로세서 상태는 eret 명령어를 실행한 후 복원 될 것입니다. 프로세서 상태가 어떤 것인지 설명하는 것은 의미가 있습니다. 프로세서 상태는 아래의 정보를 포함합니다.:

  • Condition Flags 이 플래그는 이전 실행 수행에 대한 정보를 가집니다: 결과가 부정(N flag)인지 0( A flag)인지 그리고 부호 없는 오버플로우(C flag)인지 부호 있는 오버플로우(V flag)인지. 이 플래그의 값은 조건 브랜치 명령어(conditional branch instructions)에서 사용됩니다. 예를 들어 b.eq 명령어는 마지막 비교 수행이 0과 같은 경우에만 제공된 라벨로 점프합니다. 프로세서는 Z flag가 1로 설정됬는지 확인하여 이것을 확인합니다.
  • Interrupt disable bits 이 비트들은 다른 유형의 인터럽트들을 활성화/비활성화 하게 해줍니다.
  • 예외가 처리된 후 프로세서 실행 상태를 완전히 복원하기 위해 필요한 다른 정보들

보통 spsr_el2는 EL3에 예외가 발생한 경우 자동으로 저장됩니다. 그러나 레지스터는 쓰기가 가능하고 이러한 사실을 이용하여 프로세서 상태를 자체적으로 준비합니다. SPSR_VALUE여기서 준비되고 아래 필드들을 초기화 합니다.

  • #define SPSR_MASK_ALL (7 << 6) EL을 EL1으로 바꾼 후 모든 타입의 인터러브는 마스크 될 것입니다.(또는 비활성화 됩니다. 이거는 같습니다)
  • #define SPSR_EL1h (5 << 0) EL1에서 자체 할당된 스택 포인터를 사용하거나 EL0 스택 포인터를 사용할 수 도 있습니다. EL1h 모드는 EL1 할당된 스택 포인터를 사용하는 것을 의미합니다.
    adr    x0, el1_entry        
    msr    elr_el3, x0

    eret    

elr_el3eret 명령어가 실행된 후 돌아갈 주소를 가지고 있습니다. 여기서 el1_entry 라벨의 주소로 이 주소를 설정합니다.

Conclusion

el1_entry 함수로 들어갈 때 실행은 이미 EL1 모드에 있어야 합니다. 도전해 보세요!

태그:

카테고리:

업데이트:

댓글남기기