kernel/head.S – ARM64 (new for v6.0)

<kernel v6.0>

kernel/head.S – ARM64 (new for v6.0)

시스템의 부트로더로부터 커널을 주 메모리에 로드하고 커널이 최초로 호출되는 지점이 head.S의 _head: 레이블이다. 이 시작 코드에는 커널 이미지의 헤더 및 UEFI(Unified Extensible Firmware Interface) PE(Portable Executable) 헤더등을 포함하며 최초 C 언어로 처리하기 힘든 시스템 설정들을 여기 어셈블리에서 처리한 후 C 시작 루틴인 start_kernel()로 jump 하는 역할을 한다. 이 코드들은 물리 DRAM의 2M 단위로 정렬된 주소라면 어떠한 위치에 배치하여도 동작할 수 있도록 모든 코드가 position independent 코드들로 구성되어 있다.

 

다음 그림은 부트로더로부터 커널의 어셈블리 부분과 C 부분이 차례로 호출되는 과정을 보여준다.

 

부트로더가 하는 일 요약

커널을 로드하여 동작시키기 전까지 부트로더가 수행하는 일들은 다음과 같다.

  • 주 메모리의 초기화를 수행한다.
  • DTB(Device Tree Blob)를 주 메모리에 로드한다.
    • x0 레지스터에 DTB 물리 시작 주소를 담는다.
    • x1~x3 레지스터는 미래에 사용할 레지스터로 예약하였다.
  • 커널 이미지를 주 메모리에 로드한다.
  • (옵션) 압축된 커널 이미지(예: Image.gz 등)를 사용하는 경우 decompress를 수행한다.
    • 커널 이미지 헤더가 포함된 압축 풀린 커널 이미지는 2M 단위로 정렬하여야 한다.
  • 커널 이미지의 첫 주소로 jump하여 커널의 head.S 루틴을 시작한다.
  • DTB는 Device Tree 계층도를 Device Tree Script로 작성한 후 바이너리 형태로 변환한 형태며, 자세한 내용은 다음 문서를 참고한다.

 

커널 진입전 요구사항

커널 진입 전 부트로더는 다음 조건을 만족해야 한다.

  • cpu의 레지스터
    • x0: boot cpu인 경우 DTB 시작 물리 주소, boot cpu를 제외한 나머지 secondary cpu인 경우 0 (Reserved for Future)
    • x1~x3: 0 (Reserved for Future)
  • MMU 및 캐시 상태
    • MMU: off
    • D-Cache: off
    • 로드된 커널 이미지 영역에 대해 PoC까지 clean 상태여야 한다.
  • DMA 장치
    • 모든 DMA(Direct Memory Access) 장치들은 DMA 기능이 정지되어 있어야 한다.
  • CPU mode
    • 모든 cpu들의 PSTATE.DAIF는 모두 마스크되어야 한다. (디버그, SError, IRQ 및 FIQ의 마스크)
      모든 CPU는 EL2 또는 non-secure EL1에 있어야 하고 동일해야 한다.
  • 아키텍처 타이머
    • CNTFRQ 레지스터는 타이머 주파수로 프로그래밍해야 한다.
    • CNTVOFF 레지스터는 모든 CPU에서 동일한 값으로 프로그래밍해야 한다.
    • 커널이 EL1에서 동작할 경우 하이퍼바이저(el2)가 있는 경우 CNTHCTL_EL2.EL1PCTEN을 설정해야 한다.
  • 그 외 시스템 레지스터들
    • SCR_EL3.FIQ
      • 모든 cpu들에 동일한 값이 사용되어야 한다.
    • GICv3가 v3 모드에서 사용될 때
      • EL3 존재시
        • ICC_SRE_EL3.Enable=1
        • ICC_SRE_EL3.SRE=1
        • ICC_CTLR_EL3.PMHE 모든 cpu들에 동일한 값 사용
      • 커널이 EL1에서 동작시
        • ICC.SRE_EL2.Enable=1
        • ICC_SRE_EL2.SRE=1
      • Device Tree 또는 ACPI 테이블에 GICv3 인터럽트 컨트롤러에 대해 기술되어야 한다.
    • GICv3가 v2 호환모드에서 사용될 때
      • EL3 존재시
        • ICC_SRE_EL3.SRE=0
      • 커널이 EL1에서 동작시
        • ICC_SRE_EL2.SRE=0
      • Device Tree 또는 ACPI 테이블에 GICv2 인터럽트 컨트롤러에 대해 기술되어야 한다.
    • Pointer Authentication
    • Activity Monitors Unit v1

 

head.S가 하는일 요약

어셈블리 언어로 작성된 head.S 가 커널의 시작점이다. C 언어로 할 수 없는 아키텍처 등의 설정 등을 head.S에서 수행 한 후 이어서 C 언어로 작성된 첫 실행 지점인 start_kernel() 함수가 호출된다. 그런 후 곧바로 setup_arch() 함수에서 나머지 아케틱처 설정들을  C 언어로 수행한다. 다음은 head.S에서 어셈블리 언어로 수행하는 작업들이다.

  • 하이퍼 바이저 모드(el2)가 사용되는 경우 EL2 exception 벡터 준비
  • 부트 cpu 초기화 및 커널 운영체제용 EL1 exception 벡터 준비
  • 필요시 커널 가상 공간에 매핑될 커널 이미지의 위치를 랜덤하게 변경
  • 6 종류 종류별 페이지 테이블 중 init_pg_dir, init_idmap_pg_dir, idmap_pg_dir 페이지 테이블 생성 및 활성화
  • 커널용 스택 준비
  • MMU를 켜서 가상 주소 체제로 전환
  • 마지막으로 C로 작성된 커널 시작 함수인 start_kernel()로 점프

 


커널 이미지 위치

PAGE_OFFSET

arch/arm64/kernel/head.S

#if (PAGE_OFFSET & 0x1fffff) != 0
#error PAGE_OFFSET must be at least 2MB aligned
#endif

리니어(연속) 매핑이 시작되는 가상 주소이다. 이 영역에 물리 메모리를 리니어(연속) 매핑한다.

 

arch/arm64/include/asm/memory.h

/*
 * PAGE_OFFSET - the virtual address of the start of the linear map, at the
 *               start of the TTBR1 address space.
 * PAGE_END - the end of the linear map, where all other kernel mappings begin.
 * KIMAGE_VADDR - the virtual address of the start of the kernel image.
 * VA_BITS - the maximum number of bits for virtual addresses.
 */
#define VA_BITS                 (CONFIG_ARM64_VA_BITS)
#define _PAGE_OFFSET(va)        (-(UL(1) << (va)))
#define PAGE_OFFSET             (_PAGE_OFFSET(VA_BITS))

예) CONFIG_ARM64_VA_BITS=48로 설정

  • 가상 공간의 크기가 2^48 = 256TB로 결정된다.
  • 이 때 PAGE_OFFSET 값은 0xffff_0000_0000_0000

 

__PHYS_OFFSET

커널이 동작할 물리 시작 주소 offset 값으로, 실제 물리 주소를 알고자 할 때 adrp 명령과 함께 사용되었었는데, 커널 v6.0-rc1에서 제거되었다.

 

KERNEL_START & KERNEL_END

arch/arm64/include/asm/memory.h

#define KERNEL_START      _text
#define KERNEL_END        _end

커널 이미지의 코드(_text) 시작 주소가 KERNEL_START 이다. 그리고 커널 이미지의 끝이 _end로 .bss 섹션도 포함된다.

 

TEXT_OFFSET

기존 AArch64 커널(~v4.6까지)에서 2M 정렬된 커널에서 실제 커널 시작 코드가 배치되는 위치로 jump 하기 위해 TEXT_OFFSET을 사용했었다. 사용되는 값은 제조사가 정한 offset(기존 512K offset) 또는 랜덤 offset 등을 사용해왔었다. 그런데 KASLR 도입 과정에서 relocatable kernel 개념을 적용하여 TEXT_OFFSET이 의미 없어지면서 v5.8-rc1에서 0으로 변경하였다가, v5.8-rc2에서 완전히 제거되었다.

 

다음 그림은 커널 이미지가 2M 정렬되어 위치하는 모습을 보여준다.

 

가상 공간에 커널 이미지 배치

다음 그림은 가상 주소 공간에 배치될 때의 커널 이미지 위치를 보여준다.

  • KASLR(Kernel Address Sanitizer Location Randomization)을 사용하지 않는 경우이다.

 

KASLR(Kernel Address Sanitizer Location Randomization)

  • 보안 목적으로 커널 가상 주소 공간에서 커널 이미지 및 커널 모듈이 위치해 있는 곳을 알 수 없게 런타임에 랜덤 배치한다.이때 자동적으로  RELOCATABLE 커널 옵션이 활성화된다.
    • CONFIG_RANDOMIZE_BASE
      • 커널 이미지의 위치를 런타임에 랜덤하게 변경한다.
    • CONFIG_RANDOMIZE_MODULE_REGION_FULL
      • 커널 모듈의 위치를 런타임에 랜덤하게 변경한다.
    • 참고: arm64: add support for kernel ASLR (2016, v4.6-rc1)

 


EL(Exception Level)

 

시스템 전원이 켜지고 boot cpu가 처음 가동될 때 ARM64 시스템에 구현되어 가지고 있는 가장 최상의 EL(Exception Level)에서 동작한다. exception이 발생하였을 때 EL의 상승이 발생하고 exception 처리가 끝나면 다시 하강시킬 수 있다. 32비트 ARM 시스템에서는 하이퍼바이저나 Secure Monitor가 ARM SoC에 내장되지 않은채 출하된 경우도 있었지만, 64비트 ARM 시스템에서는 거의 대부분의 확률로 모두 탑재되어 있다. 따라서 대부분 boot cpu가 처음 동작하는 EL은 EL3가 된다.

 

다음 그림은 exception이 발생한 경우 EL0를 제외하고 해당 EL 또는 상위 EL로 이동하는 모습을 보여준다.

  • User application이 동작하는 EL0의 경우 privilidge 권한이 없어 exception 처리 루틴을 동작시킬 수 없다.
  • 인터럽트 exception이 발생하였을 경우 인터럽트 번호마다 어떠한 EL로 이동하여 처리할지 최상위 EL에서 설정해야 한다.

 

다음 그림은 명령을 통해 강제로 상위 EL을 호출하는 모습을 보여준다.

  • 명령들은 sync exception으로 분리한다.

 

다음 그림은 exception이 처리된 후 되돌아가는 경로를 보여준다.

  • 하위 EL 또는 동일한 EL로 돌아갈 수 있다. 그러나 상위 EL로 되돌아가는 방법은 없다.

 

VHE(Virtual Host Extension) 기능

부트로더로 부터 리눅스 커널로의 진입 시 boot cpu는 EL1 또는 EL2에 있을 수 있다. 즉 시스템이 처음 부팅하여 리눅스 커널이 하이퍼 바이저 용도로도 사용될 수 있는 상태라면 EL2로 진입하였을 것이고, 그렇지 못한 경우는 EL1으로 시작한다.

 

다음 그림은 boot cpu가 리눅스 커널에 진입할 때 EL1에서 시작한 경우를 보여준다.

  • Hypervisor(Xen 등) 또는 QEMU/KVM 등을 통해 가동되는 Guest용 리눅스 커널은 EL1에서 시작한다.

 

리눅스 커널이 EL2로 시작한 경우 리눅스 커널은 스스로 하이퍼 바이저 역할을 수행할 수 있다. 그런데 기존의 ARM 아키텍처에서는 EL2에서 리눅스 커널을 사용하기 위해서 한 가지 문제점이 있었다. EL2에서 동작하는 레지스터들을 사용하기 위해 방대한 코드들을 모두 변경해야 하는 부담이 있어서, 실제로 그렇게 하지 않고 대부분의 기능을 EL1으로 전환하여 동작시킨다. 즉 EL2로 excecption이 발생하면 일부 stub 코드만을 사용한 후 다시 EL1으로 변경하여 리눅스 커널 코드를 재활용하는 방법을 사용한다. 이렇게 하는 과정에서 EL 전환에 따른 성능 손실이 발생하였고 이를 해결하기 위해 VHE(Virtual Host Extension) 기능이 내장된 ARMv8.2 아키텍처가 나오게 되었다.

 

ARMv8.2-VHE extension을 가지고 있는 경우 EL2 모드에서 EL1 모드로 변경하지 않고 그대로 EL1 레지스터들을 호출하는 방법을 지원하게 되면서 EL 전환에 따른 성능 손실이 일어나지 않게 되었다.

 

다음 그림은 boot cpu가 리눅스 커널에 진입할 때 EL2에서 시작한 경우에 cpu의 VHE 기능 지원 여부에 따라 Host OS가 EL EL2에 위치(VHE)하여 동작하는 것과, 그렇지 않고 EL2에 위치(nVHE)하여 동작하는 방법과 를 보여준다.

  • 대부분의 최근 ARM64 SoC들은 Hypervisor extension과 Secure Monitor extension이 모두 탑재된 상태로 출하된다.

 

VHE 지원시 _ELx 레지스터 접미사 사용 규칙

VHE 기능이 활성화된 상태에서 EL2 모드에서 각 ELx의 레지스터 호출 방법은 다음과 같다.

  • _EL1 레지스터 사용
    • EL1 레지스터에 접근하지 않고 EL2 레지스터에 접근한다.
  • _EL2 레지스터 사용
    • 그대로 EL2 레지스터에 접근한다.
  • _EL12 레지스터 사용
    • EL1 레지스터에 접근한다.

 


Static 페이지 테이블

커널이 컴파일될 때 미리 준비되는 6개 페이지 테이블의 용도는 다음과 같다.

  • init_pg_dir
    • 원래 커널 페이지 테이블은 swapper 페이지 테이블만을 사용했었다. 그런데 보안 향상을 위해 swapper 페이지 테이블을 read-only로 운영하기 위해 별도로 분리하고, 커널 초기 부팅 중에만 잠시 사용하기 위해 read-write 가능한 상태로 init 페이지 테이블을 운영한다.
    • 초기 부팅 중에만 사용되므로 매핑에 사용할 페이지 테이블의 단계와 단계별 갯수는 커널 영역(text, data, bss 섹션)에 한정하여 컴파일 타임에 계산된다.
    • 정규 매핑 준비를 수행하는 paging_init() 후에 swapper_pg_dir로 전환을 수행한 후에는 이 init 페이지 테이블은 더 이상 운영하지 않으므로 할당 해제한다.
  • swapper_pg_dir
    • 커널 부트업 과정에서 정규 매핑이 가능해지는 순간부터 swapper 페이지 테이블이 커널 페이지 테이블로 사용된다.
    • 보안 향상을 위해 읽기 전용으로 매핑하여 사용하며, 매핑 변경을 위해 엔트리 값을 수정해야 하는 경우마다 잠깐씩 fixmap 가상 주소 영역에 읽고쓰기(read-write) 매핑하여 사용한다.
    • 정규 매핑이 가능해지면서 사용되므로 static으로 만들어지는 pgd 테이블을 제외하곤 필요시 동적으로 생성된다.
  • reserved_pg_dir
    • 보안 상향을 위해 copy_from_user() 등의 별도의 전용 API 사용을 제외하고 무단으로 커널 space에서 유저 공간에 접근 못하게 금지하는 SW 에뮬레이션 방식에서 필요한 zero 페이지 테이블이다.
    • ARMv8.0까지 사용되며, ARMv8.1-PAN HW 기능을 사용하면서 이 테이블은 사용하지 않는다.
  • tramp_pg_dir
    • 고성능 cpu를 가진 시스템에서 Speculation 공격을 회피하기 위한 보안 상향을 목적으로 유저 space로 복귀 시 커널 공간에 원천적으로 접근 못하게 하기 위해 별도의 trampoline 페이지 테이블을 운영한다.
    • 이 테이블에는 커널 매핑은 없고, 커널/유저 진출입시 사용되는 SDEI(Software Delegated Exception Interface)를 사용한 trampoline 코드만 매핑되어 사용된다.
  • idmap_pg_dir
    • 가상 주소와 물리 주소가 1:1로 매핑되어 사용될 때 필요한 테이블로 영구적으로 사용된다.
    • 예) MMU enable 시 사용
  • init_idmap_pg_dir

 

다음 그림은 컴파일 타임에 static하게 만들어지는 페이지 테이블의 용도를 보여준다.

  • 리눅스 커널은 이제 5단계(pgd -> p4d -> pud -> pmd -> pte) 테이블을 사용한다. 하지만 ARM64의 head.S 코드는 실제 ARM64 아키텍처가 4단계만 사용하므로 p4d 단계는 배제하고 구현되어 있다.
  • init_으로 시작하는 2개의 페이지 테이블은 컴파일 타임에 필요한 페이지 테이블이 모두 준비된다. pgd를 제외하고 나머지 테이블들은 1개 또는 그 이상으로 구성될 수 있다. 단 4K 페이지를 사용하는 커널 이미지의 매핑에는 2M 단위의 블럭 매핑을 사용하므로 마지막 pte 테이블을 사용하지 않는다.
  • 컴파일 타임에 pgd 테이블만 준비되는 4개의 테이블들은 런타임에 정규 매핑 준비 과정에서 pgd 이후 다음 단계의 페이지 테이블부터 dynamic 하게 생성된다.

 

다음 그림은 static 페이지 테이블들이 배치된 사례를 보여준다.

  • init_pg_dir & init_idmap_pg_dir
    • 4K 페이지, 2M 블럭 매핑을 사용하면서 pte 테이블을 사용하지 않고, 1단계 줄어 3단계로 구성된다.

 

섹션(블럭) 매핑

ARM64 시스템에서 4K 페이지를 사용하는 경우 2M 단위의 섹션(블럭) 매핑을 하여 필요한 페이지 테이블 단계를 1 단계 더 줄일 수 있다. 이 방법으로 init_pg_dir 및 idmap_pg_dir 역시 1 단계를 줄여 사용할 수 있다.

 

다음 그림은 init_pg_dir에서 기존 페이지 테이블 단계(4단계, 3단계)를 1 단계 더 줄여 2M 단위 섹션 (블럭) 매핑된 모습을 보여준다.

  • SWAPPER_PGTABLE_LEVELS가 PGTABLE_LEVELS 보다 1 단계 더 적다.
  • 섹션 블럭 매핑에서 각 단계의 명칭은 아래와 같이 표현하였다.
    • 좌측 그림: ARM64 아키텍처로 보면 lvl0 -> lvl1 -> lvl2 -> 2M이고, 매크로 코드를 공유하여 사용하므로 코드 관점에서 보면 pgd -> pmd -> pte -> 2M와 같이 표현해도 좋다.
    • 우측 그림: ARM64 아키텍처로 보면 lvl0 -> lvl1 -> 2M이고, 매크로 코드를 공유하여 사용하므로 코드 관점에서 보면 pgd -> pte -> 2M와 같이 표현해도 좋다.

 

Identity 매핑

물리 주소와 가상 주소가 동일하도록 매핑을 할 때 다음과 같은 3가지 상황이 발생한다.

 

다음 그림은 물리 주소의 idmap 코드 영역이 동일한 주소의 유저 가상 주소 공간에 배치 가능한 경우이다. 가장 일반적인 상황이다.

 

다음 그림은 물리 주소의 idmap 코드 영역이 동일한 주소의 유저 가상 주소 공간에 배치가 불가능할 때 페이지  테이블 단계를 증가시켜 유저 가상 주소 공간을 키워 매핑을 하게한 상황이다.

 

다음 그림은 물리 주소의 idmap 코드 영역이 동일한 주소의 유저 가상 주소 공간에 배치가 불가능하고, VA_BITS=48 공간을 최대치인 52 비트로 확장시킬 수 있는 방법이다.

 

52bit 유저 공간

커널 v5.0-rc1에서 52비트 유저 공간을 지원한다. (4 Peta Bytes)

 

52bit 커널 공간

커널 v5.4-rc1에서 52비트 커널 공간을 지원한다. (4 Peta Bytes)

  • ARMv8.2-LPA 기능을 지원하는 아키텍처
  • 64K 페이지 사용
  • 이 기능이 동작하면서 52bit 유저 공간만 지원하던 것이 이제 유저 및 커널 모두 같은 52bit 커널 공간으로 사용한다.
    • 즉 유저용은 52bit, 커널용은 48비트와 같이 나눠서 설정하는 번거로움을 아예 불가능하게 제거하였다.
  • 참고: arm64: mm: Introduce 52-bit Kernel VAs (2019, v5.4-rc1)

 


커널 및 유저 공간 분리

  • 유저에서 커널 공간의 분리
    • swapper 및 trampoline 두 커널 페이지 테이블을 사용한다.
  • 커널에서 유저 공간의 분리
    • ARMv8.1의 PAN(Privileged Access Never) 기능을 사용하거나, 이러한 기능이 없는 경우 소프트웨어 에뮬레이션 방법(CONFIG_ARM64_SW_TTBR0_PAN)을 사용한다.
  • 참고: KAISER: hiding the kernel from user space | LWN.net

 

다음 그림과 같이 ARM64 시스템에서 커널 공간을 담당하는 TTBR1과 유저 공간을 담당하는 TTBR1을 사용하여 각각의 커널 모드와 유저 모드에서 상대방의 영역을 사용하지 못하게 분리하는 방법을 보여준다.

 

ASID(Address Space IDentification )

mm 스위칭 후 TLB 캐시 및 명령 캐시에 대한 높은 비용의 플러시를 억제하기 위해 ASID를 이용한 가상 주소의 중복을 허용하게 하였다. 이를 이용하여 각각의 태스크 마다 유니크하게 식별할 수 있도록 ASID를 발급하여 구분한다. 그런데 이 ASID는 ARM32의 경우 8 bit 만을 허용하고, ARM64의 경우 8 bit 또는 16 bit를 지원한다. 이 때문에 리눅스 커널에서 태스크의 식별에 사용하는 pid를 사용하지 못하고 별도로 ASID 발급 관리를 수행한다

 

SDEI(Software Delegated Exception Interface)

펌웨어(Secure)가 OS 및 하이퍼바이저로 시스템 이벤트를 전달하기 위한 메커니즘이다.

  • 인터럽트 마스킹 및 critical section에 의해 지연되면 안되는 exception을 처리한다.
  • 주 사용 케이스
    • 시스템 에러 핸들링(RAS)
    • 시스템 감시(watchdog)
    • 커널 디버깅
    • 샘플 프로파일링
    • 유저 모드에서 trampoline 페이지 테이블을 사용한 커널 감추기
  • 참고: SDEI: Software Delegated Exception Interface | Trusted Firmware-A

 


커널(어셈블리) 시작 전 준비

시작하기에 앞서 몇 가지 어셈블리 명령 및 어셈블리 지시어등을 미리 알고 시작해야 분석에 소요되는 시간을 절약할 수 있다.

  • 어셈블리 명령들
    • b, bl
    • lda, sta, ldp, stp
    • mov, adr
    • add, sub, tst, cmp
    • mrs, msr
  • 어셈블리 지시어들
    • .macro, .endm
    • .align, .globl, .local, weak, .L
    • .size, .quad, .long
    • .if, .endif

 

위의 기본 명령과 지시어를 이해한 후에는 다음 항목들에 대해서 필요할 때마다 확인한다.

  • 명령 뒤에 붙는 conditional branch 명령들
  • 주소 참조에 사용되는 addressing mode들

 

참고할 어셈블리 명령

adrp 명령

  • AArch64:
    • adrp Xd, label
  • Address of 4KB page at a PC-relative offset.
  • 현재 주소(pc)로부터 +-4G 주소까지의 label 주소를 알아와서 Xd 레지스터에 저장한다.
  • 참고: Addressing Mode (AArch64) | 문c

 

eret 명령 – Exception Layer 이동

  • AArch64:
    • eret
  • 현재 Exception Level을 읽어오는 방법은 Read Only인 CurrentEL 레지스터를 읽어오면 알 수 있다.  eret 명령을 사용하여 exception 발생 전으로 돌아갈 수 있는데 돌아갈 EL 모드 정보가 포함된 PSTATE 값을 pspr_elx 레지스터에 저장하고, 복귀할 주소가 담겨 있는 lr 레지스터의 값을 elr_elx 레지스터에 저장한 후 eret 명령을 사용하면 원하는 EL 및 주소로 이동이 가능하다. 단 상위 레이어로의 이동은 불가능하다.
  • eret 명령은 실제 exception과 pair로만 사용되는 것이 아니라, exception 없이 EL 전환과 특정 주소로 이동시킬 수 있다.
  • 참고: AArch64 Exception Levels | Mike’s

 

csel(condition select) 명령

  • AArch64:
    • csel Xd, Xn, Xm, #cond
  • 컨디션(#cond)이 true인 경우 Xn을 선택하여 Xd에 대입하고, false인 경우 Xm을 선택한 후 Xd에 대입한다.
    • Xd = #cond ? Xn : Xm

 

ubfx 명령

  • AArch64:
    • ubfx Xd, Xn, #lsb, #width
  • Xn에서 #lsb 비트 위치 부터 msb 방향으로 #width 비트 수만큼 읽어 Xd에 대입한다.
    • 예) Xn=0x1234_5678_9abc_def0, #lsb=4, #width=8
      • Xd=0xef

 

bic(BitwIse Clear)

  • AArch64:
    • bic Xd, Xn, #op2
  • Xn에서 #op2 비트들을 클리어하여 Xd에 대입한다.
  • Xd = Xn & ~op2
    • 예) Xn=0x1234_5678_9abc_deef0, #op2=0xfff
      • Xd=0x1234_5678_9abc_d000

 

bfi(Bit Field Insert) 명령

  • AArch64:
    • bfi Xd, Xn, #lsb, #width
  • Xn 레지스터 값에서 하위 #width 비트 수 만큼의 값을 Xd의 #lsb 비트 위치에 끼워넣는다.
    • 예) Xd=0x1111_2222_3333_4444, Xn=0x1234_5678_9abc_def0, #lsb=4, #width=8
      • Xd=0x1111_2222_3333_4f04

 

clz(Count Leading Zero) 명령

  • AArch64:
    • clz Xd, Xn
  • 0으로 시작하는 비트가 몇 개인지 센다.
    • 예) Xn=0x0000_00f1_1234_0000
      • Xd = 24

 

PC 상대(PC-relative) 주소 지정 매크로

현재 위치 PC 레지스터로부터 +- 4G 주소 범위 이내에 위치한 심볼 위치에 접근할 때 사용되는 매크로 3개를 알아본다.

 

adr_l 매크로

arch/arm64/include/asm/assembler.h

/*
 * Pseudo-ops for PC-relative adr/ldr/str <reg>, <symbol> where
 * <symbol> is within the range +/- 4 GB of the PC.
 */
        /*
         * @dst: destination register (64 bit wide)
         * @sym: name of the symbol
         */
.       .macro  adr_l, dst, sym
        adrp    \dst, \sym
        add     \dst, \dst, :lo12:\sym
        .endm

현재 주소에서 +-4G 이내 범위에 위치한 심볼 주소 @sym에 대한 주소를 @dst 레지스터에 알아온다.

 

ldr_l 매크로

arch/arm64/include/asm/assembler.h

.       /*
         * @dst: destination register (32 or 64 bit wide)
         * @sym: name of the symbol
         * @tmp: optional 64-bit scratch register to be used if <dst> is a
         *       32-bit wide register, in which case it cannot be used to hold
         *       the address
         */
.       .macro  ldr_l, dst, sym, tmp=
        .ifb    \tmp
        adrp    \dst, \sym
        ldr     \dst, [\dst, :lo12:\sym]
        .else
        adrp    \tmp, \sym
        ldr     \dst, [\tmp, :lo12:\sym]
        .endif
        .endm

현재 주소에서 +-4G 이내범위에 위치한 심볼 @sym 주소의 값을 32비트 또는 64비트 @dst 레지스터에 담아온다. 만일 @dst 레지스터가 32비트인 경우 @tmp에 64비트 레지스터를 지정해야 한다. @tmp 레지스터는 사용 후 파손된다.

 

str_l 매크로

arch/arm64/include/asm/assembler.h

.       /*
         * @src: source register (32 or 64 bit wide)
         * @sym: name of the symbol
         * @tmp: mandatory 64-bit scratch register to calculate the address
         *       while <src> needs to be preserved.
         */
.       .macro  str_l, src, sym, tmp
        adrp    \tmp, \sym
        str     \src, [\tmp, :lo12:\sym]
        .endm

현재 주소에서 +-4G 이내 범위에 위치한 심볼 @sym 주소에 32비트 또는 64비트 @dst 레지스터 값을 기록한다.

 

mov_q 매크로

include/asm/assembler.h

/*
 * mov_q - move an immediate constant into a 64-bit register using
 *         between 2 and 4 movz/movk instructions (depending on the
 *         magnitude and sign of the operand)
 */
.macro  mov_q, reg, val
        .if (((\val) >> 31) == 0 || ((\val) >> 31) == 0x1ffffffff)
            movz    \reg, :abs_g1_s:\val
        .else
            .if (((\val) >> 47) == 0 || ((\val) >> 47) == 0x1ffff)
                movz    \reg, :abs_g2_s:\val
            .else
                movz    \reg, :abs_g3:\val
                movk    \reg, :abs_g2_nc:\val
            .endif
            movk    \reg, :abs_g1_nc:\val
        .endif
        movk    \reg, :abs_g0_nc:\val
.endm

상수값 @val을 64비트 레지스터인 @reg에 대입한다. 한 개의 어셈블리 코드로 모든 64비트 상수를 대입시킬 수 없으므로, 상수를 16비트씩 나누어 최소 2회에서 최대 4회에 걸쳐 대입한다.

 

예) 다음 명령을 수행하는 경우 다음과 같이 4회에 걸쳐 mov 명령을 사용하도록 어셈블된다.

  • mov_q x2, 0x4000300020001
mov     x2, #0x4000000000000            // x2 <- 0x4_0003_0002_0001
movk    x2, #0x3, lsl #32
movk    x2, #0x2, lsl #16
movk    x2, #0x1

 

:abs_g1_s: & :abs_g1:

  • _s 접미사가 붙으면 절대 값 g1 영역의 signed 값을 의미한다.
  • 3 개의 g0 ~ g3 영역은 16비트씩 사용되며 다음과 같이 구분한다.
    • g0=bits[15:0]
    • g1=bits[31:16]
    • g2=bits[47..32]
    • g3=bits[63..48]
  • 참고: Assembly expressions | ARM

 

:lo12:

  • 하위 12비트 값을 의미한다.

 


커널(어셈블리) 시작

_head:

arch/arm64/kernel/head.S

/*
 * Kernel startup entry point.
 * ---------------------------
 *
 * The requirements are:
 *   MMU = off, D-cache = off, I-cache = on or off,
 *   x0 = physical address to the FDT blob.
 *
 * Note that the callee-saved registers are used for storing variables
 * that are useful before the MMU is enabled. The allocations are described
 * in the entry routines.
 */
        __HEAD
        /*
         * DO NOT MODIFY. Image header expected by Linux boot-loaders.
         */
        efi_signature_nop                       // special NOP to identity as PE/COFF executable
        b       primary_entry                   // branch to kernel start, magic
        .quad   0                               // Image load offset from start of RAM, little-endian
        le64sym _kernel_size_le                 // Effective size of kernel image, little-endian
        le64sym _kernel_flags_le                // Informative flags, little-endian
        .quad   0                               // reserved
        .quad   0                               // reserved
        .quad   0                               // reserved
        .ascii  ARM64_IMAGE_MAGIC               // Magic number
        .long   .Lpe_header_offset              // Offset to the PE header.

        __EFI_PE_HEADER

부트 로더로 부터 처음 진입되는 커널 코드 시작점이다.

  • 부트 로더로 부터 커널 코드 시작인 _head에 진입하기 전에 다음 규칙이 적용된다.
    • MMU는 off 되어 있어야 한다.
    • I-Cache는 on/off 상관 없다.
    • x0 레지스터에는 DTB 시작 물리 주소가 담겨 있어야 한다.
    • x0~x3까지의 레지스터를 커널(callee)이 보존해야 한다.
    • 커널 진입 전 부트 로더 등에서 처리하는 일에 대해 다음을 참고한다.
  • 코드 라인 1에서 이후의 코드가 a(allocation) 및 x(execution) 속성을 가진 섹션 “.head.text”에 위치하도록 컴파일러에 지시한다.
  • 코드 라인 5에서 ccmp(conditional compare) 명령이 처음 시작되지만 이 명령의 결과는 전혀 의미가 없는 nop 처럼 사용된다. 이를 통해 실제 목표는 UEFI 지원 커널인지 여부를 알아내는 식별자(처음 2바이트가 “MZ” 아스키 코드)로 사용된다.
  • 코드 라인 6에서 실제 코드가 있는 primary_entry 레이블로 이동한다.
  • 코드 라인 7~16에서 커널 이미지 정보이다.

 

__HEAD

include/linux/init.h

#define __HEAD          .section        ".head.text","ax"

이후의 코드가 a(allocation) 및 x(execution) 속성을 가진 섹션 “.head.text”에 위치하도록 컴파일러에 지시한다.

 

efi_signature_nop 매크로

        .macro  efi_signature_nop
#ifdef CONFIG_EFI
.L_head:
        /*
         * This ccmp instruction has no meaningful effect except that
         * its opcode forms the magic "MZ" signature required by UEFI.
         */
        ccmp    x18, #0, #0xd, pl
#else
        /*
         * Bootloaders may inspect the opcode at the start of the kernel
         * image to decide if the kernel is capable of booting via UEFI.
         * So put an ordinary NOP here, not the "MZ.." pseudo-nop above.
         */
        nop
#endif
        .endm

ccmp(conditional compare) 명령이 처음 시작되지만 이 명령의 결과는 전혀 의미가 없는 nop 처럼 사용된다. 이를 통해 실제 목표는 UEFI 지원 커널인지 여부(DOS 헤더 포함)를 알아내는 식별자(처음 2바이트가 “MZ” 아스키 코드)로 사용된다.

  • 코드 라인 2~8에서 CONFIG_EFI 커널 옵션을 사용하여 빌드하면 UEFI BIOS를 지원하게 되는데, 커널 이미지의 시작 위치 2바이트에 DOS 헤더를 알리는 “MZ” 아스키 코드가 있어야 한다. 따라서 MZ 문자가 들어가면서 특별히 시스템에 영향을 주지 않는 명령을 골라 사용한 결과로 ccmp를 사용하였다.
    • arm 및 arm64 모두 아키텍처가 빅 엔디안 모드와 리틀 엔디안 모드가 지원되는데, 위의 ccmp 명령은 0xfa40_5a4d이며 이를 디폴트 설정인 리틀 엔디안으로 빌드한 이미지에서 최초 4바이트는 다음과 같이 거꾸로 표현한다.
      • 4d 5a 40 fa (4d 5a가 각각 ‘M’과 ‘Z’ 아스키 문자)
  • 코드 라인 9~16에서 CONFIG_EFI 커널 옵션을 사용하지 않는 경우 처음 4바이트 명령에 아무 결과도 수행하지 않고 명령 사이클만 소모시키는 nop 명령으로 시작한다.

 

커널 이미지 헤더

압축 해제 상태의 커널 이미지는 다음과 같이 64바이트의 커널 이미지 헤더이고, DOS 헤더 형태와 호환되도록 최초 2 바이트를 “MZ” 아스키 문자열을 사용하여 만들어졌고, 리틀 엔디안 포맷으로 구성되어 있다.

  u32 code0;                    /* Executable code */
  u32 code1;                    /* Executable code */
  u64 text_offset;              /* Image load offset, little endian */
  u64 image_size;               /* Effective Image size, little endian */
  u64 flags;                    /* kernel flags, little endian */
  u64 res2      = 0;            /* reserved */
  u64 res3      = 0;            /* reserved */
  u64 res4      = 0;            /* reserved */
  u32 magic     = 0x644d5241;   /* Magic number, little endian, "ARM\x64" */
  u32 res5;                     /* reserved (used for PE COFF offset) */
  • code0/code1
    • stext로의 jump 코드가 있다.
      • 예) ccmp x18, #0, #0xd, pl 또는 add x13, x18, #0x16
      •        b primary_entry
    • 시스템에 UEFI 펌웨어가 있는 경우 이 코드는 skip 하며, UEFI의 PE 헤더에 포함된 entry 포인터(efi_stub_entry)로 jump 한다. 그 후 다시 code0 위치로 jump 한다.
  • text_offset
  • flags
    • bit0: 커널의 엔디안 (1=BE, 0=LE)
    • bit1~2: 커널 페이지 사이즈 (0=Unspecified, 1=4K, 2=16K, 3=64K)
    • bit3: 2M 정렬된 커널 이미지의 커널 물리 위치(Kernel Physical Placement) (0=DRAM의 시작 위치로 부터 근접, 1=DRAM의 모든 영역)
  • image_size
    • 이미지 사이즈 (v3.17 이전에는 0 값이 기록되어 있다.)
  • magic
    • AMR64 이미지임을 나타내는 식별 문자열로 “ARMd“이다.

 

다음과 같이 커널(vmlinux)을 덤프해본다. ELF 헤더 + DOS 헤더 + UEFI PE 헤더등으로 시작한다.

  • ELF 헤더 (64 바이트)
    • ELF
      • ELF(Excutable and Linkable Format)
      • 커널 이미지의 첫 부분에는 ELF 헤더가 있고, 이를 식별할 수 있도록 “ELF” 아스크코드 문자열을 볼 수 있다.
  • DOS 헤더 (64 바이트)
    • MZ
      • vmlinux 파일은 위의 ELF를 포함하고 0x10000 offset을 가지므로, UEFI를 지원하는 커널인 경우 아래와 같이 0x10000 주소만큼 떨어진 위치에서 “MZ” 아스키코드 문자열을 볼 수 있다.
      • MZ 문자열로 시작하는데 DOS 호환을 위해 사용되었다.
    • ARMd
      • ARM64 커널 이미지라는 것을 알 수 있도록 0x10038 주소에서 “ARMd” 아스키 코드 문자열을 볼 수 있다.
  • EFI PE 헤더
    • PE
      • EFI(Extensible Firmware Interface) PE(Portable Excutable)
      • UEFI 헤더를 식별할 수 있도록 “PE” 아스키 코드 문자열을 볼 수 있다.
$ hexdump -C vmlinux
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 b7 00 01 00 00 00  00 00 00 10 00 80 ff ff  |................|
00000020  40 00 00 00 00 00 00 00  50 cb cf 01 00 00 00 00  |@.......P.......|
00000030  00 00 00 00 40 00 38 00  03 00 40 00 1c 00 1b 00  |....@.8...@.....|
...
*
00010000  4d 5a 00 91 ff bf 51 14  00 00 00 00 00 00 00 00  |MZ....Q.........|
00010010  00 00 d8 01 00 00 00 00  0a 00 00 00 00 00 00 00  |................|
00010020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010030  00 00 00 00 00 00 00 00  41 52 4d 64 40 00 00 00  |........ARMd@...|
00010040  50 45 00 00 64 aa 02 00  00 00 00 00 00 00 00 00  |PE..d...........|
00010050  00 00 00 00 a0 00 06 02  0b 02 02 14 00 00 55 01  |..............U.|
00010060  00 00 86 00 00 00 00 00  fc 5e 51 01 00 00 01 00  |.........^Q.....|
00010070  00 00 00 00 00 00 00 00  00 00 01 00 00 02 00 00  |................|

 

ELF 파일이 아닌 Image 파일을 덤프해본다. ELF 헤더가 제외되고, DOS 헤더 + UEFI 헤더등으로 시작하는 것을 알 수 있다.

$ hexdump -C arch/arm64/boot/Image
00000000  4d 5a 00 91 ff bf 51 14  00 00 00 00 00 00 00 00  |MZ....Q.........|
00000010  00 00 d8 01 00 00 00 00  0a 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  41 52 4d 64 40 00 00 00  |........ARMd@...|
00000040  50 45 00 00 64 aa 02 00  00 00 00 00 00 00 00 00  |PE..d...........|
00000050  00 00 00 00 a0 00 06 02  0b 02 02 14 00 00 55 01  |..............U.|
00000060  00 00 86 00 00 00 00 00  fc 5e 51 01 00 00 01 00  |.........^Q.....|
00000070  00 00 00 00 00 00 00 00  00 00 01 00 00 02 00 00  |................|

 

ELF Header

 

DOS Header

실제 DOS Header 규격은 다음과 같다.

typedef struct _IMAGE_DOS_HEADER
{
                        // Cumulative size:
     WORD e_magic;      // 2
     WORD e_cblp;       // 4
     WORD e_cp;         // 6
     WORD e_crlc;       // 8
     WORD e_cparhdr;    // 10
     WORD e_minalloc;   // 12
     WORD e_maxalloc;   // 14
     WORD e_ss;         // 16
     WORD e_sp;         // 18
     WORD e_csum;       // 20
     WORD e_ip;         // 22
     WORD e_cs;         // 24
     WORD e_lfarlc;     // 26
     WORD e_ovno;       // 28
     WORD e_res[4];     // 36
     WORD e_oemid;      // 38
     WORD e_oeminfo;    // 40
     WORD e_res2[10];   // 60
     LONG e_lfanew;     // 64
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

 

UEFI(Unified Extensible Firmware Interface)

  • ARM64 시스템에서도 UEFI 펌웨어가 내장된 서버들이 있다. 이러한 커널에서는 CONFIG_EFI가 반드시 필요하다.
  • UEFI 펌웨어는 디바이스 정보를 자동으로 인식하거나 수동 설정된 내용을 ACPI 테이블로 변환하여 부트로더 및 커널에 전달한다. 부트 로더 및 커널은 이 정보를 가지고 시스템을 초기화한다. 이렇게 UEFI가 전달하는 ACPI 테이블이 없는 임베디드 시스템들은 Device Tree 스크립트를 작성하여 컴파일한 FDT/DTB(Flattened Device Tree / Device Tree Blob) 스타일로 디바이스 정보를 전달한다. 최근엔 주요 정보는 ACPI로 전달하고, FDT/DTB로 추가 전달하는 경우도 있다.

 


primary_entry:

arch/arm64/kernel/head.S

        __INIT

        /*
         * The following callee saved general purpose registers are used on the
         * primary lowlevel boot path:
         *
         *  Register   Scope                      Purpose
         *  x20        primary_entry() .. __primary_switch()    CPU boot mode
         *  x21        primary_entry() .. start_kernel()        FDT pointer passed at boot in x0
         *  x22        create_idmap() .. start_kernel()         ID map VA of the DT blob
         *  x23        primary_entry() .. start_kernel()        physical misalignment/KASLR offset
         *  x24        __primary_switch()                       linear map KASLR seed
         *  x25        primary_entry() .. start_kernel()        supported VA size
         *  x28        create_idmap()                           callee preserved temp register
         */
SYM_CODE_START(primary_entry)
        bl      preserve_boot_args
        bl      init_kernel_el                  // w0=cpu_boot_mode
        mov     x20, x0
        bl      create_idmap

        /*
         * The following calls CPU setup code, see arch/arm64/mm/proc.S for
         * details.
         * On return, the CPU will be ready for the MMU to be turned on and
         * the TCR will have been set.
         */
#if VA_BITS > 48
        mrs_s   x0, SYS_ID_AA64MMFR2_EL1
        tst     x0, #0xf << ID_AA64MMFR2_LVA_SHIFT
        mov     x0, #VA_BITS
        mov     x25, #VA_BITS_MIN
        csel    x25, x25, x0, eq
        mov     x0, x25
#endif
        bl      __cpu_setup                     // initialise processor
        b       __primary_switch
SYM_CODE_END(primary_entry)

커널 코드가 처음 시작되는 .init.text 섹션이다. 어셈블리 코드를 통해 임시 페이지 매핑을 수행한 후 boot cpu의 MMU 장치를 켜서 가상 주소 환경이 동작되도록 페이징 활성화한다. 그 후 C 함수로 작성된 커널의 시작 위치인 start_kernel() 함수로 진입한다. 참고로 boot cpu가 아닌 나머지 cpu 코어들은 secondary core라고 불리고 이들은 아직 시작하지 않는 상태로 있다. 시스템마다 조금씩 다르지만 특정 주소에서 스핀(spin)하며 대기 중에 있거나 또는 전원이 꺼져있는 상태로 시작한다.

  • 코드 라인 1에서기존에 stext 라는 레이블을 사용했었는데 primary_entry 레이블로 변경되었다. 헤더들을 빼면 진정한 커널 시작점이라고 할 수 있다.
  • 코드 라인 2에서 부트로더가 전달해준 x0 ~ x3 레지스터들을 boot_args 위치에 보관해둔다.
    • setup_arch() 마지막 부분에서 저장된 boot_args[] 값들 중 x1~x3에 해당하는 값이 0이 아닌 값이 있는 경우 다음과 같은 경고 메시지를 출력한다.
      • WARNING: x1-x3 nonzero in violation of boot protocol: …
  • 코드 라인 3에서 리눅스 커널이 el2로 부팅한 경우 하이퍼 바이저에 관련된 설정들을 수행한다.
  • 코드 라인 4~5에서 DTB 물리 주소를 x20에 옮긴 후, 커널에 대해 임시로 사용할 init_idmap 및 init 페이지 테이블을 생성한다.
  • 코드 라인 13~20에서 52비트 가상 주소를 지원하는 커널인 경우 코드를 통해 mmfr2_el1 레지스터에서 LVA(Large Virtual Address) 기능이 지원되는 것을 확인해본다. 만일 확인된 경우 유저 가상 주소를 표현하는 비트 수를 52로하여 x0에 대입한다. 그렇지 않은 경우 VA_BITS_MIN을 사용한다.
    • 즉 52비트를 지원하는 커널에서 cpu가 52비트를 지원하는지 여부를 알아보고 다음 둘 중 하나를 선택한다.
      • 52비트 VA를 지원하는 아키텍처인 경우 VA_BITS(52)를 선택한다.
      • 52비트 VA를 지원하지 아키텍처인 경우 VA_BITS_MIN(48)을 선택한다.
  • 코드 라인 21에서 프로세서를 초기화한다.
  • 코드 라인 22에서 MMU를 활성화시킨 후 start_kernel() 함수로 점프한다.

 

__INIT

include/linux/init.h

#define __INIT          .section        ".init.text","ax"

이후의 코드가 a(allocation) 및 x(execution) 속성을 가진 섹션 “.init.text”에 위치하도록 컴파일러에 지시한다.

  • .init 섹션에 위치한 코드 및 데이터는 커널이 부팅한 후 더이상 필요 없으므로 추후 커널의 물리 메모리를 관리하는 버디 시스템으로 되돌려 활용하는 것으로 메모리를 절약한다.

 

VA_BITS vs VA_BITS_MIN

64비트 주소 버스를 사용하는 ARMv8 아키텍처의 경우 MMU를 사용하여 가상 주소를 사용할 수 있는데 그 크기는 ARMv8 아키텍처가 지원하는 몇 가지 크기가 제공된다. 그 크기의 조절은 ARMv8 아키텍처가 지원하는 PAGE_SIZE(4K, 16K, 64K)와 페이지 테이블 레벨(2, 3, 4)의 조합으로 VA_BITS=36, 39, 42, 47, 48, 52 중 하나를 선택하여 사용할 수 있다.

 

최초 ARMv8 아키텍처의 경우 64비트 주소 중 최대 48비트만을 가상 주소 영역에 사용할 수 있었는데 그 경우 최대 지원 가능한 가상 주소 영역의 크기는 2^48 = 256TB 크기 였다.

 

그 후, 더 큰 가상 주소 영역을 지원하기 위해 52비트의 LVA(Large Virtual Address) 기능을 탑재한 새 ARMv8.2 아키텍처가 만들어졌다. 이러한 경우 VA_BITS=52로 커널 이미지를 빌드하여 만들 수 있는데, 이렇게 빌드되어 만들어진 52비트 커널 이미지는 최대 가상 주소 영역에 48비트만을 사용하는 구형 아키텍처에서도 동작시킬 수도 있고, 52비트를 지원하는 최신 아키텍처에 둘 다에서 동작시킬 수 있다. 이러한 이유로 52비트에서 동작하는 커널을 만드는 경우 VA_BITS=52로 하되 VA_BITS_MIN=48로 값을 다르게하여 빌드 타임이 아닌 커널이 부트되는 런타임에 구형 아키텍처 및 신형 아키텍처를 판별하여 둘 중 하나를 선택하도록 코드를 구성하였다.

 

결국 VA_BITS 및 VA_BITS_MIN을 살펴보면 다음과 같이 VA_BITS=48비트까지는 VA_BITS_MIN 값도 동일하다. 그렇지만 VA_BITS=52비트인 경우 VA_BITS_MIN=48과 같이 두 값이 달라지는 것을 알 수 있다.

  • VA_BITS=36, VA_BITS_MIN=36
  • VA_BITS=39, VA_BITS_MIN=39
  • VA_BITS=42, VA_BITS_MIN=42
  • VA_BITS=47, VA_BITS_MIN=47
  • VA_BITS=48, VA_BITS_MIN=48
  • VA_BITS=52, VA_BITS_MIN=48

 


부트 시 전달된 인자(x0~x3) 저장

preserve_boot_args()

arch/arm64/kernel/head.S

/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
SYM_CODE_START_LOCAL(preserve_boot_args)
        mov     x21, x0                         // x21=FDT

        adr_l   x0, boot_args                   // record the contents of
        stp     x21, x1, [x0]                   // x0 .. x3 at kernel entry
        stp     x2, x3, [x0, #16]

        dmb     sy                              // needed before dc ivac with
                                                // MMU off

        add     x1, x0, #0x20                   // 4 x 8 bytes
        b       dcache_inval_poc                // tail call
SYM_CODE_END(preserve_boot_args)

부트로더가 전달해준 x0 ~ x3 레지스터들을 boot_args 위치에 보관해둔다. x0 레지스터는 DTB 주소로 사용되고, 나머지는 추후 사용하기 위해 예약되었다.

  • 코드 라인 2~6에서 부트로더가 전달해준 x0 ~ x3 레지스터들을 boot_args 위치에 보관해둔다.
  • 코드 라인 8에서 데이터 캐시를 invalidate하여 제거하기 전에 위의 stp 명령이 내부 버퍼에서 완전하게 마무리되도록 메모리 베리어를 사용한다.
    • MMU가 꺼진 상태는 캐시를 사용하지 않지만 버퍼는 사용가능한 상태이다.
      • 참고로 MMU가 꺼져 있어도 버퍼를 통해 predictive 로딩은 가능한 상태이다.
    • MMU가 꺼진 상태에서 캐시에 쓰레기 값이 존재할 수 있다. 따라서 해당 주소의 캐시는 비워두는 것이 추후 안전하다.
      • 쓰레기 캐시 값이 남아 있게되면 나중에 MMU가 켜진 후 캐시 라인을 clean 하거나 또는 자동으로 evict되는 경우 캐시에 있었던 쓰레기 값을 메모리에 덮어 기록하면서 의도하지 않는 일이 발생할 수 있다.
  • 코드 라인 11~12에서 위에서 설명한 이유로 추후 잘못된 값이 읽히지 않도록 PoC 레벨까지 모든 cpu의 invalidate D-cache를 수행한다.

 

다음 두 그림을 통해 mmu-off 상태에서 메모리를 기록한 후 dmb와 dc ivac 명령을 사용하여 잠재적인 캐시 코히런스 문제를 제거한다.

 

 


하이퍼 바이저 지원 코드 설정

init_kernel_el()

arch/arm64/kernel/head.S

/*
 * end early head section, begin head code that is also used for
 * hotplug and needs to have the same protections as the text region
 */
        .section ".idmap.text","awx"

/*
 * Starting from EL2 or EL1, configure the CPU to execute at the highest
 * reachable EL supported by the kernel in a chosen default state. If dropping
 * from EL2 to EL1, configure EL2 before configuring EL1.
 *
 * Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
 * SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
 *
 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x0 if
 * booted in EL1 or EL2 respectively, with the top 32 bits containing
 * potential context flags. These flags are *not* stored in __boot_cpu_mode.
 */
SYM_FUNC_START(init_kernel_el)
        mrs     x0, CurrentEL
        cmp     x0, #CurrentEL_EL2
        b.eq    init_el2

el1 또는 el2 cpu가 현재 어떤 el 레벨로 커널에 진입했는지 알아오기 위해 CurrentEL 레지스터를 읽어온다. 그 후 레벨에 따라 init_el1 과 init_el2로 분기한다.

  • 코드 라인 2~3에서 CurrentEL 값을 읽어 el2 레벨인지 비교한다.
    • 커널 진입 시점의 boot cpu에서 CurrentEL 레지스터를 읽은 값은 CurrentEL_EL1(4) 또는 CurrentEL_EL2(8)이다.
  • 코드 라인 4에서 만일 el2인 경우 init_el2로 점프하고, 그렇지 않은 경우 el1이므로 다음 행에 있는 init_el1: 레이블을 진행한다.

 

init_el1:

arch/arm64/kernel/head.S

SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
        mov_q   x0, INIT_SCTLR_EL1_MMU_OFF
        msr     sctlr_el1, x0
        isb
        mov_q   x0, INIT_PSTATE_EL1
        msr     spsr_el1, x0
        msr     elr_el1, lr
        mov     w0, #BOOT_CPU_MODE_EL1
        eret
  • 코드 라인 2~3에서 MMU가 꺼진 상태 값으로 시스템 컨트롤 레지스터인 sctlr_el1 을 설정한다.
  • 코드 라인 4에서 시스템 컨트롤 레지스터의 내용을 변경한 경우에는 isb 명령을 사용하여 파이프라인을 모두 비워야 한다.
  • 코드 라인 5~6에서 EL1 전환을 위해 EL1 pstate 초깃값을 spsr_el1 레지스터에 지정한다.
    • INTI_PSTATE_EL1 값에는 DAIF 플래그를 마스크하고 EL1 모드로 변경되도록 값이 지정되어 있다.
  • 코드 라인 7에서 복귀할 주소를 지정하기 위해 lr 레지스터에 담긴 복귀 주소를 elr_el1 레지스터에 저장한다.
  • 코드 라인 8에서 el1으로 복귀하는 것을 호출한 함수에서 알 수 있도록 w0 레지스터에 담는다. (반환 값은 el1을 의미하라고 0xe11)
  • 코드 라인 9에서 exception return 명령을 통해 복귀한다.

 

INIT_PSTATE_EL1 & INIT_PSTATE_EL2 값
#define INIT_PSTATE_EL1 \
        (PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL1h)
#define INIT_PSTATE_EL2 \
        (PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL2h)

EL1 및 EL2의 PSTATE 초깃값으로 DAIF 플래그를 모두 마스크한 상태와 해당 EL 모드를 포함한다.

 

init_el2:

arch/arm64/kernel/head.S

SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
        mov_q   x0, HCR_HOST_NVHE_FLAGS
        msr     hcr_el2, x0
        isb

        init_el2_state

        /* Hypervisor stub */
        adr_l   x0, __hyp_stub_vectors
        msr     vbar_el2, x0
        isb

        mov_q   x1, INIT_SCTLR_EL1_MMU_OFF

        /*
         * Fruity CPUs seem to have HCR_EL2.E2H set to RES1,
         * making it impossible to start in nVHE mode. Is that
         * compliant with the architecture? Absolutely not!
         */
        mrs     x0, hcr_el2
        and     x0, x0, #HCR_E2H
        cbz     x0, 1f

        /* Set a sane SCTLR_EL1, the VHE way */
        msr_s   SYS_SCTLR_EL12, x1
        mov     x2, #BOOT_CPU_FLAG_E2H
        b       2f

1:
        msr     sctlr_el1, x1
        mov     x2, xzr
2:
        msr     elr_el2, lr
        mov     w0, #BOOT_CPU_MODE_EL2
        orr     x0, x0, x2
        eret
SYM_FUNC_END(init_kernel_el)

el2 에서 동작할 코드 및 exception stub 벡터들을 준비하고 el2 레지스터들을 초기화한다.

  • 코드 라인 2~4에서 하이퍼바이저 설정 레지스터를 nVHE 모드로 설정한다.
  • 코드 라인 6에서 nVHE를 위해 각종 EL2 레지스터들을 초기화한다.
  • 코드 라인 9~11에서 nVHE를 위해 호스트 커널은 EL1으로 전환하여 동작하여야 한다. 따라서 EL2에 하이퍼 바이저 용도로만 사용할 stub 벡터가 필요하므로 이를 설치한다.
  • 코드 라인 13에서 잠시 x1 레지스터에 EL1용 시스템 콘트롤레지스터에 저장할 초기값(mmu disable 포함)을 준비한다.
  • 코드 라인 20~22에서 하이퍼바이저 설정 레지스터를 읽어와서 E2H 필드 값을 읽어와서 0인 경우 VHE를 지원하지 않으므로 1: 레이블로 이동한다.
  • 코드 라인 25~27에서 아키텍처가 VHE 기능을 지원함을 확인했다. 조금 전에 x1 레지스터에 저장해두었던 값을 EL1용 시스템 컨트롤 레지스터에 저장하여 MMU를 확실하게 disable 한다. 그리고 VHE 기능을 활성화 하도록 E2H 비트를 x2레지스터에 담아 2: 레이블로 이동한다.
    • VHE 모드에서 _el12 접미사를 사용하는 레지스터는 실제 el1 레지스터를 의미한다.
  • 코드 라인 29~31에서 1: 레이블이다. 조금 전에 x1 레지스터에 저장해두었던 값을 EL1용 시스템 콘트롤레지스터에 저장하여 MMU를 확실하게 disable 한다.
  • 코드 라인 32~36에서 2: 레이블이다. el2 부팅되었음을 알리는 값을 x0 레지스터를 통해 반환한다. (반환값: el2를 의미하라고 0xe12)
  • 코드 라인 37에서 현재 EL2 모드에 있고, eret 명령을 사용하여 EL1 또는 EL2(같은 모드로도 가능) 모드로 전환하면서 elr_el2 레지스터에 저장한 복귀할 주소로 점프한다.
    • primary_entry에서 bl init_kernel_el 명령을 사용했었었다. 이렇게 bl 명령을 사용하면 돌아올 위치를 lr 레지스터에 담아두었다는 것을 기억해야 한다.

 

init_el2_state 매크로

arch/arm64/include/asm/el2_setup.h

/**
 * Initialize EL2 registers to sane values. This should be called early on all
 * cores that were booted in EL2. Note that everything gets initialised as
 * if VHE was not evailable. The kernel context will be upgraded to VHE
 * if possible later on in the boot process
 *
 * Regs: x0, x1 and x2 are clobbered.
 */
.macro init_el2_state
        __init_el2_sctlr
        __init_el2_timers
        __init_el2_debug
        __init_el2_lor
        __init_el2_stage2
        __init_el2_gicv3
        __init_el2_hstr
        __init_el2_nvhe_idregs
        __init_el2_nvhe_cptr
        __init_el2_nvhe_sve
        __init_el2_fgt
        __init_el2_nvhe_prepare_eret
.endm

EL2로 진입한 경우 먼저 VHE 지원 여부와 관계없이 먼저 각각의 EL2 레지스터들을 초기화한다.

  • 이렇게 초기화하면 nVHE에서 유효한 설정이 되는 것이고, 그 후 VHE를 지원하는 시스템에서는 나중에 추가 작업을 수행한다.

 

__init_el2_sctlr 매크로 – MMU disable

arch/arm64/include/asm/el2_setup.h

.macro __init_el2_sctlr
        mov_q   x0, INIT_SCTLR_EL2_MMU_OFF
        msr     sctlr_el2, x0
        isb
.endm

하이퍼 바이저용 시스템 콘트롤 레지스터에 MMU off 상태의 디폴트 설정으로 기록한다.

 

__init_el2_timers 매크로 – EL1 물리 타이머 enable

arch/arm64/include/asm/el2_setup.h

/*
 * Allow Non-secure EL1 and EL0 to access physical timer and counter.
 * This is not necessary for VHE, since the host kernel runs in EL2,
 * and EL0 accesses are configured in the later stage of boot process.
 * Note that when HCR_EL2.E2H == 1, CNTHCTL_EL2 has the same bit layout
 * as CNTKCTL_EL1, and CNTKCTL_EL1 accessing instructions are redefined
 * to access CNTHCTL_EL2. This allows the kernel designed to run at EL1
 * to transparently mess with the EL0 bits via CNTKCTL_EL1 access in
 * EL2.
 */
.macro __init_el2_timers
        mov     x0, #3                          // Enable EL1 physical timers
        msr     cnthctl_el2, x0
        msr     cntvoff_el2, xzr                // Clear virtual offset
.endm

el1 물리 타이머를 enable 하고, virtual offset을 클리어한다.

  • 코드 라인 2~3에서 하이퍼 바이저 카운터-타이머 컨트롤(cnthctl_el2) 레지스터의 EL1 물리 카운터와 (el1pcen)와 EL1 물리 타이머(el1pcten)에 해당하는 비트들을 설정하여 el0 및 el1에서 이들을 사용가능하도록 한다.
  • 코드 라인 4에서 virtual offset 레지스터를 0으로 클리어한다.

 

__init_el2_debug 매크로 – hw debuger enable

arch/arm64/include/asm/el2_setup.h

.macro __init_el2_debug
        mrs     x1, id_aa64dfr0_el1
        sbfx    x0, x1, #ID_AA64DFR0_PMUVER_SHIFT, #4
        cmp     x0, #1
        b.lt    .Lskip_pmu_\@                   // Skip if no PMU present
        mrs     x0, pmcr_el0                    // Disable debug access traps
        ubfx    x0, x0, #11, #5                 // to EL2 and allow access to
.Lskip_pmu_\@:
        csel    x2, xzr, x0, lt                 // all PMU counters from EL1

        /* Statistical profiling */
        ubfx    x0, x1, #ID_AA64DFR0_PMSVER_SHIFT, #4
        cbz     x0, .Lskip_spe_\@               // Skip if SPE not present

        mrs_s   x0, SYS_PMBIDR_EL1              // If SPE available at EL2,
        and     x0, x0, #(1 << SYS_PMBIDR_EL1_P_SHIFT)
        cbnz    x0, .Lskip_spe_el2_\@           // then permit sampling of physical
        mov     x0, #(1 << SYS_PMSCR_EL2_PCT_SHIFT | \
                      1 << SYS_PMSCR_EL2_PA_SHIFT)
        msr_s   SYS_PMSCR_EL2, x0               // addresses and physical counter
.Lskip_spe_el2_\@:
        mov     x0, #(MDCR_EL2_E2PB_MASK << MDCR_EL2_E2PB_SHIFT)
        orr     x2, x2, x0                      // If we don't have VHE, then
                                                // use EL1&0 translation.

PMU(Process Monitoring Unit)이 내장된 아키텍처에서 디버거를 사용할 수 있게, el2 트랩하지 않도록 한다. (자세한 설명은 skip)

 

__init_el2_lor 매크로 – LORegion 기능 disable

arch/arm64/include/asm/el2_setup.h

/* LORegions */
.macro __init_el2_lor
        mrs     x1, id_aa64mmfr1_el1
        ubfx    x0, x1, #ID_AA64MMFR1_LOR_SHIFT, 4
        cbz     x0, .Lskip_lor_\@
        msr_s   SYS_LORC_EL1, xzr
.Lskip_lor_\@:
.endm

LORegion 기능을 disable 한다.

  • 코드 라인 3~4에서 id_aa64mmfr1_el1 레지스터의 LOR 필드를 읽어온다.
  • 코드 라인 5~6에서 LORegions 기능이 있는 경우 LORC_EL1 레지스터를 클리어하여 LORegion 기능을 disable 한다.

 

__init_el2_stage2 매크로 – vttbr_el2 클리어

arch/arm64/include/asm/el2_setup.h

/* Stage-2 translation */
.macro __init_el2_stage2
        msr     vttbr_el2, xzr
.endm

vttbr_el2 레지스터를 클리어한다.

 

__init_el2_gicv3 매크로 – GICv3 초기화

arch/arm64/include/asm/el2_setup.h

/* GICv3 system register access */
.macro __init_el2_gicv3
        mrs     x0, id_aa64pfr0_el1
        ubfx    x0, x0, #ID_AA64PFR0_GIC_SHIFT, #4
        cbz     x0, .Lskip_gicv3_\@

        mrs_s   x0, SYS_ICC_SRE_EL2
        orr     x0, x0, #ICC_SRE_EL2_SRE        // Set ICC_SRE_EL2.SRE==1
        orr     x0, x0, #ICC_SRE_EL2_ENABLE     // Set ICC_SRE_EL2.Enable==1
        msr_s   SYS_ICC_SRE_EL2, x0
        isb                                     // Make sure SRE is now set
        mrs_s   x0, SYS_ICC_SRE_EL2             // Read SRE back,
        tbz     x0, #0, 1f                      // and check that it sticks
        msr_s   SYS_ICH_HCR_EL2, xzr            // Reset ICC_HCR_EL2 to defaults
.Lskip_gicv3_\@:
.endm

GICv3를 초기화한다.

  • 코드 라인 3~5에서 id_aa64pfr0_el1 레지스터에서 gic 필드를 읽어 gicv3가 구현되지 않은 경우 GICv3 설정을 skip 한다.
  • 코드 라인 7~11에서 icc_sre_el2.sre를 1로 설정하여시스템 레지스터를 enable하고,  icc_sre_el2.enable을 1로 설정하여 non-secure el1에서 icc_sre_el1을 사용하도록 설정한다.
  • 코드 라인 12~14에서 icc_sre_el2 레지스터를 다시 읽고, 63번 비트가 1로 설정된 경우 문제가 있다고 판단하여 sys_ich_hcr_el2 레지스터를 클리어하여 리셋시킨다.

 

__init_el2_hstr 매크로 – CP15 enable

arch/arm64/include/asm/el2_setup.h

.macro __init_el2_hstr
        msr     hstr_el2, xzr                   // Disable CP15 traps to EL2
.endm

hstr_el2 레지스터를 클리어하여 cp15 레지스터를 읽을 수 있도록 허용한다.

 

__init_el2_nvhe_idregs 매크로 – Virtual CPU ID 등록

arch/arm64/include/asm/el2_setup.h

/* Virtual CPU ID registers */
.macro __init_el2_nvhe_idregs
        mrs     x0, midr_el1
        mrs     x1, mpidr_el1
        msr     vpidr_el2, x0
        msr     vmpidr_el2, x1
.endm

midr_el1 레지스터의 설정값을 읽어 vpidr_el2 레지스터에 그대로 적용하고, mpidr_el1 레지스터도 vmpidr_el2 레지스터에 적용한다.

 

__init_el2_nvhe_cptr 매크로 – Enable FP & SVE trap

arch/arm64/include/asm/el2_setup.h

/* Coprocessor traps */
.macro __init_el2_nvhe_cptr
        mov     x0, #0x33ff
        msr     cptr_el2, x0                    // Disable copro. traps to EL2
.endm

Floating Point 장치나 SVE 명령을 사용시 일단 trap 되도록 설정한다.

 

__init_el2_nvhe_sve 매크로 – SVE 명령 사용 허가

arch/arm64/include/asm/el2_setup.h

/* SVE register access */
.macro __init_el2_nvhe_sve
        mrs     x1, id_aa64pfr0_el1
        ubfx    x1, x1, #ID_AA64PFR0_SVE_SHIFT, #4
        cbz     x1, .Lskip_sve_\@

        bic     x0, x0, #CPTR_EL2_TZ            // Also disable SVE traps
        msr     cptr_el2, x0                    // Disable copro. traps to EL2
        isb
        mov     x1, #ZCR_ELx_LEN_MASK           // SVE: Enable full vector
        msr_s   SYS_ZCR_EL2, x1                 // length for EL1.
.Lskip_sve_\@:
.endm

아키텍처에 SVE 기능이 내장된 경우 SVE 명령 사용 허가

  • 코드 라인 3~11에서 id_aa64pfr0_el1 레지스터에서 sve 기능이 있는 경우 cptr_el2.tz을 0으로 클리어하여 SVE 명령을 사용하여도 trap을 발생시키지 않도록 한다. 그런 후 SVE 컨트롤 레지스터 zcr_el2.len의 4비트를 모두 1로 채워 가장 큰 벡터 길이로 활성화한다.
    • 벡터 길이 = (len + 1) * 128bits
    • RAZ/WI에 해당하는 비트들은 어떠한 값을 기록해도 무시되고 0으로 읽힌다.

 

__init_el2_fgt 매크로

arch/arm64/include/asm/el2_setup.h

/* Disable any fine grained traps */
.macro __init_el2_fgt
        mrs     x1, id_aa64mmfr0_el1
        ubfx    x1, x1, #ID_AA64MMFR0_FGT_SHIFT, #4
        cbz     x1, .Lskip_fgt_\@

        mov     x0, xzr
        mrs     x1, id_aa64dfr0_el1
        ubfx    x1, x1, #ID_AA64DFR0_PMSVER_SHIFT, #4
        cmp     x1, #3
        b.lt    .Lset_fgt_\@
        /* Disable PMSNEVFR_EL1 read and write traps */
        orr     x0, x0, #(1 << 62)

.Lset_fgt_\@:
        msr_s   SYS_HDFGRTR_EL2, x0
        msr_s   SYS_HDFGWTR_EL2, x0
        msr_s   SYS_HFGRTR_EL2, xzr
        msr_s   SYS_HFGWTR_EL2, xzr
        msr_s   SYS_HFGITR_EL2, xzr

        mrs     x1, id_aa64pfr0_el1             // AMU traps UNDEF without AMU
        ubfx    x1, x1, #ID_AA64PFR0_AMU_SHIFT, #4
        cbz     x1, .Lskip_fgt_\@

        msr_s   SYS_HAFGRTR_EL2, xzr
.Lskip_fgt_\@:
.endm

statistical profiling 설정 관련 설명은 생략한다.

 

__init_el2_nvhe_prepare_eret 매크로

arch/arm64/include/asm/el2_setup.h

.macro __init_el2_nvhe_prepare_eret
        mov     x0, #INIT_PSTATE_EL1
        msr     spsr_el2, x0
.endm

일단 nVHE 상태로 spsr_el2 레지스터를 초기화한다.

 

__hyp_stub_vectors – for nVHE

arch/arm64/kernel/hyp-stub.S

        .text
        .pushsection    .hyp.text, "ax"

        .align 11

SYM_CODE_START(__hyp_stub_vectors)
        ventry  el2_sync_invalid                // Synchronous EL2t
        ventry  el2_irq_invalid                 // IRQ EL2t
        ventry  el2_fiq_invalid                 // FIQ EL2t
        ventry  el2_error_invalid               // Error EL2t

        ventry  elx_sync                        // Synchronous EL2h
        ventry  el2_irq_invalid                 // IRQ EL2h
        ventry  el2_fiq_invalid                 // FIQ EL2h
        ventry  el2_error_invalid               // Error EL2h

        ventry  elx_sync                        // Synchronous 64-bit EL1
        ventry  el1_irq_invalid                 // IRQ 64-bit EL1
        ventry  el1_fiq_invalid                 // FIQ 64-bit EL1
        ventry  el1_error_invalid               // Error 64-bit EL1

        ventry  el1_sync_invalid                // Synchronous 32-bit EL1
        ventry  el1_irq_invalid                 // IRQ 32-bit EL1
        ventry  el1_fiq_invalid                 // FIQ 32-bit EL1
        ventry  el1_error_invalid               // Error 32-bit EL1
SYM_CODE_END(__hyp_stub_vectors)
  • 호스트 OS에서 발생하는 sync exception만 처리하며 elx_sync 레이블이 호출된다.
  • 참고: Exception -7- (ARM64 Vector) | 문c

 


페이지 테이블 생성

create_idmap()

arch/arm64/kernel/head.S -1/2-

SYM_FUNC_START_LOCAL(create_idmap)
        mov     x28, lr
        /*
         * The ID map carries a 1:1 mapping of the physical address range
         * covered by the loaded image, which could be anywhere in DRAM. This
         * means that the required size of the VA (== PA) space is decided at
         * boot time, and could be more than the configured size of the VA
         * space for ordinary kernel and user space mappings.
         *
         * There are three cases to consider here:
         * - 39 <= VA_BITS < 48, and the ID map needs up to 48 VA bits to cover
         *   the placement of the image. In this case, we configure one extra
         *   level of translation on the fly for the ID map only. (This case
         *   also covers 42-bit VA/52-bit PA on 64k pages).
         *
         * - VA_BITS == 48, and the ID map needs more than 48 VA bits. This can
         *   only happen when using 64k pages, in which case we need to extend
         *   the root level table rather than add a level. Note that we can
         *   treat this case as 'always extended' as long as we take care not
         *   to program an unsupported T0SZ value into the TCR register.
         *
         * - Combinations that would require two additional levels of
         *   translation are not supported, e.g., VA_BITS==36 on 16k pages, or
         *   VA_BITS==39/4k pages with 5-level paging, where the input address
         *   requires more than 47 or 48 bits, respectively.
         */
#if (VA_BITS < 48)
#define IDMAP_PGD_ORDER (VA_BITS - PGDIR_SHIFT)
#define EXTRA_SHIFT     (PGDIR_SHIFT + PAGE_SHIFT - 3)

        /*
         * If VA_BITS < 48, we have to configure an additional table level.
         * First, we have to verify our assumption that the current value of
         * VA_BITS was chosen such that all translation levels are fully
         * utilised, and that lowering T0SZ will always result in an additional
         * translation level to be configured.
         */
#if VA_BITS != EXTRA_SHIFT
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endif
#else
#define IDMAP_PGD_ORDER (PHYS_MASK_SHIFT - PGDIR_SHIFT)
#define EXTRA_SHIFT
        /*
         * If VA_BITS == 48, we don't have to configure an additional
         * translation level, but the top-level table has more entries.
         */
#endif
        adrp    x0, init_idmap_pg_dir
        adrp    x3, _text
        adrp    x6, _end + MAX_FDT_SIZE + SWAPPER_BLOCK_SIZE
        mov     x7, SWAPPER_RX_MMUFLAGS

        map_memory x0, x1, x3, x6, x7, x3, IDMAP_PGD_ORDER, x10, x11, x12, x13, x14, EXTRA_SHIFT

메모리에 로드된 커널 이미지 및 FDT 영역을 물리 주소와 가상 주소를 1:1(VA = PA) 동일하게 변환되도록, Read Only 속성으로 init_idmap_pg_dir 테이블에 매핑한다. 그런 후 커널 이미지 내에 있는 init_pg_dir 페이지 테이블 영역과 커널 이미지 위에 위치한 FDT 영역은 별도록 다시 한 번 Read/Write 속성으로 재매핑을 한다.

VA_BITS는 유저 및 커널이 사용할 가상 주소의 크기에 사용할 비트 수 이다. IDMAP 테이블도 이와 동일하게 만들어야 하지만 DRAM에 로드된 커널이미지의 끝 물리 주소가 VA_BITS로 인해 커버할 가상 주소가 공간보다 더 높은 주소에 위치하면 사용자가 원하는 VA_BITS를 IDMAP에 그대로 사용할 수 없다. 때문에 이러한 경우를 위해 IDMAP에 사용할 페이지 테이블 레벨을 한 단계 더 끌어올려 사용할 수도 있다. (현재 커널 코드는 IDMAP 생성 시 페이지 테이블 레벨을 1단계 더 올리는 것은 가능하지만 2단계 이상은 처리하지 못한다.)

  • 39 <= VA_BITS < 48
    • 예) VA=39(4K 페이지, 3 레벨 페이지 테이블), PA=48 -> va_actual=48
    • 예) VA=42(64K 페이지, 3 레벨 페이지 테이블), PA=52 -> va_actual=52
  • VA_BITS == 48
    • 예) VA=48(64K 페이지, 3레벨 페이지 테이블), PA=52 -> va_actual=52

 

  • 코드 라인 2에서 일단 복귀할 주소를 담은 lr을 x28로 옮긴다.
  • 코드 라인 27~48에서 다음과 같이 VA_BITS가 48 미만인 경우와 이상인 경우에 따라 IDMAP_PGD_ORDER 및 EXTRA_SHIFT를 컴파일 타임에 결정한다.
    • VA_BITS < 48
      • IDMAP_PGD_ORDER
        • 사용할 VA_BITS – PGD에 사용할 비트
      • EXTRA_SHIFT
        • PGD에 사용할 비트 + 추가 페이지 테이블 비트
    • VA_BITS >= 48
      • IDMAP_PGD_ORDER
        • 사용할 PA_BITS – PGD에 사용할 비트
      • EXTRA_SHIFT
        • <지정된 값 없음>
  • 코드 라인 49에서 init_idmap_pg_dir의 주소 중 4K 페이지 단위로 내림 정렬한 주소를 알아와서 x0 레지스터에 대입한다.
    • 예) init_idmap_pg_dir=0x2000_4124
      • 16진수 주소의 뒷부분 3자리를 0으로 자르면 x0 = 0x2000_4000
  • 코드 라인 50~54에서 물리 주소와 가상 주소가 1:1 동일하게 준비한 후 map_memory 매크로를 통해 커널 이미지와 FDT 모두를 Read Only 매핑을 수행한다.
    • 참고로 커널의 시작 위치는 2M 단위로 정렬되어 있지만, 최대 사이즈 2M의 FDT는 커널에서 8 바이트 정렬된 주소를 사용하고 있다. FDT는 조금 뒤에 나오는 코드에서 Read/Write로 재매핑을 해야 하는데 FDT의 끝부분이 재매핑에서 잘릴 수 있으므로  미리 SWAPPER_BLOCK_SIZE 만큼 추가하여 매핑을 해둔다.
    • 주의: FDT 규격은 4 바이트 정렬을 사용한다. 그러나 커널에 지정해주는 시작 주소는 8 바이트 정렬된 주소를 사용해야 한다.
    • 커널 코드 시작 부분의 주소를 x3 레지스터에 대입한다.
    • 커널 코드 끝 주소에 fdt 최대 사이즈(2M)와 swapper 블럭 사이즈(pmd or pte)를 추가한 주소를 x6 레지스터에 대입한다.
    • 매핑에 사용할 플래그 값들을 x7에 대입한다.

 

arch/arm64/kernel/head.S -2/2-

        /* Remap the kernel page tables r/w in the ID map */
        adrp    x1, _text
        adrp    x2, init_pg_dir
        adrp    x3, init_pg_end
        bic     x4, x2, #SWAPPER_BLOCK_SIZE - 1
        mov     x5, SWAPPER_RW_MMUFLAGS
        mov     x6, #SWAPPER_BLOCK_SHIFT
        bl      remap_region

        /* Remap the FDT after the kernel image */
        adrp    x1, _text
        adrp    x22, _end + SWAPPER_BLOCK_SIZE
        bic     x2, x22, #SWAPPER_BLOCK_SIZE - 1
        bfi     x22, x21, #0, #SWAPPER_BLOCK_SHIFT              // remapped FDT address
        add     x3, x2, #MAX_FDT_SIZE + SWAPPER_BLOCK_SIZE
        bic     x4, x21, #SWAPPER_BLOCK_SIZE - 1
        mov     x5, SWAPPER_RW_MMUFLAGS
        mov     x6, #SWAPPER_BLOCK_SHIFT
        bl      remap_region

        /*
         * Since the page tables have been populated with non-cacheable
         * accesses (MMU disabled), invalidate those tables again to
         * remove any speculatively loaded cache lines.
         */
        dmb     sy

        adrp    x0, init_idmap_pg_dir
        adrp    x1, init_idmap_pg_end
        bl      dcache_inval_poc
        ret     x28
SYM_FUNC_END(create_idmap)
  • 코드 라인 2~8에서 초기 커널 페이지 테이블 용도로 사용해야 할 init_pg_dir 영역에 대해서 Read/Write 매핑으로 리매핑한다.
  • 코드 라인 11~19에서 FDT 영역도 Read/Write 매핑으로 리매핑한다. 이 과정에서 FDT 영역이 매핑단위로 align 되어 있지 않으므로 매핑 단위인 SWAPPER_BLOCK_SIZE 만큼 더해 매핑해야 FDT 영역의 전체 영역이 Read/Write 가능하다.
    • 주의: FDT 영역은 물리 주소에서 커널 이미지 공간과 다른 위치에 있을 수 있다. 처음 시스템이 부팅될 때 부트로더가 x0 레지스터에 FDT 위치를 넘겨주는 이유이다. 따라서 이 공간은 가상 주소에 매핑할 때 커널 이미지 위에 위치하도록 매핑한다. 즉 FDT 공간 만큼은 1:1 idendity 매핑을 수행하지 않는다.
  • 코드 라인 26~31에서 메모리 배리어를 수행하여 메모리에 완전하게 기록을 완료 시킨 후 init_idmap_pg_dir 전체 영역에 대해 데이터 캐시를 invalidate 한다. (해당 이유는 본문 앞 부분에서 거론하였으므로 설명은 생략)

 

다음 그림은 커널 이미지 + FDT 영역에 대해 init_idmap_pg_dir 페이지 테이블에 R/O 속성으로 1:1 identity 매핑한 모습을 보여준다.

 

 

다음 그림은 커널 이미지 내부에 있는 init_pg_dir 페이지 테이블 영역에 대해 init_idmap_pg_dir 페이지 테이블에 R/W 속성으로 재매핑하는 모습을 보여준다.

 

 

다음 그림은 커널 이미지 외부에 있는 FDT 영역에 대해 init_idmap_pg_dir 페이지 테이블에 R/W 속성으로 재매핑하는 모습을 보여준다.

  • 주의: FDT는 커널 이미지와 떨어져 있기 때문에 init_idmap_pg_dir 페이지 테이블을 사용하여도 1:1 identity 매핑을 하지 않는것을 알 수 있다.

 


페이지 테이블 관련 매크로

map_memory 매크로

arch/arm64/kernel/head.S

/*
 * Map memory for specified virtual address range. Each level of page table needed supports
 * multiple entries. If a level requires n entries the next page table level is assumed to be
 * formed from n pages.
 *
 *      tbl:    location of page table
 *      rtbl:   address to be used for first level page table entry (typically tbl + PAGE_SIZE)
 *      vstart: virtual address of start of range
 *      vend:   virtual address of end of range - we map [vstart, vend - 1]
 *      flags:  flags to use to map last level entries
 *      phys:   physical address corresponding to vstart - physical memory is contiguous
 *      order:  #imm 2log(number of entries in PGD table)
 *
 * If extra_shift is set, an extra level will be populated if the end address does
 * not fit in 'extra_shift' bits. This assumes vend is in the TTBR0 range.
 *
 * Temporaries: istart, iend, tmp, count, sv - these need to be different registers
 * Preserves:   vstart, flags
 * Corrupts:    tbl, rtbl, vend, istart, iend, tmp, count, sv
 */
        .macro map_memory, tbl, rtbl, vstart, vend, flags, phys, order, istart, iend, tmp, count, sv, extra_shift
        sub \vend, \vend, #1
        add \rtbl, \tbl, #PAGE_SIZE
        mov \count, #0

        .ifnb   \extra_shift
        tst     \vend, #~((1 << (\extra_shift)) - 1)
        b.eq    .L_\@
        compute_indices \vstart, \vend, #\extra_shift, #(PAGE_SHIFT - 3), \istart, \iend, \count
        mov \sv, \rtbl
        populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
        mov \tbl, \sv
        .endif
.L_\@:
        compute_indices \vstart, \vend, #PGDIR_SHIFT, #\order, \istart, \iend, \count
        mov \sv, \rtbl
        populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
        mov \tbl, \sv

#if SWAPPER_PGTABLE_LEVELS > 3
        compute_indices \vstart, \vend, #PUD_SHIFT, #(PAGE_SHIFT - 3), \istart, \iend, \count
        mov \sv, \rtbl
        populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
        mov \tbl, \sv
#endif

#if SWAPPER_PGTABLE_LEVELS > 2
        compute_indices \vstart, \vend, #SWAPPER_TABLE_SHIFT, #(PAGE_SHIFT - 3), \istart, \iend, \count
        mov \sv, \rtbl
        populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
        mov \tbl, \sv
#endif

        compute_indices \vstart, \vend, #SWAPPER_BLOCK_SHIFT, #(PAGE_SHIFT - 3), \istart, \iend, \count
        bic \rtbl, \phys, #SWAPPER_BLOCK_SIZE - 1
        populate_entries \tbl, \rtbl, \istart, \iend, \flags, #SWAPPER_BLOCK_SIZE, \tmp
        .endm

pgd 테이블 @tbl에 가상 주소 영역 [@vstart, @vend-1]을 필요한 전체 단계의 테이블에 매핑한다. 4K 페이지를 지원하는 경우 2M 단위로 블럭 매핑한다.

  • 코드 라인 2에서 @vend-1까지 매핑을 해야 하므로 @vend 값을 1 감소 시킨다.
  • 코드 라인 3에서 @rtbl은 다음 1레벨 페이지 테이블을 가리키도록 @tbl + #PAGE_SIZE를 대입한다.
  • 코드 라인 4에서 다음 레벨 매핑을 위해 이전 레벨에서 extra 매핑한 엔트리 수를 담는 @count를 0으로 클리어한다.
    • 다음 단계에서 사용할 기본 테이블 1개를 제외하고 추가로 필요로하는 테이블 수가 담긴다. (@count 변수 명을 @extra_count라고 생각하면 쉽다)
  • 코드 라인 6~8에서 .ifnb(if not blank)에서 @extra-shift가 지정된 경우 @vend가@extra_shift 커버 범위를 초과하지 않아 extra 페이지 테이블을 만들 필요가 없으면,즉 idmap을 위해 1단계의 추가 테이블이 필요 없으면 .L_\@ 레이블로이동한다.
    • extra_shift는 (VA_BITS < 48)에서만 (PGDIR_SHIFT + PAGE_SHIFT – 3) 값으로 지정되었었다.
  • 코드 라인 9~12에서 원래 처음 만들어야 할 pgd의 앞인 extra용 페이지 테이블에 [@vstart, @vend-1] 가상 주소에 해당하는 인덱스 엔트리를 매핑하여 연결한다.
  • 코드 라인 15~18에서 다음 단계의 pgd 페이지 테이블에 [@vstart, @vend-1] 가상 주소에 해당하는 인덱스 엔트리를 매핑하여 연결한다.
  • 코드 라인 20~25에서 다음 단계의 pud 페이지 테이블에 [@vstart, @vend-1] 가상 주소에 해당하는 인덱스 엔트리를 매핑하여 연결한다.
    • SWAPPER_PGTABLE_LEVELS이 4단계 이상에서만 pud 테이블을 사용한다.
  • 코드 라인 27~32에서 다음 단계의 페이지 테이블을 pmd 테이블에 [@vstart, @vend-1] 가상 주소에 해당하는 인덱스 엔트리에 연결한다.
    • SWAPPER_PGTABLE_LEVELS이 3단계 이상에서만 pmd 테이블을 사용한다.
  • 코드 라인 34~37에서 페이지 또는 2M 섹션(블럭)을 SWAPPER_BLOCK_SIZE 단위로 절삭하여 pte 테이블의 [@vstart, @vend-1] 가상 주소에 해당하는 인덱스 엔트리에 매핑할 때 @flags 속성을 추가하여 매핑한다.
    • 마지막 테이블 레벨의 매핑은 커널 설정에 따라 둘 중 하나의 방식을 사용한다.
      • 1) PAGE_SIZE(16K or 64K) 단위로 페이지 매핑을 사용하는 경우
      • 2) 4K 페이지 설정을 사용하여 PMD(2M) 단위의 블럭 매핑을 사용하는 경우
        • PMD 매핑을 사용하는 경우 엔트리 수에 PTRS_PER_PTE 상수가 보여 이상하다고 느낄것이다. 왜 PTRS_PER_PMD를 사용하지 않고 PTRS_PER_PTE를 사용했을까라는 의심이 있을것이다. 그냥 커널 옵션 설정에 따라 이 코드에서 PTE 매핑과 PMD 매핑 둘 다 같은 코드를 사용하고 있으므로 단지 마지막 단위의 PTRS_PER_PTE 를 사용하고 있다. 참고로 이렇게 할 수 있는 이유는 PTRS_PER_PTE와 PTRS_PER_PMD가 같은 값을 사용하기 때문이다.
    • 마지막 pupulate에 사용할 @count는 더 이상 count 용도로 사용하지 않고, 매핑할 물리 주소를 담아사용한다.

 

다음 그림은 테이블을 1 레벨 확장하여 사용하는 방법을 보여준다.

  • 확장하여 extra 페이지 테이블을 사용하는 경우 extra -> pgd -> pud -> pmd 순으로 페이지 테이블을 생성한다.

 

다음 그림은 커널 이미지 영역을 map_memory 매크로를 통해 init_idmap_pg_dir에 매핑하는 모습을 보여준다.

  • 참고로 다음 그림은 extra 페이지 테이블을 사용하지 않을 때의 사례이다.

 

compute_indices 매크로

arch/arm64/kernel/head.S

/*
 * Compute indices of table entries from virtual address range. If multiple entries
 * were needed in the previous page table level then the next page table level is assumed
 * to be composed of multiple pages. (This effectively scales the end index).
 *
 *      vstart: virtual address of start of range
 *      vend:   virtual address of end of range - we map [vstart, vend]
 *      shift:  shift used to transform virtual address into index
 *      order:  #imm 2log(number of entries in page table)
 *      istart: index in table corresponding to vstart
 *      iend:   index in table corresponding to vend
 *      count:  On entry: how many extra entries were required in previous level, scales
 *                        our end index.
 *              On exit: returns how many extra entries required for next page table level
 *
 * Preserves:   vstart, vend
 * Returns:     istart, iend, count
 */
.       .macro compute_indices, vstart, vend, shift, order, istart, iend, count
        ubfx    \istart, \vstart, \shift, \order
        ubfx    \iend, \vend, \shift, \order
        add     \iend, \iend, \count, lsl \order
        sub     \count, \iend, \istart
        .endm

페이지 테이블에서 가상 주소 범위 [@vstart, vend]에 해당하는 인덱스 번호 [@istart, @iend]를 산출한다. @count는 입출력 인자로 입력시에는 전단계에서 산출된 추가 필요 테이블 수를 담아오고, 출력시에는 다음 단계에서 사용할 기본 테이블 1개를 제외하고 추가로 필요로하는 테이블 수가 담긴다. (@count 변수 명을 @extra_count라고 생각하면 쉽다)

  • 코드 라인 2에서 가상 주소 @vstart를 @shift 비트 부터 msb 방향으로 @order 비트 수 만큼 값을 잘라 @istart에 대입한다.
  • 코드 라인 3에서 가상 주소 @vend를 @shift 비트 부터 msb 방향으로 @order 비트 수 만큼 값을 잘라 @iend에 대입한다.
  • 코드 라인 4에서 그런 후 @iend 값에 extra 엔트리 수인 @count << @order 한 값을 추가한다.
  • 코드 라인 5에서 @iend – @istart 값을 @count에 대입한다.

 

다음 그림은 compute_indices가 init_idmap 페이지 테이블에 대해 단계별로 3번 호출되는 모습을 보여준다.

 

다음 그림은 compute_indices가 init_idmap 페이지 테이블에 대해 단계별로 3번 호출되며 3개의 테이블이 더 추가된 모습을 보여준다.

 

다음 그림은 compute_indices가 작은 크기의 init_idmap 페이지 테이블에 대해 단계별로 3번 호출되며 2개의 테이블이 더 추가된 모습을 보여준다.

  • 주의: 실제 사이즈와 다르게 극단적인 가상의 사례로 8K 밖에 안되는 메모리를 매핑하였다.

 

populate_entries 매크로

arch/arm64/kernel/head.S

/*
 * Macro to populate page table entries, these entries can be pointers to the next level
 * or last level entries pointing to physical memory.
 *
 *      tbl:    page table address
 *      rtbl:   pointer to page table or physical memory
 *      index:  start index to write
 *      eindex: end index to write - [index, eindex] written to
 *      flags:  flags for pagetable entry to or in
 *      inc:    increment to rtbl between each entry
 *      tmp1:   temporary variable
 *
 * Preserves:   tbl, eindex, flags, inc
 * Corrupts:    index, tmp1
 * Returns:     rtbl
 */
        .macro populate_entries, tbl, rtbl, index, eindex, flags, inc, tmp1
.Lpe\@: phys_to_pte \tmp1, \rtbl
        orr     \tmp1, \tmp1, \flags    // tmp1 = table entry
        str     \tmp1, [\tbl, \index, lsl #3]
        add     \rtbl, \rtbl, \inc      // rtbl = pa next level
        add     \index, \index, #1
        cmp     \index, \eindex
        b.ls    .Lpe\@
        .endm

@tbl 페이지 테이블의 [@index, @eindex] 범위까지 다음 단계 테이블 또는 메모리(페이지 or 블럭)인 @rtbl에 속성 @flags를 mix하여 만든 pte 엔트리 값으로 매핑한다.

  • 코드 라인 3~4에서 @rtbl 물리 주소로 pte 엔트리 값으로 변환하고 속성 값 @flags를 추가하여 pte 엔트리 값을 구한다.
  • 코드 라인 5에서 pte 엔트리 값을 @tbl 페이지 테이블의 @index*8 주소 위치에 저장하여 매핑한다.
  • 코드 라인 6~8에서 다음 매핑할 물리 주소를 산출하기 위해 @inc를 더하고, @eindex 까지 반복한다.

 

다음 그림은 페이지 테이블이 static하게 연속된 페이지 다음 단계 테이블들에 연결되는 모습을 보여준다.

  • [index, eindex] 엔트리들이 다음 단계 페이지 테이블들로 연결된다.

 

phys_to_pte 매크로

arch/arm64/include/asm/assembler.h

        .macro  phys_to_pte, pte, phys
#ifdef CONFIG_ARM64_PA_BITS_52
        /*
         * We assume \phys is 64K aligned and this is guaranteed by only
         * supporting this configuration with 64K pages.
         */
        orr     \pte, \phys, \phys, lsr #36
        and     \pte, \pte, #PTE_ADDR_MASK
#else
        mov     \pte, \phys
#endif
        .endm

물리 주소 @phys를 사용하여 @pte 엔트리 값을 구성한다. (속성 값은 아직 더하지 않은 상태이다)

  • 코드 라인 7~8에서 52비트 물리 주소를 지원하는 경우 @phys 값에 36비트 우측 시프트한 @phys 값을 더한 후 필요 주소 영역(bits[47:12])만 사용할 수 있도록 마스크하여 @pte에 저장한다. 저장되는 @pte 값은 다음과 같이 구성된다.
    • @pte bits[47:16] <– 물리 주소 @phys bits[47:16]
    • @pte bits[15:12] <– 물리 주소 @phys bits[51:48]
  • 코드 라인 10에서 52비트 물리 주소를 사용하지 않는 경우 @phys 값을 @pte로 그대로 사용한다.

 

다음 그림은 연결될 물리 주소 phys를 사용하여 pte 엔트리로 변경된 모습을 보여준다.

  • VABITS=52를 사용하는 경우 phys의 bits[55:48]이 bits[15:12] 위치로 이동한다.

 

remap_region()

arch/arm64/kernel/head.S

/*
 * Remap a subregion created with the map_memory macro with modified attributes
 * or output address. The entire remapped region must have been covered in the
 * invocation of map_memory.
 *
 * x0: last level table address (returned in first argument to map_memory)
 * x1: start VA of the existing mapping
 * x2: start VA of the region to update
 * x3: end VA of the region to update (exclusive)
 * x4: start PA associated with the region to update
 * x5: attributes to set on the updated region
 * x6: order of the last level mappings
 */
SYM_FUNC_START_LOCAL(remap_region)
        sub     x3, x3, #1              // make end inclusive

        // Get the index offset for the start of the last level table
        lsr     x1, x1, x6
        bfi     x1, xzr, #0, #PAGE_SHIFT - 3

        // Derive the start and end indexes into the last level table
        // associated with the provided region
        lsr     x2, x2, x6
        lsr     x3, x3, x6
        sub     x2, x2, x1
        sub     x3, x3, x1

        mov     x1, #1
        lsl     x6, x1, x6              // block size at this level

        populate_entries x0, x4, x2, x3, x5, x6, x7
        ret
SYM_FUNC_END(remap_region)

요청한 영역을 다시 다른 속성으로 변경하여 리매핑을 수행한다.

  • x0: map_memory 수행 후 반환된 마지막 레벨 페이지 테이블 주소
  • x1: 기존 매핑된 시작 가상 주소
  • x2: 갱신할 영역의 시작 가상 주소
  • x3: 갱신할 영역의 끝 가상 주소
  • x4: 갱신할 영역에 연결될 시작 물리 주소
  • x5: 갱신할 영역에 설정할 속성
  • x6: 마지막 레벨 매핑 order

 

  • 코드 라인 2에서 갱신할 테이블의 끝 부분의 주소를 -1을 한다.
  • 코드 라인 5~6에서 x1 레지스터 값을 블럭(or 페이지) 매핑에 사용한 order(x6) 만큼 우측 shift 하여 인덱스 표현에 필요 없는 하위 비트들을 버린다. 그런 후 이 값에서 다시 하위 PAGE_SHIFT-3 비트 수 만큼을 clear 하여 마지막 테이블의 인덱스만을 꺼내기 위한 기준값(x1)을 만들어둔다.
  • 코드 라인 10~13에서 갱신할 영역의 시작(x2)과 끝(x3)을 대상으로 각각 order(x6) 만큼 우측 시프트하고 기준값(x1)을 빼면 시작 인덱스(sindex)와 끝 인덱스(eindex)가 산출된다.
  • 코드 라인 15~16에서 매핑에 사용할 메모리(블럭 or 페이지)사이즈를 x6에 대입한다.
  • 코드 라인 18~19에서 populate_entries  매크로를 통해 매핑을 수행한다.
    • @tbl(x0): 매핑에 사용할 테이블 주소
    • @rtbl(x4): 매핑될 물리 메모리(페이지 or 블럭)
    • @index(x2): 매핑에 사용할 테이블 내 시작 인덱스
    • @eindex(x3): 매핑에 사용할 테이블 내 끝 인덱스
    • @flags(x5): 매핑에 사용할 속성
    • @inc(x6): 인덱스 변경 시 마다 증가할 사이즈
    • @tmp1(x7): 임시 레지스터

 

다음과 같은 조건에서 init_pg_dir 테이블을 R/W 속성으로 재매핑을 하는 과정을 알아본다.

예) 4K 페이지, VA_BITS=48, 시작주소, 커널 이미지 40M

  • 주요 가상 주소((컴파일하여 산출된 심볼 가상 주소) 및 물리 주소는 다음과 같다고 가정한다. idmap 방식으로 매핑할때에는 PA 주소와 동일하게 VA를 사용한다는 점에 주의해야 한다.
    • _text
      • 심볼 VA=0xffff800010000000
      • VA=PA=0x4020_0000
    • _end
      • 심볼 VA=0xffff8000128f0000
      • VA=PA=0x42a0_0000
    • init_pg_dir
      • 심볼 VA=0xffff8000128e3000
      • VA=PA=0x429f_3000
    • init_pg_end
      • 심볼 VA=0xffff8000128e6000
      • VA=PA=0x429f_6000
  • 4K 페이지를 선택하였으므로 다음과 같은 매핑 사이즈를 갖는다.
    • PMD(2M) 단위의 블럭매핑 사용
    • SWAPPER_BLOCK_SHIFT=21
    • SWAPPER_BLOCK_SIZE=2M
    • 매핑할 전체 사이즈
      • 40M 커널 이미지 + MAX_FDT_SIZE(2M) + SWAPPER_BLOCK_SIZE(2M) = 44M 이다.
  • remap_region()에 사용된 인자
    • x0:
      • last level table address (returned in first argument to map_memory)
      • map_memory()가 init_idmap_pg_dir의 매핑(pgd->pud->pmd)에 사용한 마지막 테이블(pmd 테이블 시작)을 지정한다.
    • x1:
      • 기존 매핑에 사용한 시작 가상 주소
      • idmap에 사용하였으니 VA=PA=__pa(_text)=0x4020_0000
    • x2:
      • start VA of the region to update
      • 다시 매핑할 영역의 시작 VA=PA=__pa(init_pg_dir)=0x429f_3000
    • x3:
      • end VA of the region to update (exclusive)
      • 다시 매핑할 영역의 끝 VA=PA=__pa(init_pg_end)=0x429f_6000
    • x4:
      • start PA associated with the region to update
      • init_pg_dir 주소를 2M 단위로 매핑하므로 2M 단위 절삭한 물리 주소=0x4280_0000
    • x5:
      • attributes to set on the updated region
      • 갱신할 속성은 RW가 가능하도록 SWAPPER_RW_MMUFLAGS를 사용한다.
    • x6:
      • order of the last level mappings
      • 마지막 레벨에 사용하는 매핑 order = SWAPPER_BLOCK_SHIFT = 21
  • remap_region()이 진행하면서 산출되는 값들
    • x3:
      • 끝 주소에서 1을 뺀 값=__pa(init_pg_dir-1)=0x429f_2fff
    • x1:
      • 기존 매핑에 사용한 시작 가상 주소 >> 21 하고 하위 9비트를 clear 한다. (x1 레지스터는 임시 변수로 사용된다)
      • x1 = 0x4020_0000 >> 21 & ~0x1ff = 0x201 & ~0x1ff = 0x200
    • x2:
      • (0x429f_3000 >> 21) – x1 = 0x214 – 0x200 = 0x14
    • x3:
      • (0x429f_6000 >> 21) – x1 = 0x214 – 0x200 = 0x14
    • x6:
      • 1 << 21 = 0x20_0000
  • populate_entries()로 전달되는 값
    • @tbl(x0):
      • 매핑에 사용할 테이블 주소 =
    • @rtbl(x4):
      • 매핑할 메모리 시작 블럭 주소
      • init_pg_dir 주소를 2M 단위로 매핑하므로 2M 단위 절삭한 주소 = 0x4280_0000
    • @index(x2):
      • 매핑에 사용할 테이블 내 시작 인덱스 = 0x14
    • @eindex(x3):
      • 매핑에 사용할 테이블 내 끝 인덱스 = 0x14
    • @flags(x5): 매핑에 사용할 속성
      • SWAPPER_RW_MMUFLAGS
    • @inc(x6): 인덱스 변경 시 마다 증가할 사이즈
      • 0x20_0000
    • @tmp1(x7): 임시 레지스터

 


CPU 설정

__cpu_setup()

arch/arm64/mm/proc.S -1/3-

/*
 *      __cpu_setup
 *
 *      Initialise the processor for turning the MMU on.
 *
 * Input:
 *      x0 - actual number of VA bits (ignored unless VA_BITS > 48)
 * Output:
 *      Return in x0 the value of the SCTLR_EL1 register.
 */
        .pushsection ".idmap.text", "awx"
SYM_FUNC_START(__cpu_setup)
        tlbi    vmalle1                         // Invalidate local TLB
        dsb     nsh

        mov     x1, #3 << 20
        msr     cpacr_el1, x1                   // Enable FP/ASIMD
        mov     x1, #1 << 12                    // Reset mdscr_el1 and disable
        msr     mdscr_el1, x1                   // access to the DCC from EL0
        isb                                     // Unmask debug exceptions now,
        enable_dbg                              // since this is per-cpu
        reset_pmuserenr_el0 x1                  // Disable PMU access from EL0
        reset_amuserenr_el0 x1                  // Disable AMU access from EL0

        /*
         * Default values for VMSA control registers. These will be adjusted
         * below depending on detected CPU features.
         */
        mair    .req    x17
        tcr     .req    x16
        mov_q   mair, MAIR_EL1_SET
        mov_q   tcr, TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
                        TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
                        TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS

MMU를 켜기 위해 현재 cpu를 초기화한다. (x0 레지스터에 vabits_actual 값이 지정되며, 단 VA_BITS > 48 이상에서만 유효하다)

  • 코드 라인 3~4에서 local cpu의 TLB 캐시를 모두 invalidate 하고 dsb 명령을 통해 TLB 캐시 invalidate 작업이 완료될 때까지 기다린다.
    • cold 부팅한 cpu의 TLB 캐시안에 있는 쓰레기 값을 모두 제거한 후 MMU를 켜야 한다.
  • 코드 라인 6~7에서 CPACR_EL1의 bit[20:21]에 0b11 값을 지정하여 FP/ASIMD 명령 사용 시 커널로 trap 되지 않도록 enable 한다.
  • 코드 라인 8~9에서 EL0 즉 사용자 공간에서 디버그 동작을 수행하도록 mdscr_el1 레지스터의 DCC(Debug Communication Channel) 비트를 on하여 커널로 trap 한다.
  • 코드 라인 10~11에서 isb 명령을 통해 명령 파이프를 비우고, DAIF 플래그 중 A 플래그를 unmask 하는 것으로 지금 이후 부터 곧바로 debug exceptions이 가능하도록 한다.
    • enable_dbg 매크로는 daifclr에 #8 값을 기록하여 A 플래그를 unmask 한다.
  • 코드 라인 12~13에서 유저 영역에서 PMU 및 AMU 레지스터의 사용을 금지시킨다.
  • 코드 라인 19~20에서 as 디렉티브 .req를 사용하여 x17 및 x16레지스터 이름을 mair 및 tcr을 사용할 수 있게 어셈블러에 지시 한다.
  • 코드 라인 21에서 MAIR(Memory Attribute Indirection Register) 레지스터에 기록할 메모리 페이지의 간접 매핑 속성들을  mail(x17)에 대입한다.
    • MAIR 레지스터는 최대 8개의 속성을 담아둘 수 있고, 현재 커널이 5개의 속성을 지정하여 사용한다.
    • 아직 부팅한 시스템이 MTE(Memory Tagging Extension) 기능을 가지고 있는지 확인이 안되었으므로 디폴트 설정에서는 mte 메모리 매핑 속성 자리에 normal 메모리 매핑 속성 값을 대신 사용한다.
  • 코드 라인 22에서 TCR(Translation Control Register) 레지스터에 기록할 디폴트 설정을 tcr(x16)에 대입한다.

 

arch/arm64/mm/proc.S -2/3-

#ifdef CONFIG_ARM64_MTE
        /*
         * Update MAIR_EL1, GCR_EL1 and TFSR*_EL1 if MTE is supported
         * (ID_AA64PFR1_EL1[11:8] > 1).
         */
        mrs     x10, ID_AA64PFR1_EL1
        ubfx    x10, x10, #ID_AA64PFR1_MTE_SHIFT, #4
        cmp     x10, #ID_AA64PFR1_MTE
        b.lt    1f

        /* Normal Tagged memory type at the corresponding MAIR index */
        mov     x10, #MAIR_ATTR_NORMAL_TAGGED
        bfi     mair, x10, #(8 *  MT_NORMAL_TAGGED), #8

        mov     x10, #KERNEL_GCR_EL1
        msr_s   SYS_GCR_EL1, x10

        /*
         * If GCR_EL1.RRND=1 is implemented the same way as RRND=0, then
         * RGSR_EL1.SEED must be non-zero for IRG to produce
         * pseudorandom numbers. As RGSR_EL1 is UNKNOWN out of reset, we
         * must initialize it.
         */
        mrs     x10, CNTVCT_EL0
        ands    x10, x10, #SYS_RGSR_EL1_SEED_MASK
        csinc   x10, x10, xzr, ne
        lsl     x10, x10, #SYS_RGSR_EL1_SEED_SHIFT
        msr_s   SYS_RGSR_EL1, x10

        /* clear any pending tag check faults in TFSR*_EL1 */
        msr_s   SYS_TFSR_EL1, xzr
        msr_s   SYS_TFSRE0_EL1, xzr

        /* set the TCR_EL1 bits */
        mov_q   x10, TCR_MTE_FLAGS
        orr     tcr, tcr, x10
1:
#endif
        tcr_clear_errata_bits tcr, x9, x5

ARMv8.2-MTE 기능이 탑재된 경우 유저 영역에서 할당한 메모리에 대해 color 태그를 부여하여 다른 영역에 액세스하는 것을 방지할 수 있다. 이러한 것을 커널이 지원하기 위해 관련된 레지스터들을 설정한다.

  • 코드 라인 6~9에서 AArch64 아키텍처에는 해당 cpu가 지원하는 기능들의 지원여부를 알아보기 위한 ID 레지스터들이 여러 개가 존재한다. 그 중 ID_AA64PFR1_EL1 레지스터의 MTE 필드를 조사하여 MTE(Memory Tagging Extension) 기능을 지원하지 않으면 1: 레이블로 이동한다.
  • 코드 라인  12~13에서 MTE 기능을 지원하는 것을 확인하였기 때문에 MTE 속성을 지원하도록  해당 속성위치의 값을 MAIR_ATTR_NORMAL_TAGGED 값으로 변경한다.
  • 코드 라인 15~16에서 GCR_EL1 (Tag Control Register) 레지스터에 디폴트 설정을 기록한다.
  • 코드 라인 24~28에서 의사 난수를 생성하기 위해 Virtual 카운터(CNTVCT) 레지스터의 하위 16비트 값을 읽어와서 RGSR_EL1 레지스터의 SEED 필드 값으로 설정한다. 단 SEED 값은 0을 사용하면 안되므로 이 때엔 SEED 값은 1을 사용한다.
    • ands(Bitwise AND Setting flags) 명령어
      • and 연산도 하고, tst 명령과 같이 그 결과에 따라 NZCV 플래그들을 설정한다.
    • csinc(conditional select increment)  명령어
      • ne(not equal)인 경우 x10값을 그대로 사용하고  그렇지 않은 경우 xzr + 1, 즉 1을 대입한다.
  • 코드 라인 31~32에서 EL1과 EL0에서 pending 되었을 수도 있는 tag check faults를 클리어하기 위해 TFSR_EL1(Tag Fault Status Register) 및 TFSRE0_EL1 레지스터를 0으로 초기화한다.
  • 코드 라인 35~36에서 tcr(x16)에 TBI1 및 TBID1 필드 값들을 1로 설정한다.
    • TBI1(Top Byte Ignore for TTBR1_EL1 region)
      • 1=EL1에서 주소 계산에 Top Byte Ignore를 사용한다.
      • 0=EL1에서 주소 계산에 Top Byte도 사용한다.
    • TBID1
      • 1=위의 TBI1 기능 사용시 데이터 주소에만 적용하도록 한다.
      • 0=위의 TBI1 기능 사용시 명령 주소와 데이터 주소 모두에 적용하도록 한다.
  • 코드 라인 39에서 대형 클러스터 시스템 구축에 상요된 Fujitsu-A64 칩을 위한 errata 코드이다.
    • Fujitsu-A64FX erratum E#010001: Undefined fault may occur wrongly

 

arch/arm64/mm/proc.S -3/3-

#ifdef CONFIG_ARM64_VA_BITS_52
        sub             x9, xzr, x0
        add             x9, x9, #64
        tcr_set_t1sz    tcr, x9
#else
        idmap_get_t0sz  x9
#endif
        tcr_set_t0sz    tcr, x9

        /*
         * Set the IPS bits in TCR_EL1.
         */
        tcr_compute_pa_size tcr, #TCR_IPS_SHIFT, x5, x6
#ifdef CONFIG_ARM64_HW_AFDBM
        /*
         * Enable hardware update of the Access Flags bit.
         * Hardware dirty bit management is enabled later,
         * via capabilities.
         */
        mrs     x9, ID_AA64MMFR1_EL1
        and     x9, x9, #0xf
        cbz     x9, 1f
        orr     tcr, tcr, #TCR_HA               // hardware Access flag update
1:
#endif  /* CONFIG_ARM64_HW_AFDBM */
        msr     mair_el1, mair
        msr     tcr_el1, tcr
        /*
         * Prepare SCTLR
         */
        mov_q   x0, INIT_SCTLR_EL1_MMU_ON
        ret                                     // return to head.S

        .unreq  mair
        .unreq  tcr
SYM_FUNC_END(__cpu_setup)
  • 코드 라인 1~8에서 idmap 페이지 테이블을 위해 TCR 레지스터의 t0sz와 t1sz 필드 값을 지정한다.
    • 이 값을 통해 idmap 페이지 테이블이 관리할 가상 주소 공간의 크기가 결정된다.
    • 52비트 커널을 사용하도록 빌드된 커널 이미지인 경우 x0 값에는 실제 va 비트 수가 담기므로 48 또는 52로 이 함수에 진입한다. 이 값을 TCR 레지스터의 T1SZ에 저장해야 하므로 64 – x0 한 값을 사용한다.
    • 그 외 52비트 커널을 사용한 경우가 아니라면 실제 커널의 끝 주소와 사용할 VA 공간과의 비트 차이만큼을 고려하여 TCR 레지스터의 T0SZ 필드에 대입한다.
  • 코드 라인 13에서 아키텍처가 지원하는 최대 PA 사이즈와 커널빌드시 지정한 최대 PA 사이즈 중 가장 작은 값으로 최대 PA를 최대한 줄여 최적화한 값을 사용하도록 @tcr의 @pos 위치에 있는 3비트의 IPS 필드 값을 갱신한다.
  • 코드 라인 20~23에서 ID_AA64MMFR1_EL1 레지스터의 하위 4비트 HAFDBS(Hardware updates to Access flag and Dirty state) 필드 값을 읽어 0이 아니면 HA 기능이 지원되므로 @tcr의 HA(Hardware Access flags update) 비트를 설정하여 El0와 EL1에 대한 stage 1 변환 내에서 HA 비트 갱신 기능이 가능하도록 한다.
  • 코드 라인 26~27에서 지금까지 준비한 mair(x17) 값을 MAIR_EL1 레지스터에 저장하고, tcr(x16) 레지스터 값도 TCR_EL1 레지스터에 저장한다.
  • 코드 라인 31~32에서 x0 레지스터에 mmu on을 하기 위한 SCTLR 레지스터용 초깃값을 대입하고, 복귀한다.
  • 코드 라인 34~35에서 “.req”로 정의한 mair 및 tcr 레지스터 이름을 취소한다.

 

tcr_set_t0sz 매크로

arch/arm64/include/asm/assembler.h

/*
 * tcr_set_t0sz - update TCR.T0SZ so that we can load the ID map
 */
.       .macro  tcr_set_t0sz, valreg, t0sz
        bfi     \valreg, \t0sz, #TCR_T0SZ_OFFSET, #TCR_TxSZ_WIDTH
        .endm

TCR 레지스터에 기록하기 위한 @valreg 값 중 T0SZ 필드에 @t0sz 값을 채운다.

  • @valreg에 @t0sz 값을 #TCR_T0SZ_OFFSET(0) 비트 위치부터 #TCR_TxSZ_WIDTH(6) 비트 수만큼 채운다.

 

tcr_set_t1sz 매크로

arch/arm64/include/asm/assembler.h

/*
 * tcr_set_t1sz - update TCR.T1SZ
 */
.       .macro  tcr_set_t1sz, valreg, t1sz
        bfi     \valreg, \t1sz, #TCR_T1SZ_OFFSET, #TCR_TxSZ_WIDTH
        .endm

TCR 레지스터에 기록하기 위한 @valreg 값 중 T1SZ 필드에 @t1sz 값을 채운다.

  • @valreg에 @t1sz 값을 #TCR_T1SZ_OFFSET(16) 비트 위치부터 #TCR_TxSZ_WIDTH(6) 비트 수만큼 채운다.

 

idmap_get_t0sz 매크로

arch/arm64/include/asm/assembler.h

/*
 * idmap_get_t0sz - get the T0SZ value needed to cover the ID map
 *
 * Calculate the maximum allowed value for TCR_EL1.T0SZ so that the
 * entire ID map region can be mapped. As T0SZ == (64 - #bits used),
 * this number conveniently equals the number of leading zeroes in
 * the physical address of _end.
 */
.       .macro  idmap_get_t0sz, reg
        adrp    \reg, _end
        orr     \reg, \reg, #(1 << VA_BITS_MIN) - 1
        clz     \reg, \reg
        .endm

@reg에 idmap을 커버할 T0SZ 값을 구해온다.

  • 커널 이미지의 끝 주소(_end)를 VA_BITS_MIN 만큼 쉬프트하고 쉬프트한 비트들은 1로 채운다. 그런 후 bit63부터 시작하여 처음 발견되는 0의 개수를 알아온다.
    • 예) _end = 0x1234_5678,VA_BITS_MIN=39
      • @reg = 0x1234_5000 | 0x0000_007f_ffff_ffff = 0x0000_007f_ffff_ffff
      • 최종 @reg = 25
    • 예) _end = 0x0000_1234_5678_9ab0, VA_BITS_MIN=39
      • @reg = 0x0000_1234_5678_9000 | 0x0000_007f_ffff_ffff = 0x0000_127f_ffff_ffff
      • 죄종 @reg = 19
  • TCR_EL1.T0SZ에 사용할 값은 64 – 매핑에 사용할 VA 비트 값이다.

 

MAIR_EL1_SET 매크로 상수

arch/arm64/mm/proc.S

/*
 * Default MAIR_EL1. MT_NORMAL_TAGGED is initially mapped as Normal memory and
 * changed during __cpu_setup to Normal Tagged if the system supports MTE.
 */
#define MAIR_EL1_SET                                                    \
        (MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRnE, MT_DEVICE_nGnRnE) |      \
         MAIR_ATTRIDX(MAIR_ATTR_DEVICE_nGnRE, MT_DEVICE_nGnRE) |        \
         MAIR_ATTRIDX(MAIR_ATTR_NORMAL_NC, MT_NORMAL_NC) |              \
         MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL) |                    \
         MAIR_ATTRIDX(MAIR_ATTR_NORMAL, MT_NORMAL_TAGGED))

MAIR_EL1 레지스터에는 커널이 사용할 5가지 디폴트 매핑 설정이 지정된다. 단 MT_NORMAL_TAGGED (1) 속성 자리에는 tagged 메모리 매핑 속성인 MAIR_ATTR_NORMAL_TAGGED 대신 일반 메모리 매핑 속성인 MAIR_ATTR_NORMAL 값이 사용되고 있으므로 기본 설정만으로는 tagged 메모리 속성을 지원하지 않는다.

 

tcr_compute_pa_size 매크로

arch/arm64/include/asm/assembler.h

/*
 * tcr_compute_pa_size - set TCR.(I)PS to the highest supported
 * ID_AA64MMFR0_EL1.PARange value
 *
 *      tcr:            register with the TCR_ELx value to be updated
 *      pos:            IPS or PS bitfield position
 *      tmp{0,1}:       temporary registers
 */
.       .macro  tcr_compute_pa_size, tcr, pos, tmp0, tmp1
        mrs     \tmp0, ID_AA64MMFR0_EL1
        // Narrow PARange to fit the PS field in TCR_ELx
        ubfx    \tmp0, \tmp0, #ID_AA64MMFR0_PARANGE_SHIFT, #3
        mov     \tmp1, #ID_AA64MMFR0_PARANGE_MAX
        cmp     \tmp0, \tmp1
        csel    \tmp0, \tmp1, \tmp0, hi
        bfi     \tcr, \tmp0, \pos, #3
        .endm

아키텍처가 지원하는 최대 PA 사이즈와 커널빌드시 지정한 최대 PA 사이즈 중 가장 작은 값으로 최대 PA를 최대한 줄여 최적화한 값을 사용하도록 @tcr의 @pos 위치에 있는 3비트의 IPS 필드 값을 갱신한다.

  • 코드 라인 2~4에서 AA64MMFR0_EL1 레지스터의 PARANGE 필드값 3비트를 @tmp0에 읽어온다.
  • 코드 라인 5~7에서 이 값을 최대 PA 비트(ID_AA64MMFR0_PARANGE_MAX=6 or 5)가 담긴 @tmp1과 비교하여 두 값중 더 작은 값을 @tmp0로 사용한다.
  • 코드 라인 8에서 산출된 가장 작은 PA 비트 값에 해당하는 @tcr 레지스터의 @pos 위치에 3비트를 기록한다.

 

TCR 레지스터의 IPS(Intermediate Physical address Size)  값

최대 물리 주소 표현에 사용하는 비트 수

  • 0=32bits, 4GB
  • 1=36bits, 64GB
  • 2=40bits, 1TB
  • 3=42bits, 4TB
  • 4=44bits, 16TB
  • 5=48bits, 256TB
  • 6=52bits, 4PB

 


부트 CPU 스위치

 

다음 그림은 head.S가 진행되는 흐름을 보여준다. __primary_switch: 이후 __primary_switched:로의 전환이 커널 v6.0부터 일부 바뀌었다.

 

MMU 스위치 전

__primary_switch:

arch/arm64/kernel/head.S

SYM_FUNC_START_LOCAL(__primary_switch)
        adrp    x1, reserved_pg_dir
        adrp    x2, init_idmap_pg_dir
        bl      __enable_mmu
#ifdef CONFIG_RELOCATABLE
        adrp    x23, KERNEL_START
        and     x23, x23, MIN_KIMG_ALIGN - 1
#ifdef CONFIG_RANDOMIZE_BASE
        mov     x0, x22
        adrp    x1, init_pg_end
        mov     sp, x1
        mov     x29, xzr
        bl      __pi_kaslr_early_init
        and     x24, x0, #SZ_2M - 1             // capture memstart offset seed
        bic     x0, x0, #SZ_2M - 1
        orr     x23, x23, x0                    // record kernel offset
#endif
#endif
        bl      clear_page_tables
        bl      create_kernel_mapping

        adrp    x1, init_pg_dir
        load_ttbr1 x1, x1, x2
#ifdef CONFIG_RELOCATABLE
        bl      __relocate_kernel
#endif
        ldr     x8, =__primary_switched
        adrp    x0, KERNEL_START                // __pa(KERNEL_START)
        br      x8
SYM_FUNC_END(__primary_switch)

MMU를 활성화하고 [커널 랜덤 위치를 결정한 후] 초기 커널용 페이지 테이블을 매핑하고, [커널 심볼 재배치]를 수행한다. 그런 후 __primary_switched로 점프한다. []안은 커널 옵션에 따라 수행한다.

  • 코드 라인 2~4에서 init_idmap_pg_dir 페이지 테이블을 사용하여 mmu를 활성화한다.
  • 코드 라인 5~7에서 커널 심볼 재배치 설정이 있는 경우 x23에 커널이 시작되는 시작 주소(MMU는 켜있지만 여전히 VA=PA)를 대입하고, 2M 단위로 내림 정렬한다.
  • 코드 라인 8~13에서 KASLR(커널 랜덤 위치) 옵션이 지정된 경우 커널의 랜덤 위치를 결정하는데 사용할 랜덤 seed 값을 디바이스 트리(DTB)에서 /chosen 노드의 kaslr-seed= 속성에 지정된 값을 읽어온다. 그 후 이를 기반으로 랜덤값을 산출하고 이 랜덤값으로 커널이 위치할 랜덤 주소 offset 값을 알아온다.
    • x0에 DTB 주소를 담고, x1에는 init_pg_dir의 끝 주소를 담는다. 이 주소는 sp 에도 저장해두고, x29에 0을 대입한다.
    • 예) kaslr-seed = <0xfeedbeef 0xc0def00d>;
    • 이후 C 루틴으로 동작하는 최초 커널 함수인 start_kernel()부터 랜덤 가상 주소에서 실행된다.
  • 코드 라인 14~16에서 커널이 위치할 랜덤 주소 offset 값을 알아왔으면 이 주소0 값을 2M 단위로 내림정렬한 후 x24에 대입한다. x23에 커널 랜덤 시작 위치를 대입하기 위해 __pi_kaslr_early_init에서 산출한 랜덤 offset 주소(x0)를 기존 커널 시작 주소(x23)에 더한다.
  • 코드 라인 19에서 초기 커널 페이지 테이블 용도로 사용할 init_pg_dir을 모두 0으로 클리어한다.
  • 코드 라인 20에서 초기 커널 페이지 테이블 init_pg_dir에 전체 커널 이미지(_text ~ _end)를 랜덤 주소부터 RW 속성으 매핑한다.
  • 코드 라인 22~23에서 init_pg_dir 페이지 테이블을 ttbr1에 지정한다.
  • 코드 라인 24~26에서 커널 심볼 재배치 설정이 있는 경우 커널 심볼들을 재배치한다.
    • 재배치 정보를 담고 있는 .rela.dyn 섹션에 위치한 엔트리들을 옮긴다.
  • 코드 라인 27~29에서x0에 커널 이미지의 물리 주소 위치(offset 구간 포함)가 담긴 주소를 담고 __primary_switched로 점프한다.
    • __primary_switched() 함수로 점프할 때부터 init_pg_dir 페이지 테이블을 사용한다.

 

MMU 활성화

__enable_mmu()

arch/arm64/kernel/head.S

/*
 * Enable the MMU.
 *
 *  x0  = SCTLR_EL1 value for turning on the MMU.
 *  x1  = TTBR1_EL1 value
 *  x2  = ID map root table address
 *
 * Returns to the caller via x30/lr. This requires the caller to be covered
 * by the .idmap.text section.
 *
 * Checks if the selected granule size is supported by the CPU.
 * If it isn't, park the CPU
 */
SYM_FUNC_START(__enable_mmu)
        mrs     x3, ID_AA64MMFR0_EL1
        ubfx    x3, x3, #ID_AA64MMFR0_TGRAN_SHIFT, 4
        cmp     x3, #ID_AA64MMFR0_TGRAN_SUPPORTED_MIN
        b.lt    __no_granule_support
        cmp     x3, #ID_AA64MMFR0_TGRAN_SUPPORTED_MAX
        b.gt    __no_granule_support
        phys_to_ttbr x2, x2
        msr     ttbr0_el1, x2                   // load TTBR0
        load_ttbr1 x1, x1, x3

        set_sctlr_el1   x0

        ret
SYM_FUNC_END(__enable_mmu)

TTBR0 레지스터가 x2(init_idmap or idmap용) 테이블, 그리고 TTBR1 레지스터가 x1(reserved or swapper용) 테이블을 가리키게 하고 MMU를 켠다.

  • 이 함수가 각 cpu 마다 호출될 때 동작은 다음과 같다.
    • primary cpu에서 이 함수 호출
      • x1=reserved_pg_dir, x2=reserved_pg_dir 이 담긴채로 진입
      • MMU를 enable 하는 순간 PC(Program Counter)는 주소가 낮은 물리 주소에서 동작하므로 init_idmap_pg_dir 페이지 테이블을 기반으로 먼저 동작한다.
    • seconcary cpu에서 이 함수 호출
      • x1=swapper_pg_dir, x2=idmap_pg_dir이 담긴채로 진입
      • MMU를 enable 하는 순간 PC(Program Counter)는 주소가 낮은 물리 주소에서 동작하므로 idmap_pg_dir을 기반으로 동작한다.
  • 코드 라인2-7에서 MMFR0_EL1 (Memory Model Feature Register 0 Register – EL1)의 TGRAN 필드값을 읽어와서 커널이 설정한 페이지 단위(4K, 16K, 64K)의 각각의 범위가 지원하는지 확인하고, 지원하지 않는 경우 __no_granule_support 레이블로 이동한다.
  • 코드 라인 8~9에서 TTBR0_EL1 레지스터에 x2 테이블(init_idmap_pg_dir or idmap_pg_dir)을 지정하여 사용할 수 있도록 설정한다.
  • 코드 라인 10에서 TTBR1_EL1 레지스터에 x1 테이블(reserved_pg_dir or swapper_pg_dir)을 지정하여 사용할 수 있도록 설정한다.
    • reserved_pg_dir 는 블랭크 테이블이므로 MMU가 켜진 이후부터 커널 공간의 주소를 액세스할 수 없게 한다.
  • 코드 라인 12에서 드디어 SCTLR_EL1 레지스터를 사용하여 MMU를 활성화한다.

 

phys_to_ttbr 매크로

arch/arm64/include/asm/assembler.h

/*
 * Arrange a physical address in a TTBR register, taking care of 52-bit
 * addresses.
 *
 *      phys:   physical address, preserved
 *      ttbr:   returns the TTBR value
 */
        .macro  phys_to_ttbr, ttbr, phys
#ifdef CONFIG_ARM64_PA_BITS_52
        orr     \ttbr, \phys, \phys, lsr #46
        and     \ttbr, \ttbr, #TTBR_BADDR_MASK_52
#else
        mov     \ttbr, \phys
#endif
        .endm

테이블의 물리주소 @phys를 TTBR 레지스터 포맷으로 변경한다.

  • 52bit VA를 지원하는 커널에서는 주소의 일부 비트들을 이동시켜야 한다.

 

load_ttbr1 매크로

arch/arm64/include/asm/assembler.h

/*
 * load_ttbr1 - install @pgtbl as a TTBR1 page table
 * pgtbl preserved
 * tmp1/tmp2 clobbered, either may overlap with pgtbl
 */
.       .macro          load_ttbr1, pgtbl, tmp1, tmp2
        phys_to_ttbr    \tmp1, \pgtbl
        offset_ttbr1    \tmp1, \tmp2
        msr             ttbr1_el1, \tmp1
        isb
        .endm

TTBR1 레지스터가 페이지 테이블 @pgtbl 의 주소를 가리키도록 저장한다. (idmap에서 pgd 테이블 엔트리가 확장되어 사용되는 경우 이를 반영하는 주소로 지정된다)

  • 코드 라인 2에서 페이지 테이블 주소 @pgtb을 먼저 임시 레지스터 @tmp1 에 대입한다.
  • 코드 라인 3에서 다시 offset 적용이 필요한 상황에서 offset을 적용한 주소를 @tmp1으로 재산출한다. (@tmp2 는 임시 레지스터로 활용한다)
  • 코드 라인 4에서 TTBR1_EL1 레지스터에 @tmp1을 저장한다.
  • 코드 라인 5에서 명령 파이프를 비운다.

 

offset_ttbr1 매크로

arch/arm64/include/asm/assembler.h

/*
 * Offset ttbr1 to allow for 48-bit kernel VAs set with 52-bit PTRS_PER_PGD.
 * orr is used as it can cover the immediate value (and is idempotent).
 * In future this may be nop'ed out when dealing with 52-bit kernel VAs.
 *      ttbr: Value of ttbr to set, modified.
 */
        .macro  offset_ttbr1, ttbr, tmp
#ifdef CONFIG_ARM64_VA_BITS_52
        mrs_s   \tmp, SYS_ID_AA64MMFR2_EL1
        and     \tmp, \tmp, #(0xf << ID_AA64MMFR2_LVA_SHIFT)
        cbnz    \tmp, .Lskipoffs_\@
        orr     \ttbr, \ttbr, #TTBR1_BADDR_4852_OFFSET
.Lskipoffs_\@ :
#endif
        .endm

커널 이미지를 VA=52로 사용하는데 LVA(VA=52 support)가 지원되지 않는 시스템의 경우 어쩔 수 없이 VA=48로 운영해야하는데 PGD 테이블 위치를 offset(0x1E00) 만큼 더한 주소를 적용시킨다.

  • 이렇게 더한 주소를 사용하는 경우 리눅스의 pgd_index()등의 함수가 유저 페이지 테이블이든 커널 페이지 테이블이든 1개의 API로 통일하여 사용할 수 있는 잇점이 있다.
  • ttbr offset

 

다음 그림은 64K 페이지, 3 단계 페이지 테이블을 사용하는 시스템에서 VA=48 및 VA=52 구성인 경우 각 페이지 테이블에 사용하는 엔트리 수를 보여준다.

  • VA_BITS=48
    • PGDIR_SHIFT=42
    • PTRS_PER_PGD=64
  • VA_BITS=52
    • PGDIR_SHIFT=42
    • PTRS_PER_PGD=1024

 

다음 그림은 64K, 3 단계 페이지 테이블을 사용하는 시스템의 3 가지 시스템 구성에서 pgd_index()를 공통적으로 사용할 수 있도록 일부 구성에서 offset이 적용된 모습을 보여준다.

 

초기 커널 페이지 테이블 생성

clear_page_tables()

arch/arm64/kernel/head.S

SYM_FUNC_START_LOCAL(clear_page_tables)
        /*
         * Clear the init page tables.
         */
        adrp    x0, init_pg_dir
        adrp    x1, init_pg_end
        sub     x2, x1, x0
        mov     x1, xzr
        b       __pi_memset                     // tail call
SYM_FUNC_END(clear_page_tables)

init_pg_dir 페이지테이블 내용을 0 값으로 클리어한다.

 

create_kernel_mapping()

arch/arm64/kernel/head.S

SYM_FUNC_START_LOCAL(create_kernel_mapping)
        adrp    x0, init_pg_dir
        mov_q   x5, KIMAGE_VADDR                // compile time __va(_text)
#ifdef CONFIG_RELOCATABLE
        add     x5, x5, x23                     // add KASLR displacement
#endif
 
        adrp    x6, _end                        // runtime __pa(_end)
        adrp    x3, _text                       // runtime __pa(_text)
        sub     x6, x6, x3                      // _end - _text
        add     x6, x6, x5                      // runtime __va(_end)
        mov     x7, SWAPPER_RW_MMUFLAGS

        map_memory x0, x1, x5, x6, x7, x3, (VA_BITS - PGDIR_SHIFT), x10, x11, x12, x13, x14

        dsb     ishst                           // sync with page table walker
        ret
SYM_FUNC_END(create_kernel_mapping)

1init_pg_dir 테이블의 랜덤 주소 적용된 커널 가상 주소(__va(texst) + random offset)에 해당하는 엔트리들에 커널 이미지의 물리 주소 _text ~ _end 까지를 R/W 속성으로 매핑한다.

 

다음 그림은 KASLR 옵션에 따라 커널 이미지가 위치할 랜덤 가상 주소가 결정된 후 init_pg_dir 페이지 테이블에 R/W 속성으로 매핑하는 모습을 보여준다.

 


재배치 엔트리 리로케이션

__relocate_kernel()

arch/arm64/kernel/head.S -1/2-

#ifdef CONFIG_RELOCATABLE
SYM_FUNC_START_LOCAL(__relocate_kernel)
        /*
         * Iterate over each entry in the relocation table, and apply the
         * relocations in place.
         */
        adr_l   x9, __rela_start
        adr_l   x10, __rela_end
        mov_q   x11, KIMAGE_VADDR               // default virtual offset
        add     x11, x11, x23                   // actual virtual offset

0:      cmp     x9, x10
        b.hs    1f
        ldp     x12, x13, [x9], #24
        ldr     x14, [x9, #-8]
        cmp     w13, #R_AARCH64_RELATIVE
        b.ne    0b
        add     x14, x14, x23                   // relocate
        str     x14, [x12, x23]
        b       0b

커널 코드의 재배치가 일어나는 경우 재배치 정보를 담고 있는 .rela.dyn 섹션에 위치한 엔트리 정보들을 사용해 엔트리가 상대 주소(#R_AARCH64_RELATIVE)를 사용하는 타입인 경우 이들이 가리키는 주소의 값을 변경된 offset 만큼 추가하여 변경한다.

  • 코드 라인 7~8에서 x9 레지스터에 __rela_start(.relr.dyn 섹션) 주소와 x10 레지스터에 __rela_end 주소를 대입한다.
  • 코드 라인 9~10에서 컴파일 타임에 생성된 커널 이미지의 시작 가상 주소를 x11에 대입하고, virtual offset을 더한 실제 커널 가상 주소를 x11에 대입한다.
  • 코드 라인 12~13에서 0: 레이블이다. 루프 비교 조건을 수행한다. x9 레지스터 값이 끝 주소 이상이면 리로케이션이 모두 완료되었으므로 1: 레이블로 전진하여 함수를 빠져나간다.
  • 코드 라인 14~15에서 24바이트로 구성된 다이나믹 리로케이션 엔트리를 읽어온다.
    • x9주소의 16바이트를 Offset(x12)와 인포타입(x13) 레지스터로 읽고, 다음 엔트리를 위해 x9 주소에 #24를 더 한다. 그리고 x9  – 8 주소 위치의 값을 Addend 값(x14) 레지스터로 읽는다.
    • ldp x12, x13, [x9], #24 의 경우 post-indexed 어드레싱을 사용했다.
    • ldr x14, [x9, #-8]의 경우 offset 어드레싱을 사용했다.
  • 코드 라인 16~17에서 인포 타입(w13) 레지스터의 값이 #R_AARCH64_RELATIVE가 아닌 경우 skip 하고 다시 0 레이블로 반복한다.
  • 코드 라인 18~20에서 다이나믹 리로케이션 엔트리의 Offset(x12) + relocation offset(x23)가 가리키는 주소에 엔트리의 Addend 값(x14) + relocation offset(x23) 값으로 갱신하고 0레이블로 이동하여 반복한다.

 

다음 그림은 하나의 R_AARCH64_RELATIVE 엔트리의 offset + KASLR offset이 가리키는 주소의 8바이트 값을 addend 값과 KASLR offset이 더한 값으로 교체하는 과정을 보여준다.

 

arch/arm64/kernel/head.S -2/2-

1:
#ifdef CONFIG_RELR
        /*
         * Apply RELR relocations.
         *
         * RELR is a compressed format for storing relative relocations. The
         * encoded sequence of entries looks like:
         * [ AAAAAAAA BBBBBBB1 BBBBBBB1 ... AAAAAAAA BBBBBB1 ... ]
         *
         * i.e. start with an address, followed by any number of bitmaps. The
         * address entry encodes 1 relocation. The subsequent bitmap entries
         * encode up to 63 relocations each, at subsequent offsets following
         * the last address entry.
         *
         * The bitmap entries must have 1 in the least significant bit. The
         * assumption here is that an address cannot have 1 in lsb. Odd
         * addresses are not supported. Any odd addresses are stored in the RELA
         * section, which is handled above.
         *
         * Excluding the least significant bit in the bitmap, each non-zero
         * bit in the bitmap represents a relocation to be applied to
         * a corresponding machine word that follows the base address
         * word. The second least significant bit represents the machine
         * word immediately following the initial address, and each bit
         * that follows represents the next word, in linear order. As such,
         * a single bitmap can encode up to 63 relocations in a 64-bit object.
         *
         * In this implementation we store the address of the next RELR table
         * entry in x9, the address being relocated by the current address or
         * bitmap entry in x13 and the address being relocated by the current
         * bit in x14.
         */
        adr_l   x9, __relr_start
        adr_l   x10, __relr_end

2:      cmp     x9, x10
        b.hs    7f
        ldr     x11, [x9], #8
        tbnz    x11, #0, 3f                     // branch to handle bitmaps
        add     x13, x11, x23
        ldr     x12, [x13]                      // relocate address entry
        add     x12, x12, x23
        str     x12, [x13], #8                  // adjust to start of bitmap
        b       2b

3:      mov     x14, x13
4:      lsr     x11, x11, #1
        cbz     x11, 6f
        tbz     x11, #0, 5f                     // skip bit if not set
        ldr     x12, [x14]                      // relocate bit
        add     x12, x12, x23
        str     x12, [x14]

5:      add     x14, x14, #8                    // move to next bit's address
        b       4b

6:      /*
         * Move to the next bitmap's address. 8 is the word size, and 63 is the
         * number of significant bits in a bitmap entry.
         */
        add     x13, x13, #(8 * 63)
        b       2b

7:
#endif
        ret

SYM_FUNC_END(__relocate_kernel)
#endif
  • CONFIG_RELR 커널 옵션을 사용하는 경우 커널 이미지의 크기를 줄일 수 있는 RELR 재배치 기능을 지원한다.

 

.rela.dyn 섹션

먼저 .rela.dyn 섹션에서 offset 값을 알아본다.

$ readelf -S vmlinux
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .head.text        PROGBITS         ffff000010080000  00010000
       0000000000001000  0000000000000000  AX       0     0     4096
  [ 2] .text             PROGBITS         ffff000010081000  00011000
       0000000000a9b7e8  0000000000000008  AX       0     0     2048
(...생략...)
  [16] .init.data        PROGBITS         ffff000011136000  010c6000
       000000000008e5f0  0000000000000000  WA       0     0     256
  [17] .data..percpu     PROGBITS         ffff0000111c5000  01155000
       000000000000db18  0000000000000000  WA       0     0     64
  [18] .rela.dyn         RELA             ffff0000111d2b18  01162b18
       00000000003231a8  0000000000000018   A       0     0     8
  [19] .data             PROGBITS         ffff000011500000  01490000
       000000000017e240  0000000000000000  WA       0     0     4096
(...생략...) 

 

다음과 같이 .relay.dyn 섹션에 위치한 137,063개의 리로케이션 엔트리들을 볼 수 있다.

  • 다이나믹 리로케이션 엔트리당 24 바이트이며, 다음과 같이 구성되어 있다.
    • Offset 주소(8 바이트) + Info(8 바이트) + Addend 값(8 바이트)
      • Info는 심볼 인덱스(4 바이트) + 타입(4 바이트)으로 구성되어 있다.
$ readelf -r vmlinux
Relocation section '.rela.dyn' at offset 0x1162b18 contains 137063 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
ffff0000100aed68  000000000403 R_AARCH64_RELATIV                    -ffffeff4e7f4
ffff0000100aed70  000000000403 R_AARCH64_RELATIV                    -ffffeff4e7dc
ffff0000100fbbc8  000000000403 R_AARCH64_RELATIV                    -ffffeef9da40
ffff00001015e658  000000000403 R_AARCH64_RELATIV                    -ffffef24c520
ffff00001015e660  000000000403 R_AARCH64_RELATIV                    -ffffef107fa8
(...생략...)

 

위의 엔트리들의 실제 덤프 값을 확인해본다.

  • 엔트리 하나 당 24 바이트임을 알 수 있다.
$ xxd -s 0x01162b18 -l 0x78 -g 8 -e vmlinux
01162b18: ffff0000100aed68 0000000000000403  h...............
01162b28: ffff0000100b180c ffff0000100aed70  ........p.......
01162b38: 0000000000000403 ffff0000100b1824  ........$.......
01162b48: ffff0000100fbbc8 0000000000000403  ................
01162b58: ffff0000110625c0 ffff00001015e658  .%......X.......
01162b68: 0000000000000403 ffff000010db3ae0  .........:......
01162b78: ffff00001015e660 0000000000000403  `...............
01162b88: ffff000010ef8058                   X.......

 


부트 CPU MMU 스위치 후

__primary_switched:

arch/arm64/kernel/head.S

/*
 * The following fragment of code is executed with the MMU enabled.
 *
 *   x0 = __pa(KERNEL_START)
 */
SYM_FUNC_START_LOCAL(__primary_switched)
        adr_l   x4, init_task
        init_cpu_task x4, x5, x6

        adr_l   x8, vectors                     // load VBAR_EL1 with virtual
        msr     vbar_el1, x8                    // vector table address
        isb

        stp     x29, x30, [sp, #-16]!
        mov     x29, sp

        str_l   x21, __fdt_pointer, x5          // Save FDT pointer

        ldr_l   x4, kimage_vaddr                // Save the offset between
        sub     x4, x4, x0                      // the kernel virtual and
        str_l   x4, kimage_voffset, x5          // physical mappings

        mov     x0, x20
        bl      set_cpu_boot_mode_flag

        // Clear BSS
        adr_l   x0, __bss_start
        mov     x1, xzr
        adr_l   x2, __bss_stop
        sub     x2, x2, x0
        bl      __pi_memset
        dsb     ishst                           // Make zero page visible to PTW

#if VA_BITS > 48
        adr_l   x8, vabits_actual               // Set this early so KASAN early init
        str     x25, [x8]                       // ... observes the correct value
        dc      civac, x8                       // Make visible to booting secondaries
#endif

#ifdef CONFIG_RANDOMIZE_BASE
        adrp    x5, memstart_offset_seed        // Save KASLR linear map seed
        strh    w24, [x5, :lo12:memstart_offset_seed]
#endif
#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
        bl      kasan_early_init
#endif
        mov     x0, x21                         // pass FDT address in x0
        bl      early_fdt_map                   // Try mapping the FDT early
        mov     x0, x20                         // pass the full boot status
        bl      init_feature_override           // Parse cpu feature overrides
        mov     x0, x20
        bl      finalise_el2                    // Prefer VHE if possible
        ldp     x29, x30, [sp], #16
        bl      start_kernel
        ASM_BUG()
SYM_FUNC_END(__primary_switched)

MMU를 켠 후 동작하는 코드로 커널용 스택과 벡터 포인터를 지정하고 BSS 영역을 클리어한 후 start_kernel() 함수로 점프한다.

  • 코드 라인 2~3에서 최초 커널용 태스크인 init_task의 주소를 SP_EL0에 저장하고 , 초기 커널 스택 위치를 SP 레지스터에 대입하는 등 초기화를 수행한다.
    • sp_el0는 유저 공간으로 context switch 된 후 유저용 스택 위치를 가리키는 용도로 사용된다.
    • 그러나 커널(el1)에서는 사용하지 않는 스크래치 레지스터 용도일 뿐이므로 이를 활용하여 thread_info를 가리키는 레지스터로 사용한다.
  • 코드 라인 5~7에서 vbar_el1 레지스터에 vector 위치를 지정한 후 isb를 수행하여 이후 실행되는 명령이 isb 전에 변경한 컨텍스트가 적용되어 동작하도록 한다.
  • 코드 라인 9~10에서 x29 및 x30(lr) 레지스터 내용을 스택에 보관하고, x29 레지스터를 sp 레지스터에 대입한다.
    • stp x29, x30, [sp, #-16]! 는 pre-index 방식의 어드레싱 모드를 사용하고 있다. 따라서 sp += -16을 먼저 연산한 후 sp가 가리키는 주소에 x29와 x30 레지스터를 저장한다.
  • 코드 라인 12에서 fdt 시작 물리 주소를 담고 있는 x21 레지스터를 변수 __fdt_pointer에 저장한다.
  • 코드 라인 14~16에서 커널 시작 가상 주소(x4)에서 커널 시작 물리 주소(x0=__PHYS_OFFSET)를 뺀 offset을 변수 kimage_voffset에 저장한다.
    • x0에는 이 루틴이 호출되기 전에 __PHYS_OFFSET이 담겨 호출된다.
    • 예) kimage_vaddr=0xffff_0000_1000_0000, __PHYS_OFFSET=0x4000_0000
      • kimage_voffset=0xfffe_ffff_d000_0000
  • 코드 라인 18~19에서 커널 부팅 시 boot 모드 값(EL1 또는 EL2 여부)를 저장한다.
  • 코드 라인 22~27에서 BSS 영역을 0으로 모두 클리어한 후 기록된 0 값이 다른 inner-share 영역의 cpu들이 볼 수 있도록 반영한다.
  • 코드 라인 29~33에서 VA=52를 사용하는 커널인 경우 vabits_actual 값을 읽어 x25에 대입한다. MMU가 on된 이후 vabits_actual에 저장된 값이 캐시 라인에 있을 수 있으므로 이 값이 다른 cpu에서도 메모리를 통해 정확히 읽힐 수 있도록 캐시라인을 clean & invalidate 한다.
  • 코드 라인 35~38에서 CONFIG_RANDOMIZE_BASE 커널 옵션을 사용한 경우 memstart_offset_seed 변수에 저장된 값을 w24에 대입한다.
  • 코드 라인 39~41에서 KASAN 초기화를 수행한다. (KASAN 디버그 생략)
  • 코드 라인 42~43에서 FDT 물리 주소를 인자로 전달하여 FDT를 fixmap 가상 공간을 이용하여 매핑한다.
  • 코드 라인 44~45에서 init_feature_override() 함수를 호출하여 몇 개 레지스터들(mmfr1, &pfr0, …)의 overide 영역의 캐시를 PoC 영역까지 clean & invalidate 한다.
  • 코드 라인 46~47에서 EL2 모드의 사용을 위해 마지막 준비를 수행한다. (생략)
  • 코드 라인 48~49에서 조금 전에 백업해 두었던 x29와 x30(lr) 레지스터를 원위치시키고, start_kernel() 함수로 점프한다.

 

init_cpu_task 매크로

arch/arm64/kernel/head.S

.       /*
         * Initialize CPU registers with task-specific and cpu-specific context.
         *
         * Create a final frame record at task_pt_regs(current)->stackframe, so
         * that the unwinder can identify the final frame record of any task by
         * its location in the task stack. We reserve the entire pt_regs space
         * for consistency with user tasks and kthreads.
         */
.       .macro  init_cpu_task tsk, tmp1, tmp2
        msr     sp_el0, \tsk

        ldr     \tmp1, [\tsk, #TSK_STACK]
        add     sp, \tmp1, #THREAD_SIZE
        sub     sp, sp, #PT_REGS_SIZE

        stp     xzr, xzr, [sp, #S_STACKFRAME]
        add     x29, sp, #S_STACKFRAME

        scs_load \tsk

        adr_l   \tmp1, __per_cpu_offset
        ldr     w\tmp2, [\tsk, #TSK_TI_CPU]
        ldr     \tmp1, [\tmp1, \tmp2, lsl #3]
        set_this_cpu_offset \tmp1
        .endm

태스크 @tsk의 주소를 SP_EL0에 저장하고 , @tsk->stack 위치를 SP 레지스터에 대입하는 등 초기화를 수행한다.

커널 태스크와 관련 스택들을 초기화한다.

  • 코드 라인 2에서 MMU를 enable 한 후에는 먼저 유저용 스택 레지스터인 sp_el0에 active task인 @tsk를 대입해야 한다.
  • 코드 라인 4~5에서 또한 현재 스택(sp) 레지스터에는 active task @tsk의 stack을 대입해야 한다.
    • @tsk->stack이 가리키는 스택 주소에서 + THREAD_SIZE(스택 사이즈)를 가리켜 빈 스택을 가리키게 한다.
    • arm/arm64 아키텍처는 top-down 방식의 스택을 사용하므로 스택의 가장 높은 위치로 스택포인터를 지정한다.
    • init_task.stack이 가리키는 주소는 &init_stack 이다.
  • 코드 라인 6에서 스택에 PT_REGS_SIZE(pt_regs 구조체 크기)를 확보한다.
  • 코드 라인 8~9에서 확보한 pt_regs->stackframe[2]을 0으로 클리어하고, x29 레지스터에 이 위치를 저장해 둔다.
  • 코드 라인 11에서 CONFIG_SHADOW_CALL_STACK 커널 옵션을 사용하는 경우에만 @tsk->thread_info.scs_sp에 저장된 값을 임시로 x18 레지스터에 로드해둔다.
    • CONFIG_SHADOW_CALL_STACK
      • Shadow Call Stack은 스택을 사본으로 복제한 후 Stack buffer overflow 공격을 막기 위해 사용된다.
        • 예) 사본에 저장된 리턴 주소(return address)가 변경되어 오염된 경우 이를 막기 위해 사용된다.
        • 참고: Shadow Stack | WIKIPEDIA
  • 코드 라인 13~16에서 현재 cpu에 대한 per-cpu offset을 tpidr 레지스터에 저장한다.
    • boot cpu의 offset은 0이다.

 

set_this_cpu_offset 매크로

arch/arm64/include/asm/assembler.h

        .macro  set_this_cpu_offset, src
alternative_if_not ARM64_HAS_VIRT_HOST_EXTN
        msr     tpidr_el1, \src
alternative_else
        msr     tpidr_el2, \src
alternative_endif
        .endm

TPIDR_EL1 레지스터에 @src를 저장한ㄷ. 이 레지스터는 리눅스 커널에서 per-cpu 자료구조의 cpu별 offset을 담아 사용한다.

  • 코드 라인 2~3에서 VHE 기능이 없는 경우 tpidr_el1 레지스터에 @src를 저장한다.
  • 코드 라인 4~6에서 VHE 기능이 있는 경우 코드에서 tpidr_el2에 액세스하여도 실제로는 tpidr_el1에 액세스한다.

 

__primary_switch에서 __primary_switched로 전환

다음 3개의 영역별로 섹션과 매핑 그리고 pc에 대한 관계를 살펴본다.

  • primary_entry
    • 섹션: .init.text
    • 매핑: 없음
    • pc: 물리 주소에서 시작 (예: 0x416b_0000)
  • __primary_switch
    • 섹션: .idmap
    • 매핑: .idmap 섹션 코드들을 idmap_pg_dir 페이지 테이블에 va=pa 1:1 매핑
    • pc: 물리주소를 그대로 가상주소로 사용 (예: 0x40f7_8310)
  • __primary_switched
    • 섹션: .init.text
    • 매핑: .text 섹션 코드 및 데이터를 init_pg_dir 페이지 테이블 사용하여 매핑
    • pc: 커널 가상 주소가 본격적으로 사용 (예: 0xffff_8000_114b_0330)

 

근거리 및 원거리 점프 방법에 대해 알아본다.

  • 근거리 점프를 위해서는 pc + relative offset 방식을 사용하는 b 및 bl 명령들을 사용할 수 있다.
  • 원거리 점프를 위해서는 레지스터를 이용한 br 및 blr 명령을 사용할 수 있다.
    • 예) ldr <Rd>, =<label>과 같은 특수한 pesudo instruction을 사용
      • 이 명령은 실제 존재하는 명령이 아니라 매크로와 같은 명령으로 컴파일러가 12바이트의 코드를 만들어낸다.
      • 4바이트는 ldr <Rd>, <코드 인근 주소 레이블>과 같은 명령을 사용하고,
      • 8바이트에는 컴파일 타임에 <label>에 대한 주소를 저장한다.
      • 결국 컴파일 타임에 저장해둔 8바이트 주소를 레지스터에 읽어들이는 코드를 생성한다.

 

__primary_switch에서 pc가 0x4xxx_xxxx 주소를 사용할 때에는 TTBR0_EL1과 연결된 idmap_pg_dir을 사용하다, __primary_switched에 해당하는 0xffff_8000_1xxx_xxxx 주소를 사용할 때에는 TTBR1_EL1에 연결된 init_pg_dir 페이지 테이블을 사용하는 식으로 자연스럽게 전환되므로 page fault는 발생하지 않는다.

 

다음은 디버거를 이용하여 런타임에 사용된 주소들을 보여준다.

_head:                                               ; KERNEL_START
   0x0000000040200000:  add     x13, x18, #0x16      ; 
   0x0000000040200004:  b       0x416b0000           ; primary_entry

primary_entry:
   0x00000000416b0000:  bl      0x416b0020           ; preserve_boot_args
   0x00000000416b0004:  bl      0x40f78000           ; el2_setup
   0x00000000416b0008:  adrp    x23, 0x40200000    
   0x00000000416b000c:  and     x23, x23, #0x1fffff
   0x00000000416b0010:  bl      0x40f78180           ; set_cpu_boot_mode_flag
   0x00000000416b0014:  bl      0x416b0040           ; __create_page_tables
   0x00000000416b0018:  bl      0x40f7860c           ; __cpu_setup
   0x00000000416b001c:  b       0x40f78310           ; __primary_switch


__primary_switch:
   0x0000000040f78310:  adrp    x1, 0x41fbb000      
   0x0000000040f78314:  bl      0x40f78248           ; __enable_mmu
   0x0000000040f78318:  bl      0x40f782c8           ; __relocate_kernel
   0x0000000040f7831c:  ldr     x8, 0x40f78338       ; =__primary_switched
   0x0000000040f78320:  adrp    x0, 0x40200000       ; __PHYS_OFFSET
   0x0000000040f78324:  br      x8
   ...
   0x0000000040f78338:  .inst   0x114b0330      
   0x0000000040f78338:  .inst   0xffff8000

__primary_switched:
   0xffff8000114b0330 <+0>:     adrp    x4, 0xffff800011a70000
   0xffff8000114b0334 <+4>:     add     sp, x4, #0x4, lsl #12
   0xffff8000114b0338 <+8>:     adrp    x5, 0xffff800011a83000 <envp_init+104>
   0xffff8000114b033c <+12>:    add     x5, x5, #0x300
   0xffff8000114b0340 <+16>:    msr     sp_el0, x5
   0xffff8000114b0344 <+20>:    adrp    x8, 0xffff800010010000 <dw_apb_ictl_handle_irq>
   0xffff8000114b0348 <+24>:    add     x8, x8, #0x800
   0xffff8000114b034c <+28>:    msr     vbar_el1, x8
   0xffff8000114b0350 <+32>:    isb
   0xffff8000114b0354 <+36>:    stp     xzr, x30, [sp, #-16]!
   0xffff8000114b0358 <+40>:    mov     x29, sp
   0xffff8000114b035c <+44>:    adrp    x5, 0xffff800011561000 <tmp_cmdline.61939+2040>
   0xffff8000114b0360 <+48>:    str     x21, [x5, #904]
   0xffff8000114b0364 <+52>:    adrp    x4, 0xffff800010d80000 <kimage_vaddr>
   0xffff8000114b0368 <+56>:    ldr     x4, [x4]
   0xffff8000114b036c <+60>:    sub     x4, x4, x0
   0xffff8000114b0370 <+64>:    adrp    x5, 0xffff80001143e000
   0xffff8000114b0374 <+68>:    str     x4, [x5, #3888]
   0xffff8000114b0378 <+72>:    adrp    x0, 0xffff800011d2c000 <__boot_cpu_mode>
   0xffff8000114b037c <+76>:    add     x0, x0, #0xa00
   0xffff8000114b0380 <+80>:    mov     x1, xzr
   0xffff8000114b0384 <+84>:    adrp    x2, 0xffff800011dba000 <write_buf.76616+30304>
   0xffff8000114b0388 <+88>:    add     x2, x2, #0xd3c
   0xffff8000114b038c <+92>:    sub     x2, x2, x0
   0xffff8000114b0390 <+96>:    bl      0xffff800010477840 <memset>
   0xffff8000114b0394 <+100>:   dsb     ishst
   0xffff8000114b0398 <+104>:   add     sp, sp, #0x10
   0xffff8000114b039c <+108>:   mov     x29, #0x0                       // #0
   0xffff8000114b03a0 <+112>:   mov     x30, #0x0                       // #0
   0xffff8000114b03a4 <+116>:   b       0xffff8000114b0a3c <start_kernel>

 

kimage_vaddr & kimage_voffset

kimage_vaddr 변수

arch/arm64/kernel/head.S

        .pushsection ".rodata", "a"
SYM_DATA_START(kimage_vaddr)
        .quad           _text
SYM_DATA_END(kimage_vaddr)
EXPORT_SYMBOL(kimage_vaddr)
        .popsection

kimage_vaddr은 MMU 상태와 관계없이 근거리내에서 해당 레이블을 읽어야 하므로 .idmap.text 섹션에 위치해 있고, 컴파일 타임에 커널 시작 가상 주소 _text의 주소가 저장되어 있다. 단 랜더마이즈에 의해 리로케이션이 진행되면 이 심볼 값도 랜더마이즈된 커널 이미지 시작 주소로 변경된다.

 

kimage_voffset 변수

arch/arm64/mm/mmu.c

u64 kimage_voffset __ro_after_init;
EXPORT_SYMBOL(kimage_voffset);

부트업 타임에 다음과 같은 값으로 저장된 후 읽기 전용으로 사용된다.

  • kimage_voffset = kimage_vaddr – 커널 물리 시작 주소

 

다음 그림은 이미지의 가상 주소에서 물리 주소의 차이를 kimage_voffset 값에 담았고, 이 값을 통해 이미지의 가상 주소와 물리 주소의 변환 API에 활용되는 모습을 보여준다.

 

kimage_voffset 저장 과정

primary_switched 내부에는 아래와 같이 kimage_voffset를 저장하는 루틴이 있다.

  • ldr_l x4, kimage_vaddr
    • kimage_vaddr에는 리로케이션된 커널 시작 가상 주소가 담긴다.
  • sub x4, x4, x0
    • x0에는 런타임 커널 시작 주소(1st: 0x4020_0000)가 담겨있다.
  • str_l x4, kimage_voffset, x5
    • kimage_voffset = 랜더마이즈한 커널 시작 주소 – 물리 시작 주소 값

 

kimage_voffset 값은 커널 이미지의 가상 주소 vs 물리 주소와의 변환에 다음 API를 통해 사용한다.

  • __kimg_to_phys()
  • __phys_to_kimg()

 

__kimg_to_phys()

arch/arm64/include/asm/memory.h

#define __kimg_to_phys(addr) ((addr) - kimage_voffset)

커널 이미지 가상 주소를 커널 이미지 물리 주소로 변환하여 알아온다.

 

__phys_to_kimg()

arch/arm64/include/asm/memory.h

#define __phys_to_kimg(x)       ((unsigned long)((x) + kimage_voffset))

커널 이미지 물리 주소를 커널 이미지 가상 주소로 변환하여 알아온다.

 

커널 스택 크기

arch/arm64/include/asm/memory.h

#define THREAD_SIZE             (UL(1) << THREAD_SHIFT)

커널 스택 사이즈는 디폴트 커널 설정(4K 페이지)에서 16K를 사용한다.

 

arch/arm64/include/asm/memory.h

/*
 * VMAP'd stacks are allocated at page granularity, so we must ensure that such
 * stacks are a multiple of page size.
 */
#if defined(CONFIG_VMAP_STACK) && (MIN_THREAD_SHIFT < PAGE_SHIFT)
#define THREAD_SHIFT            PAGE_SHIFT
#else
#define THREAD_SHIFT            MIN_THREAD_SHIFT
#endif

페이지 사이즈로 64K로 선택한 경우 vmap(디폴트)을 사용한 커널 스택은 1 개의 64K 페이지를 사용한다. 그렇지 않은 경우 MIN_THREAD_SHIFT 단위의 커널 스택을 사용한다.

 

arch/arm64/include/asm/memory.h

#define MIN_THREAD_SHIFT        (14 + KASAN_THREAD_SHIFT)

커널 스택 최소 단위는 16K이며 KASAN을 사용하는 경우 32K를 사용하고, KASAN Extra를 사용하는 경우 64K를 사용한다.

 

부트 cpu 모드 저장

set_cpu_boot_mode_flag:

arch/arm64/kernel/head.S

/*
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in w0. See arch/arm64/include/asm/virt.h for more info.
 */
SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
        adr_l   x1, __boot_cpu_mode
        cmp     w0, #BOOT_CPU_MODE_EL2
        b.ne    1f
        add     x1, x1, #4
1:      str     w0, [x1]                        // Save CPU boot mode
        ret
SYM_FUNC_END(set_cpu_boot_mode_flag)

커널 부트 진입 시 cpu 모드(el1 ~ el2)를 파악하여 변수 __boot_cpu_mode[0~1]에 저장한다.

  • 코드 라인 2~7에 첫 번째 인자 w0 값이 el2 모드가 아닌 경우 w0를 __boot_cpu_mode[0]에 저장하고, el2 모드로 부팅한 경우 __boot_cpu_mode[1]에 w0를 저장한다.

 

__boot_cpu_mode[]

arch/arm64/include/asm/virt.h

/*
 * __boot_cpu_mode records what mode CPUs were booted in.
 * A correctly-implemented bootloader must start all CPUs in the same mode:
 * In this case, both 32bit halves of __boot_cpu_mode will contain the
 * same value (either 0 if booted in EL1, BOOT_CPU_MODE_EL2 if booted in EL2).
 *
 * Should the bootloader fail to do this, the two values will be different.
 * This allows the kernel to flag an error when the secondaries have come up.
 */

arch/arm64/mm/mmu.c

u32 __boot_cpu_mode[] = { BOOT_CPU_MODE_EL2, BOOT_CPU_MODE_EL1 };

__boot_cpu_mode[]의 초기 값은 다음과 같이 두 개 값이 담겨있다.

  • BOOT_CPU_MODE_EL2=0xe12
  • BOOT_CPU_MODE_EL1=0xe11

 


Secondary CPU 부팅

secondary_entry:

arch/arm64/kernel/head.S

        /*
         * Secondary entry point that jumps straight into the kernel. Only to
         * be used where CPUs are brought online dynamically by the kernel.
         */
SYM_FUNC_START(secondary_entry)
        bl      init_kernel_el                  // w0=cpu_boot_mode
        b       secondary_startup
SYM_FUNC_END(secondary_entry)

부트 cpu를 제외한 나머지 cpu들이 깨어날 때 수행될 루틴들이다. 하이퍼 바이저 설정 코드를 수행하고, 부트 cpu 모드를 저장한 후 secondary_startup 레이블로 이동하여 계속 처리한다.

  • 잠들어 있는 cpu들은 wfe 동작과 같은 상태로 클럭이 멈춰있는 상태이고 부트가 되어야 할지 여부가 기록된 스핀 테이블 내용이 변경되지 않는 한 루프를 돌며 다시 wfe 상태가 된다.

 

secondary_startup:

arch/arm64/kernel/head.S

SYM_FUNC_START_LOCAL(secondary_startup)
        /*
         * Common entry point for secondary CPUs.
         */
        mov     x20, x0                         // preserve boot mode
        bl      finalise_el2
        bl      __cpu_secondary_check52bitva
#if VA_BITS > 48
        ldr_l   x0, vabits_actual
#endif
        bl      __cpu_setup                     // initialise processor
        adrp    x1, swapper_pg_dir
        adrp    x2, idmap_pg_dir
        bl      __enable_mmu
        ldr     x8, =__secondary_switched
        br      x8
SYM_FUNC_END(secondary_startup)

프로세서를 초기화하고 MMU를 켠 후 __secondary_switched: 루틴으로 점프한다.

  • 코드 라인 4~5에서 x20에 x0 레지스터를 보관한 후 EL2 모드의 사용을 위해 마지막 준비를 수행한다. (생략)
  • 코드 라인 6에서 부트 cpu가 52bit 유저 가상 주소를 사용한 경우 secondary cpu가 이를 지원하는지 여부를 체크하는데, 지원하지 않는 경우 stuck한다. 이 함수를 빠져나오면 x0에 시스템이 지원하는 vabits_actual 비트가 담겨있다.
  • 코드 라인 7~9에서 52비트 VA를 지원하는 커널인 경우 시스템이 실제 운영가능한  vabits_actual(48 or 52) 값을 x0에 대입한다.
  • 코드 라인 10에서 MMU를 켜기 위해 현재 cpu를 초기화한다. (x0 레지스터에 vabits_actual 값이 지정되며, 단 VA_BITS > 48 이상에서만 유효하다)
  • 코드 라인 11~13에서 TTBR1 레지스터가 swapper_pg_dir을 가리키고, TTBR0 레지스터가 idmap_pg_dir을 가리키게한 후 MMU를 켠다.
    • MMU가 enable하는 순간에는 물리 주소에서 PC(Program Counter)가 동작하고 있었기 때문에 처음에는 idmap_pg_dir을 기반으로 동작하게 된다.
  • 코드 라인 14~15에서 __secondary_switched 루틴으로 점프한다.

 

__cpu_secondary_check52bitva:

arch/arm64/kernel/head.S

SYM_FUNC_START(__cpu_secondary_check52bitva)
#if VA_BITS > 48
        ldr_l   x0, vabits_actual
        cmp     x0, #52
        b.ne    2f

        mrs_s   x0, SYS_ID_AA64MMFR2_EL1
        and     x0, x0, #(0xf << ID_AA64MMFR2_LVA_SHIFT)
        cbnz    x0, 2f

        update_early_cpu_boot_status \
                CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_52_BIT_VA, x0, x1
1:      wfe
        wfi
        b       1b

#endif
2:      ret
SYM_FUNC_END(__cpu_secondary_check52bitva)

secondary cpu의 52 비트 가상 주소 지원 여부를 체크한다. 지원하지 않는 cpu는 부팅되지 않고 stuck한다.

  • 코드 라인 3~5에서 부트 cpu에서 저장한 변수 vabits_actual에 담긴 값이 52가 아니면 함수를 빠져나간다.
  • 코드 라인 7~9에서 mmfr2_el1 레지스터의 VARange 필드 값이 1이면 유저 가상 주소로 52비트를 지원하는 것이므로 함수를 빠져나간다.
  • 코드 라인 11~15에서 52bit를 지원하지 않아 변수 __early_cpu_boot_status에 0x102를 저장하고 cpu가 정지(stuck)한다.

 

update_early_cpu_boot_status 매크로

arch/arm64/kernel/head.S

/*
 * The booting CPU updates the failed status @__early_cpu_boot_status,
 * with MMU turned off.
 *
 * update_early_cpu_boot_status tmp, status
 *  - Corrupts tmp1, tmp2
 *  - Writes 'status' to __early_cpu_boot_status and makes sure
 *    it is committed to memory.
 */
        .macro  update_early_cpu_boot_status status, tmp1, tmp2
        mov     \tmp2, #\status
        adr_l   \tmp1, __early_cpu_boot_status
        str     \tmp2, [\tmp1]
        dmb     sy
        dc      ivac, \tmp1                     // Invalidate potentially stale cache line
        .endm

변수 __early_cpu_boot_status에 부트 상태 @status를 저장한다.

  • @tmp1과 @tmp2에는 파괴되도 상관이 없는 임시 레지스터를 지정한다.
  • 주석 내용에서 인자가 잘못되어 있음을 확인할 수 있다.

 

__secondary_switched:

arch/arm64/kernel/head.S

SYM_FUNC_START_LOCAL(__secondary_switched)
        mov     x0, x20
        bl      set_cpu_boot_mode_flag
        str_l   xzr, __early_cpu_boot_status, x3
        adr_l   x5, vectors
        msr     vbar_el1, x5
        isb

        adr_l   x0, secondary_data
        ldr     x2, [x0, #CPU_BOOT_TASK]
        cbz     x2, __secondary_too_slow

        init_cpu_task x2, x1, x3

#ifdef CONFIG_ARM64_PTR_AUTH
        ptrauth_keys_init_cpu x2, x3, x4, x5
#endif

        bl      secondary_start_kernel
        ASM_BUG()
SYM_FUNC_END(__secondary_switched)

커널용 벡터 포인터와 스택을 지정한 후 C 루틴인 secondary_start_kernel() 루틴으로 점프한다.

 


CPU stuck 시 Reason 코드 확인

__early_cpu_boot_status 변수

arch/arm64/kernel/head.S

/*
 * The booting CPU updates the failed status @__early_cpu_boot_status,
 * with MMU turned off.
 */
SYM_DATA_START(__early_cpu_boot_status)
        .quad
SYM_DATA_END(__early_cpu_boot_status)

다음과 같이 하위 8비트는 cpu boot 상태를 표시하고, 상위 비트에서 stuck 이유를 담는다.

 

arch/arm64/include/asm/smp.h

/* Values for secondary_data.status */
#define CPU_STUCK_REASON_SHIFT          (8)
#define CPU_BOOT_STATUS_MASK            ((UL(1) << CPU_STUCK_REASON_SHIFT) - 1)

#define CPU_MMU_OFF                     (-1)
#define CPU_BOOT_SUCCESS                (0)
/* The cpu invoked ops->cpu_die, synchronise it with cpu_kill */
#define CPU_KILL_ME                     (1)
/* The cpu couldn't die gracefully and is looping in the kernel */
#define CPU_STUCK_IN_KERNEL             (2)
/* Fatal system error detected by secondary CPU, crash the system */
#define CPU_PANIC_KERNEL                (3)

#define CPU_STUCK_REASON_52_BIT_VA      (UL(1) << CPU_STUCK_REASON_SHIFT)
#define CPU_STUCK_REASON_NO_GRAN        (UL(2) << CPU_STUCK_REASON_SHIFT)

 

__no_granule_support:

arch/arm64/kernel/head.S

SYM_FUNC_START_LOCAL(__no_granule_support)
        /* Indicate that this CPU can't boot and is stuck in the kernel */
        update_early_cpu_boot_status \
                CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_NO_GRAN, x1, x2
1:
        wfe
        wfi
        b       1b
SYM_FUNC_END(__no_granule_support)

커널이 설정한 페이지 테이블 단위를 해당 cpu의 아키텍처가 지원하지 않아 stuck 한다.

 

참고