<kernel v5.10>
kernel/head.S – ARM64 (new for v5.10)
시스템의 부트로더로부터 커널을 주 메모리에 로드하고 최초 호출되는 지점이 head.S의 _head: 레이블이다. 이 시작 코드에는 커널 이미지의 헤더 및 UEFI PE 헤더등을 포함한다. 이 코드들은 물리 DRAM의 2M 단위로 정렬된 주소라면 어떠한 위치에 배치하여도 동작할 수 있도록 모든 코드가 position independent 코드들로 구성되어 있다.
부트로더가 하는 일 요약
커널을 로드하여 동작시키기 전까지 부트로더가 수행하는 일들은 다음과 같다.
- 주 메모리의 초기화를 수행한다.
- DTB를 주 메모리에 로드한다.
- x0 레지스터에 DTB 물리 시작 주소를 담는다.
- x1~x3 레지스터는 미래에 사용할 레지스터로 예약하였다.
- 커널 이미지를 주 메모리에 로드한다.
- (옵션) 압축된 커널 이미지(예: Image.gz)를 사용하는 경우 decompress를 수행한다.
- AArch64 커널의 경우 커널 이미지는 self-decompress를 지원하지 않는다. 따라서 부트로더가 gzip 등으로 압축을 풀어야 한다.
- 커널 이미지 헤더가 포함된 압축 풀린 커널 이미지는 2M 단위로 정렬하여야 한다.
- 커널 이미지의 첫 주소로 jump하여 커널의 head.S 루틴을 시작한다.
- 자세한 내용은 다음 문서를 참고한다.
- 참고: Booting AArch64 Linux | Kernel.org
커널 진입전 요구사항
커널 진입 전 부트로더는 다음 조건을 만족해야 한다.
- 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 장치들은 DMA 기능이 정지되어 있어야 한다.
- CPU mode
- 모든 cpu들의 PSTATE.DAIF는 모두 마스크되어야 한다. (디버그, SError, IRQ 및 FIQ의 마스크)
모든 CPU는 EL2 또는 non-secure EL1에 있어야 하고 동일해야 한다.
- 모든 cpu들의 PSTATE.DAIF는 모두 마스크되어야 한다. (디버그, SError, IRQ 및 FIQ의 마스크)
- 아키텍처 타이머
- 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 인터럽트 컨트롤러에 대해 기술되어야 한다.
- EL3 존재시
- GICv3가 v2 호환모드에서 사용될 때
- EL3 존재시
- ICC_SRE_EL3.SRE=0
- 커널이 EL1에서 동작시
- ICC_SRE_EL2.SRE=0
- Device Tree 또는 ACPI 테이블에 GICv2 인터럽트 컨트롤러에 대해 기술되어야 한다.
- EL3 존재시
- Pointer Authentication
- 설명 생략
- Activity Monitors Unit v1
- 설명 생략
- SCR_EL3.FIQ
head.S가 하는일 요약
C 함수의 첫 실행 지점인 start_kernel() 함수가 호출될 수 있도록 준비하는 과정은 다음과 같다.
- 하이퍼 바이저 모드(el2)를 사용하여 처음 호출된 경우 해당 el2 exception 벡터 준비
- 부트 cpu 초기화 및 el1 exception 벡터 준비
- 필요시 커널 가상 공간에 매핑될 커널 이미지의 위치를 랜덤하게 변경
- 커널 코드와 데이터를 대상으로 init 페이지 테이블(init_pg_dir) 생성
- 일부 mmu 전환용 코드를 대상으로 idmap 페이지 테이블(idmap_pg_dir) 생성
- 커널용 스택 준비
- MMU를 켜서 가상 주소 체제로 전환
- 마지막으로 start_kernel() jump
커널 이미지 위치
DRAM 가상 주소 위치
arch/arm64/kernel/head.S
#define __PHYS_OFFSET (KERNEL_START) #if (PAGE_OFFSET & 0x1fffff) != 0 #error PAGE_OFFSET must be at least 2MB aligned #endif
__PHYS_OFFSET는 커널이 시작 가상 주소(KERNEL_START)에 대응하는 커널의 물리 시작 주소 offset 값이고, 실제 물리 주소를 알고자 할 때 adrp 명령과 함께 사용된다.
- 2M 단위의 섹션 매핑을 위해 커널 이미지의 시작 주소를 2M로 정렬하고, pc(program counter) 레지스터를 기준으로 상대 주소를 참조하는 방법으로 poinstion independent 코드를 만들 수 있다.
- 참고: arm64: allow kernel Image to be loaded anywhere in physical memory (2016, v4.6-rc1)
KERNEL_START & KERNEL_END
arch/arm64/include/asm/memory.h
#define KERNEL_START _text #define KERNEL_END _end
커널 이미지의 코드(_text) 시작 주소가 KERNEL_START 이다. 그리고 커널 이미지의 끝이 _end로 .bss 섹션도 포함된다.
- 참고: Memory Layout on AArch64 Linux | Kernel.org
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에서 완전히 제거되었다.
- 참고:
- arm64: don’t map TEXT_OFFSET bytes below the kernel if we can avoid it (2016, v4.7-rc1)
- arm64: set TEXT_OFFSET to 0x0 in preparation for removing it entirely (2020. v5.8-rc1)
- arm64: remove TEXT_OFFSET randomization (2020, v5.8-rc2)
- arm64: get rid of TEXT_OFFSET (2020, v5.10-rc1)
다음 그림은 TEXT_OFFSET이 위치를 보여준다. (decompress 상태의 커널)
가상 공간에 커널 이미지 배치
다음 그림은 가상 주소 공간에 배치될 때의 커널 이미지 위치를 보여준다.
- 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)
- CONFIG_RANDOMIZE_BASE
Static 페이지 테이블
커널이 컴파일될 때 미리 준비되는 5개 페이지 테이블의 용도는 다음과 같다.
- init_pg_dir
- 원래 커널 페이지 테이블은 swapper 페이지 테이블만을 사용했었다. 그런데 보안 향상을 위해 swapper 페이지 테이블을 read-only로 운영하기 위해 별도로 분리하고, 커널 초기 부팅 중에만 잠시 사용하기 위해 read-write 가능한 상태로 init 페이지 테이블을 운영한다.
- 참고: arm64/mm: Separate boot-time page tables from swapper_pg_dir (2018, v4.21-rc1)
- 초기 부팅 중에만 사용되므로 매핑에 사용할 페이지 테이블의 단계와 단계별 갯수는 커널 영역(text, data, bss 섹션)에 한정하여 컴파일 타임에 계산된다.
- 정규 매핑 준비를 수행하는 paging_init() 후에 swapper_pg_dir로 전환을 수행한 후에는 이 init 페이지 테이블은 더 이상 운영하지 않으므로 할당 해제한다.
- 원래 커널 페이지 테이블은 swapper 페이지 테이블만을 사용했었다. 그런데 보안 향상을 위해 swapper 페이지 테이블을 read-only로 운영하기 위해 별도로 분리하고, 커널 초기 부팅 중에만 잠시 사용하기 위해 read-write 가능한 상태로 init 페이지 테이블을 운영한다.
- swapper_pg_dir
- 커널 부트업 과정에서 정규 매핑이 가능해지는 순간부터 swapper 페이지 테이블이 커널 페이지 테이블로 사용된다.
- 보안 향상을 위해 읽기 전용으로 매핑하여 사용하며, 매핑 변경을 위해 엔트리 값을 수정해야 하는 경우마다 잠깐씩 fixmap 가상 주소 영역에 읽고쓰기(read-write) 매핑하여 사용한다.
- 정규 매핑이 가능해지면서 사용되므로 static으로 만들어지는 pgd 테이블을 제외하곤 필요시 동적으로 생성된다.
- reserved_ttbr0
- 보안 상향을 위해 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 시 사용
다음 그림은 컴파일 타임에 static하게 만들어지는 페이지 테이블의 용도를 보여준다.
- pgd 테이블만 준비되는 항목들은 다음 단계의 페이지 테이블이 정규 매핑 준비된 경우 dynamic 하게 생성된다.
- 리눅스 커널은 이제 5단계(pgd -> p4d -> pud -> pmd -> pte) 테이블을 사용한다. 하지만 ARM64의 head.S 코드는 실제 ARM64 아키텍처가 4단계만 사용하므로 p4d 단계는 배제하고 구현되어 있다.)
다음 그림은 static 페이지 테이블들이 배치된 사례를 보여준다.
- init_pg_dir
- 4K 페이지 및 VA_BITS=48 조건에서 4 단계 페이지 테이블이 2M 블럭 매핑을 사용하면서 1단계 줄어 3단계로 구성된다.
- idmap_pg_dir
- 4K 페이지 및 PA_BITS=48 조건에서 4 단계 페이지 테이블이 2M 블럭 매핑을 사용하면서 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 비트로 확장시킬 수 있는 방법이다.
- 조건: ARMv8.2-LPA 기능을 지원하는 아키텍처에서 64K 페이지 및 3단계 페이지 테이블을 사용할 때 가능하다.
- 참고:
- arm64: allow ID map to be extended to 52 bits (2017, v4.16-rc1)
- arm64: handle 52-bit physical addresses in page table entries (2017, v4.16-rc1)
52bit 유저 공간
커널 v5.0-rc1에서 52비트 유저 공간을 지원한다. (4 Peta Bytes)
- 사용 제약
- ARMv8.2-LPA 기능을 지원하는 아키텍처
- 64K 페이지 사용
- 참고: arm64: mm: introduce 52-bit userspace support (2018, v5.0-rc1)
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과 유저 공간을 담당하는 TTBR0을 사용하여 각각의 커널 모드와 유저 모드에서 상대방의 영역을 사용하지 못하게 분리하는 방법을 보여준다.
SDEI(Software Delegated Exception Interface)
펌웨어(Secure)가 OS 및 하이퍼바이저로 시스템 이벤트를 전달하기 위한 메커니즘이다.
- 인터럽트 마스킹 및 critical section에 의해 지연되면 안되는 exception을 처리한다.
- 주 사용 케이스
- 시스템 에러 핸들링(RAS)
- 시스템 감시(watchdog)
- 커널 디버깅
- 샘플 프로파일링
- 유저 모드에서 trampoline 페이지 테이블을 사용한 커널 감추기
- 참고: SDEI: Software Delegated Exception Interface | Trusted Firmware-A
커널(어셈블리) 시작
_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. * * This code is mostly position independent so you call this at * __pa(PAGE_OFFSET). * * 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 _head: /* * DO NOT MODIFY. Image header expected by Linux boot-loaders. */ #ifdef CONFIG_EFI /* * This add instruction has no meaningful effect except that * its opcode forms the magic "MZ" signature required by UEFI. */ add x13, x18, #0x16 b primary_entry #else b primary_entry // branch to kernel start, magic .long 0 // reserved #endif .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 #ifdef CONFIG_EFI .long pe_header - _head // Offset to the PE header. pe_header: __EFI_PE_HEADER #else .long 0 // reserved #endif
부트 로더로 부터 처음 진입되는 커널 코드 시작점이다.
- 부트 로더로 부터 커널 코드 시작인 _head에 진입하기 전에 다음 규칙이 적용된다.
- MMU는 off 되어 있어야 한다.
- 참고: Why MMU and D-Cache must be off at Startup point in ARM64 | more or less insightful
- D-Cache는 off 되어 있어야 한다.
- I-Cache는 on/off 상관 없다.
- x0 레지스터에는 DTB 시작 물리 주소가 담겨 있어야 한다.
- 참고: Open Firmware and Devicetree | Kernel.org
- x0~x3까지의 레지스터를 커널(callee)이 보존해야 한다.
- 커널 진입 전 부트 로더 등에서 처리하는 일에 대해 다음을 참고한다.
- 참고: Booting AArch64 Linux | Kernel.org
- MMU는 off 되어 있어야 한다.
- 코드 라인 1에서 이후의 코드가 a(allocation) 및 x(execution) 속성을 가진 섹션 “.head.text”에 위치하도록 컴파일러에 지시한다.
- 코드 라인 2에서 _head: 레이블로 커널 최초 시작점이다.
- 코드 라인 6에서 UEFI 펌웨어를 지원한다.
- 코드 라인 11에서 ARM64 아키텍처가 add x13, x18, #0x16 명령을 통해 UEFI 지원 커널인지 여부를 알아내는 식별자로 “MZ” 아스키 코드를 가장 처음에 위치하게 만들어낸다. 그 외에 이 코드는 실제로는 커널에 아무런 의미도 없는 코드이다.
- 코드 라인 12에서 실제 코드가 있는 primary_entry 레이블로 이동한다.
- 코드 라인 17~30에서 커널 이미지 정보이다.
커널 이미지 헤더
압축 해제 상태의 커널 이미지에는 다음과 같이 64바이트의 커널 이미지 헤더가 존재하고, 리틀 엔디안 포맷으로 구성되어 있다.
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 코드가 있다.
- 예) add x13, x18, #0x16
- b primary_entry
- 시스템에 UEFI 펌웨어가 있는 경우 이 코드는 skip 하며, UEFI의 PE 헤더에 포함된 entry 포인터(efi_stub_entry)로 jump 한다. 그 후 다시 code0 위치로 jump 한다.
- stext로의 jump 코드가 있다.
- text_offset
- 이미지의 로드 offset 이다. (v3.17 이전에는 0x80000 값이 엔디안 지정없이 기록되어 있다.)
- 커널 v4.7 이후부터 text_offset 값은 0을 사용한다.
- 참고: arm64: don’t map TEXT_OFFSET bytes below the kernel if we can avoid it (2016, v4.7-rc1)
- 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” 아스크코드 문자열을 볼 수 있다.
- ELF
- DOS 헤더 (64 바이트)
- MZ
- 커널 이미지는 0x10000 offset을 가지므로, UEFI를 지원하는 커널인 경우 아래와 같이 0x10000 주소에 “MZ” 아스키코드 문자열을 볼 수 있다.
- MZ 문자열로 시작하는데 DOS 호환을 위해 사용되었다.
- ARMd
- ARM64 커널 이미지라는 것을 알 수 있도록 0x10038 주소에서 “ARMd” 아스키 코드 문자열을 볼 수 있다.
- MZ
- EFI PE 헤더
- PE
- EFI(Extensible Firmware Interface) PE(Portable Excutable)
- UEFI 헤더를 식별할 수 있도록 “PE” 아스키 코드 문자열을 볼 수 있다.
- 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 파일을 덤프해본다. 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
- 참고: ELF Relocations (AArch64) | 문c
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(Unified Extensible Firmware Interface) Specification | uefi.org
- UEFI 펌웨어는 디바이스 정보를 자동으로 인식하거나 수동 설정된 내용을 ACPI 테이블로 변환하여 부트로더 및 커널에 전달한다. 부트 로더 및 커널은 이 정보를 가지고 시스템을 초기화한다. 이렇게 UEFI가 전달하는 ACPI 테이블이 없는 임베디드 시스템들은 Device Tree 스크립트를 작성하여 컴파일한 FDT/DTB(Flattened Device Tree / Device Tree Blob) 스타일로 디바이스 정보를 전달한다. 최근엔 주요 정보는 ACPI로 전달하고, FDT/DTB로 추가 전달하는 경우도 있다.
- 참고: ACPI on ARMv8 Servers | Kernel.org
__HEAD
include/linux/init.h
#define __HEAD .section ".head.text","ax"
이후의 코드가 a(allocation) 및 x(execution) 속성을 가진 섹션 “.head.text”에 위치하도록 컴파일러에 지시한다.
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 * x21 stext() .. start_kernel() FDT pointer passed at boot in x0 * x23 stext() .. start_kernel() physical misalignment/KASLR offset * x28 __create_page_tables() callee preserved temp register * x19/x20 __primary_switch() callee preserved temp registers */
SYM_CODE_START(primary_entry) bl preserve_boot_args bl el2_setup // Drop to EL1, w0=cpu_boot_mode adrp x23, __PHYS_OFFSET and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0 bl set_cpu_boot_mode_flag bl __create_page_tables /* * 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. */ bl __cpu_setup // initialise processor b __primary_switch SYM_CODE_END(primary_entry)
커널 코드가 처음 시작되는 .init.text 섹션이다. 어셈블리 코드를 통해 임시 페이지 매핑을 수행한 후 mmu를 켜고 C 함수로 작성된 커널의 시작 위치인 start_kernel() 함수로 진입한다.
- 코드 라인 1에서기존에 stext 라는 레이블을 사용했었는데 primary_entry 레이블로 변경되었다. 헤더들을 빼면 진정한 커널 시작점이라고 할 수 있다.
- 참고: arm64: rename stext to primary_entry (2020, v5.8-rc1)
- 코드 라인 2에서 부트로더가 전달해준 x0 ~ x3 레지스터들을 boot_args 위치에 보관해둔다.
- setup_arch() 마지막 부분에서 저장된 boot_args[] 값들 중 x1~x3에 해당하는 값이 0이 아닌 값이 있는 경우 다음과 같은 경고 메시지를 출력한다.
- “WARNING: x1-x3 nonzero in violation of boot protocol: …“
- setup_arch() 마지막 부분에서 저장된 boot_args[] 값들 중 x1~x3에 해당하는 값이 0이 아닌 값이 있는 경우 다음과 같은 경고 메시지를 출력한다.
- 코드 라인 3에서 리눅스 커널이 el2로 부팅한 경우 하이퍼 바이저에 관련된 설정들을 수행한다.
- 코드 라인 4~5에서 커널 물리 시작 위치에서 커널 이미지 정렬단위인 2M 사이즈 이내의 offset 만을 추출해둔다.
- 코드 라인 6에서 커널 모드(el1)에서 부트했는지 하이퍼바이저(el2)에서 부트했는지 알 수 있도록 부트 모드 플래그를 __boot_cpu_mode에 저장한다.
- 코드 라인 7에서 커널에 대해 임시로 사용할 init 및 idmap 페이지 테이블을 생성한다.
- 코드 라인 14에서 프로세서를 초기화한다.
- 코드 라인 15에서 MMU를 활성화시킨 후 start_kernel() 함수로 점프한다.
adrp 명령
- adrp Xd, label
- Address of 4KB page at a PC-relative offset.
- 현재 주소(pc)로부터 +-4G 주소까지의 label 주소를 알아와서 Xd 레지스터에 저장한다.
- 참고: Addressing Mode (AArch64) | 문c
__INIT
include/linux/init.h
#define __INIT .section ".init.text","ax"
이후의 코드가 a(allocation) 및 x(execution) 속성을 가진 섹션 “.init.text”에 위치하도록 컴파일러에 지시한다.
- .init 섹션에 위치한 코드 및 데이터는 커널이 부팅한 후 더 이상 필요 없으므로 버디 시스템에 할당 해제 하여 활용한다.
부트 시 전달된 인자(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 mov x1, #0x20 // 4 x 8 bytes b __inval_dcache_area // tail call SYM_CODE_END(preserve_boot_args)
부트로더가 전달해준 x0 ~ x3 레지스터들을 boot_args 위치에 보관해둔다. x0는 DTB 주소로 사용되고, 나머지는 추후 사용하기 위해 예약되었다.
- 코드 라인 2~6에서 부트로더가 전달해준 x0 ~ x3 레지스터들을 boot_args 위치에 보관해둔다.
- 코드 라인 8에서 메모리 베리어를 사용하여 데이터 캐시 clean & invalidate를 수행하기 전에 MMU가 꺼진 상태에서 기존 요청한 저장 동작을 완전히 마무리하게 한다.
- 참고로 MMU가 꺼져 있어도 predictive 로딩은 가능한 상태이다.
- 코드 라인 11~12에서 부트 cpu에 대한 캐시는 아직 가동되지는 않았지만, 캐시를 가동한 후 쓰레기 데이터에 의해 잘못된 값이 읽히지 않도록 PoC 레벨까지 모든 cpu의 invalidate D-cache를 수행한다.
다음 두 그림을 통해 mmu-off 상태에서 메모리를 기록한 후 dmb와 dc ivac 명령을 사용하여 잠재적인 캐시 코히런스 문제를 제거한다.
-
- STR -> DMB -> DC IVAC 순서대로 처리한다.
- 참고: arm64: head: fix cache flushing and barriers in set_cpu_boot_mode_flag (2014, v3.16-rc1)
하이퍼 바이저 지원 코드 설정
el2_setup:
arch/arm64/kernel/head.S -1/3-
/* * If we're fortunate enough to boot at EL2, ensure that the world is * sane before dropping to EL1. * * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in w0 if * booted in EL1 or EL2 respectively. */
SYM_FUNC_START(el2_setup) msr SPsel, #1 // We want to use SP_EL{1,2} mrs x0, CurrentEL cmp x0, #CurrentEL_EL2 b.eq 1f mov_q x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1) msr sctlr_el1, x0 mov w0, #BOOT_CPU_MODE_EL1 // This cpu booted in EL1 isb ret 1: mov_q x0, (SCTLR_EL2_RES1 | ENDIAN_SET_EL2) msr sctlr_el2, x0 #ifdef CONFIG_ARM64_VHE /* * Check for VHE being present. For the rest of the EL2 setup, * x2 being non-zero indicates that we do have VHE, and that the * kernel is intended to run at EL2. */ mrs x2, id_aa64mmfr1_el1 ubfx x2, x2, #ID_AA64MMFR1_VHE_SHIFT, #4 #else mov x2, xzr #endif /* Hyp configuration. */ mov_q x0, HCR_HOST_NVHE_FLAGS cbz x2, set_hcr mov_q x0, HCR_HOST_VHE_FLAGS set_hcr: msr hcr_el2, x0 isb
cpu가 el2 레벨로 진입한 것은 즉 하이퍼 바이저를 사용하여 부팅된 경우로 이에 대한 설정 코드를 수행한다. 만일 el1 레벨로 부팅한 경우 아무것도 처리하지 않는다.
EL1 또는 EL2 부트 모드 확인
- 코드 라인 2에서 SP_EL1 스택을 선택하게 한다.
- 코드 라인 3~7에서 현재 EL 레벨을 읽어와서 el1인 경우 시스템 레지스터에 엔디안 설정 비트를 지정한다.
- 코드 라인 8~10에서 cpu가 el1 모드에서 부팅했음을 알리기 위해 w0에 BOOT_CPU_MODE_EL1(0xe11) 값을 담아 함수를 빠져나간다.
- 시스템 컨트롤 레지스터의 내용을 변경한 경우에는 isb 명령을 사용하여 파이프라인을 모두 비워야 한다.
- 코드 라인 12~13에서 1: 레이블로 진입한 경우는 커널이 el2로 부팅을 한 경우이다. SCTLR_EL2 레지스터에서 RES1에 해당하는 모든 비트들을 1로 설정하고, 사용할 엔디안을 결정한다.
- 참고: ARM64 시스템 주요 레지스터 | 문c
VHE 또는 nVHE 지정
- 코드 라인 15~25에서 ARMv8.1 VHE(Virtualization Host Extension) 커널 옵션을 사용하는 경우 시스템이 VHE가 지원되는지 여부를 알아오기 위해 id_aa64mmfr1_el1 레지스터의 vhe 필드(bits[11:8])를 읽어 x2 레지스터에 담아온다. 커널 옵션이 사용되지 않는 경우 x2에 0을 담아온다.
- 코드 라인 28~33에서 hcr_el2 레지스터에 nVHE 또는 VHE 플래그들을 설정하고 명령어 베리어 isb를 수행한다.
- 하이퍼 바이저 콘트롤 레지스터를 변경한 후에는 isb 명령을 사용하여 파이프라인을 모두 비워야 한다.
- nVHE 시스템(x2==0)은 HCR_HOST_NVHE_FLAGS 값을 기록
- nVHE 시스템(x2!=0)은 HCR_HOST_VHE_FLAGS 값을 기록
arch/arm64/kernel/head.S -2/3-
/* * 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. */ cbnz x2, 1f mrs x0, cnthctl_el2 orr x0, x0, #3 // Enable EL1 physical timers msr cnthctl_el2, x0 1: msr cntvoff_el2, xzr // Clear virtual offset #ifdef CONFIG_ARM_GIC_V3 /* GICv3 system register access */ mrs x0, id_aa64pfr0_el1 ubfx x0, x0, #ID_AA64PFR0_GIC_SHIFT, #4 cbz x0, 3f 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, 3f // and check that it sticks msr_s SYS_ICH_HCR_EL2, xzr // Reset ICC_HCR_EL2 to defaults 3: #endif /* Populate ID registers. */ mrs x0, midr_el1 mrs x1, mpidr_el1 msr vpidr_el2, x0 msr vmpidr_el2, x1 #ifdef CONFIG_COMPAT msr hstr_el2, xzr // Disable CP15 traps to EL2 #endif
EL1 물리 타이머 enable
- 코드 라인 11~16에서 VHE 기능이 있는 경우 1: 레이블로 이동하여 virtual offset 레지스터를 0으로 클리어한다. nVHE인 경우 하이퍼 바이저 카운터-타이머 컨트롤(cnthctl_el2) 레지스터의 EL1 물리 카운터와 (el1pcen)와 EL1 물리 타이머(el1pcten)에 해당하는 비트들을 설정하여 el0 및 el1에서 이들을 사용가능하도록 한다.
GICv3
- 코드 라인 20~22에서 id_aa64pfr0_el1 레지스터에서 gic 필드를 읽어 gicv3가 구현되지 않은 경우 3f 레이블로 이동한다.
- 코드 라인 24~27에서 icc_sre_el2.sre를 1로 설정하여시스템 레지스터를 enable하고, icc_sre_el2.enable을 1로 설정하여 non-secure el1에서 icc_sre_el1을 사용하도록 설정한다.
- 코드 라인 28~31에서 icc_sre_el2 레지스터를 다시 읽어 63번 비트를 읽어 1로 설정된 경우 문제가 있다고 판단하여 sys_ich_hcr_el2 레지스터를 클리어하여 리셋시킨다.
- 코드 라인 37~40에서 midr_el1 레지스터의 설정값을 읽어 vpidr_el2 레지스터에 그대로 적용하고, mpidr_el1 레지스터도 vmpidr_el2 레지스터에 적용한다.
- 코드 라인 43에서 hstr_el2 레지스터를 클리어하여 cp15 레지스터를 읽을 수 있도록 허용한다.
arch/arm64/kernel/head.S -3/3-
/* EL2 debug */ mrs x1, id_aa64dfr0_el1 sbfx x0, x1, #ID_AA64DFR0_PMUVER_SHIFT, #4 cmp x0, #1 b.lt 4f // Skip if no PMU present mrs x0, pmcr_el0 // Disable debug access traps ubfx x0, x0, #11, #5 // to EL2 and allow access to 4: csel x3, xzr, x0, lt // all PMU counters from EL1 /* Statistical profiling */ ubfx x0, x1, #ID_AA64DFR0_PMSVER_SHIFT, #4 cbz x0, 7f // Skip if SPE not present cbnz x2, 6f // VHE? mrs_s x4, SYS_PMBIDR_EL1 // If SPE available at EL2, and x4, x4, #(1 << SYS_PMBIDR_EL1_P_SHIFT) cbnz x4, 5f // then permit sampling of physical mov x4, #(1 << SYS_PMSCR_EL2_PCT_SHIFT | \ 1 << SYS_PMSCR_EL2_PA_SHIFT) msr_s SYS_PMSCR_EL2, x4 // addresses and physical counter 5: mov x1, #(MDCR_EL2_E2PB_MASK << MDCR_EL2_E2PB_SHIFT) orr x3, x3, x1 // If we don't have VHE, then b 7f // use EL1&0 translation. 6: // For VHE, use EL2 translation orr x3, x3, #MDCR_EL2_TPMS // and disable access from EL1 7: msr mdcr_el2, x3 // Configure debug traps /* LORegions */ mrs x1, id_aa64mmfr1_el1 ubfx x0, x1, #ID_AA64MMFR1_LOR_SHIFT, 4 cbz x0, 1f msr_s SYS_LORC_EL1, xzr 1: /* Stage-2 translation */ msr vttbr_el2, xzr cbz x2, install_el2_stub mov w0, #BOOT_CPU_MODE_EL2 // This CPU booted in EL2 isb ret
- 코드 라인 2~28에서 el2 디버그 및 statistical profiling 설정 관련 설명은 생략한다.
- 코드 라인 31~34에서 id_aa64mmfr1_el1 레지스터의 LOR 필드를 읽어 LORegions 기능이 있는 경우 LORC_EL1 레지스터를 클리어하여 LORegion 기능을 disable 한다.
- 코드 라인 38에서 vttbr_el2 레지스터를 클리어한다.
- 코드 라인 40에서 nVHE인 경우 호스트 커널이 EL1으로 전환하여 동작시키기 위해 el2용 stub 코드를 설치한다.
- 코드 라인 42~44에서 el2 부팅되었음을 알리는 값을 w0 레지스터를 통해 반환한다. (반환값: 0xe12)
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:
- 절대 값 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
nVHE 운영시 사용할 el2 stub 코드 설치
install_el2_stub:
arch/arm64/kernel/head.S
SYM_INNER_LABEL(install_el2_stub, SYM_L_LOCAL) /* * When VHE is not in use, early init of EL2 and EL1 needs to be * done here. * When VHE _is_ in use, EL1 will not be used in the host and * requires no configuration, and all non-hyp-specific EL2 setup * will be done via the _EL1 system register aliases in __cpu_setup. */ mov_q x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1) msr sctlr_el1, x0 /* Coprocessor traps. */ mov x0, #0x33ff msr cptr_el2, x0 // Disable copro. traps to EL2 /* SVE register access */ mrs x1, id_aa64pfr0_el1 ubfx x1, x1, #ID_AA64PFR0_SVE_SHIFT, #4 cbz x1, 7f 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. /* Hypervisor stub */ 7: adr_l x0, __hyp_stub_vectors msr vbar_el2, x0 /* spsr */ mov x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT |\ PSR_MODE_EL1h) msr spsr_el2, x0 msr elr_el2, lr mov w0, #BOOT_CPU_MODE_EL2 // This CPU booted in EL2 eret SYM_FUNC_END(el2_setup)
el2로 부팅하여 none-VHE 모드로 el1에서 호스트 커널을 운영하기 위해 el2 하이퍼바이저용 stub 코드를 설치한다. 반환 값은 0xe12 값으로 el2로 부팅했음을 나타낸다.
- 코드 라인 9~10에서 시스템 콘트롤 레지스터 sctlr_el1의 엔디안을 설정한다.
enable SVE
- 코드 라인 13~14에서 el2 커널 코드에서 코프로세서 명령인 SVE(Scalable Vector Extension)을 사용하는 경우 trap을 발생시키도록 아키텍처 Feature 트랩 레지스터 cptr_el2.sve를 1로 설정한다.(sve 기능이 있는 경우는 이 다음 코드 진행에서 해당 8번 비트를 클리어할 예정이다.)
- cptr_el2 레지스터의 RES1 값이 0x32ff이고, sve는 bit8을 사용한다.
- 코드 라인 17~25에서 id_aa64pfr0_el1 레지스터에서 sve 기능이 있는 경우 cptr_el2.tz을 0으로 클리어하여 SVE를 사용하여도 trap을 발생시키지 않도록 한다. 그런 후 SVE 컨트롤 레지스터 zcr_el2.len의 4비트를 모두 1로 채워 가장 큰 벡터 길이로 활성화한다.
- 벡터 길이 = (len + 1) * 128bits
- RAZ/WI에 해당하는 비트들은 어떠한 값을 기록해도 무시되고 0으로 읽힌다.
el2 stub용 벡터 지정
- 코드 라인 28~29에서 vbar_el2 레지서터에 하이퍼 바이저용 el2 stub 벡터를 지정한다.
- 코드 라인 32~34에서 spsr_el2 레지스터에 DAIF 비트를 모두 마스크하고 EL1 모드로 지정하여 기록한다.
- 코드 라인 35~37에서 el2(0xe12)로 부팅했음을 알리는 값을 w0 레지스터에 대입한 후, elr_el2 레지스터에 lr 값을 기록하고 복귀한다.
__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 el2_sync_invalid // Synchronous EL2h ventry el2_irq_invalid // IRQ EL2h ventry el2_fiq_invalid // FIQ EL2h ventry el2_error_invalid // Error EL2h ventry el1_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)
- EL1 호스트 OS에서 발생하는 sync exception만 처리하도록 하나의 벡터만 구성되어 있고, el1_sync 레이블이 호출된다.
부트 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] // This CPU has booted in EL1 dmb sy dc ivac, x1 // Invalidate potentially stale cache line ret SYM_FUNC_END(set_cpu_boot_mode_flag)
커널 부트 진입 시 cpu 모드(el0 ~ el2)를 파악하여 변수 __boot_cpu_mode[0~1]에 저장한다.
- 코드 라인 2~6에 첫 번째 인자 w0 값이 el2 모드가 아닌 경우 w0를 __boot_cpu_mode[0]에 저장하고, el2 모드로 부팅한 경우 __boot_cpu_mode[1]에 w0를 저장한다.
- 코드 라인 7~8에서 모든 메모리 읽기/쓰기 작업이 완료될 때 까지 기다린 후 방금 전에 저장한 주소에 해당하는 캐시 라인에 대해 캐시를 clean & invalidate 한다.
__boot_cpu_mode:
arch/arm64/kernel/head.S
/* * These values are written with the MMU off, but read with the MMU on. * Writers will invalidate the corresponding address, discarding up to a * 'Cache Writeback Granule' (CWG) worth of data. The linker script ensures * sufficient alignment that the CWG doesn't overlap another section. */ .pushsection ".mmuoff.data.write", "aw"
/* * We need to find out the CPU boot mode long after boot, so we need to * store it in a writable variable. * * This is not in .bss, because we set it sufficiently early that the boot-time * zeroing of .bss would clobber it. */
SYM_DATA_START(__boot_cpu_mode) .long BOOT_CPU_MODE_EL2 .long BOOT_CPU_MODE_EL1
__boot_cpu_mode[]의 초기 값은 다음과 같이 두 개 값이 담겨있다.
- BOOT_CPU_MODE_EL2=0xe12
- BOOT_CPU_MODE_EL1=0xe11
페이지 테이블 관련 매크로
create_table_entry 매크로
이 매크로는 VA_BITS가 48보다 작고 identity 매핑할 물리 주소가 VA_BITS 커버 범위를 벗어나는 경우에 사용된다. Identity 매핑의 Case 2)에 해당한다.
arch/arm64/kernel/head.S
/* * Macro to create a table entry to the next page. * * tbl: page table address * virt: virtual address * shift: #imm page table shift * ptrs: #imm pointers per table page * * Preserves: virt * Corrupts: ptrs, tmp1, tmp2 * Returns: tbl -> next level table page address */
.macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2 add \tmp1, \tbl, #PAGE_SIZE phys_to_pte \tmp2, \tmp1 orr \tmp2, \tmp2, #PMD_TYPE_TABLE // address of next table and entry type lsr \tmp1, \virt, #\shift sub \ptrs, \ptrs, #1 and \tmp1, \tmp1, \ptrs // table index str \tmp2, [\tbl, \tmp1, lsl #3] add \tbl, \tbl, #PAGE_SIZE // next level table page .endm
테이블 단계를 1 단계 확장할 때 호출된다. 최상위 테이블 @tbl에서 다음 단계 페이지 테이블에 연결하기 위해 가상 주소 @virt에 해당하는 최상위 페이지 테이블의 인덱스 엔트리에 기록한다.
- 코드 라인 2~4에서 다음 단계 페이지 테이블의 시작 물리 주소와 table 타입 디스크립터 속성을 추가하여 pte 엔트리로 사용될 @tmp2를 구성한다.
- 참고로 첫 번째 단계에 사용되는 pgd 엔트리의 디스크립터 타입에는 항상 table 타입을 사용한다.
- idmap_pg_dir에는 idmap 섹션 영역을 커버하기 위해 pgd부터 pud, pmd, pte까지 사용될 모든 테이블이 포함되어 있다.
- 코드 라인 5~7에서 가상 주소 @virt 를 @shift 만큼 우측 쉬프트한 값에 extra 엔트리 수 범위로 한정한 테이블 인덱스 값을 tmp1에 저장한다.
- 코드 라인 8에서 pte 엔트리 값인 @tmp2 값을 산출된 테이블 인덱스 위치에 저장하여 다음 테이블을 연결한다.
- 코드 라인 9에서 테이블 주소가 다음 단계의 페이지 테이블을 가리키게 한다.
다음 그림은 2단계로 사용될 예정인 idmap 페이지 테이블이 물리 주소 공간에 대응할 가상 주소 공간이 부족하여 테이블 단계를 확장하여 사용되는 모습을 보여준다.
- 컴파일 타임에 init 페이지 테이블이 VA_BITS를 사용하여 페이지 테이블들을 준비하는 것에 반해, idmap 페이지 테이블은 PA_BITS를 사용하여 페이지 테이블들을 준비한다.
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] 위치로 이동한다.
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] 범위까지 다음 단계 테이블 또는 페이지인 @rtbl에 속성 @flags를 mix하여 만든 pte 엔트리 값으로 매핑한다.
- 코드 라인 3~4에서 @rtbl 물리 주소로 pte 엔트리 값으로 변환하고 속성 값 @flags를 추가하여 pte 엔트리 값을 구한다.
- 코드 라인 5에서 pte 엔트리 값을 @tbl 페이지 테이블의 @index*8 주소 위치에 저장하여 매핑한다.
- 코드 라인 6~8에서 다음 매핑할 물리 주소를 산출하기 위해 @inc를 더하고, @eindex 까지 반복한다.
다음 그림은 페이지 테이블이 static하게 연속된 페이지 다음 단계 테이블들에 연결되는 모습을 보여준다.
- [index, eindex] 엔트리들이 다음 단계 페이지 테이블들로 연결된다.
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 * shift: shift used to transform virtual address into index * ptrs: 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, shift, ptrs * Returns: istart, iend, count */
.macro compute_indices, vstart, vend, shift, ptrs, istart, iend, count lsr \iend, \vend, \shift mov \istart, \ptrs sub \istart, \istart, #1 and \iend, \iend, \istart // iend = (vend >> shift) & (ptrs - 1) mov \istart, \ptrs mul \istart, \istart, \count add \iend, \iend, \istart // iend += (count - 1) * ptrs // our entries span multiple tables lsr \istart, \vstart, \shift mov \count, \ptrs sub \count, \count, #1 and \istart, \istart, \count sub \count, \iend, \istart .endm
페이지 테이블에서 가상 주소 범위 [@vstart, vend]에 해당하는 인덱스 번호 [@istart, @iend]를 산출한다. @count는 입출력 인자로 입력시에는 전단계에서 산출된 추가 필요 테이블 수를 담아오고, 출력시에는 다음 단계에서 사용할 기본 테이블 1개를 제외하고 추가로 필요로하는 테이블 수가 담긴다. (@count 변수 명을 @extra_count라고 생각하면 쉽다)
- 코드 라인 2~5에서 가상 주소 @vend를 @shift 만큼 우측 시프트하여 @ptrs 엔트리 수 이내로 제한하면 현재 테이블의 끝 인덱스인 @iend가 산출된다.
- @iend = (@vend >> @shift) & (@ptrs – 1)
- 코드 라인 6~8에서 전단계 산출된 결과인 추가 필요한 @count 테이블 수만큼 @ptrs 엔트리를 곱한후 끝 인덱스 @iend에 더한다.
- @iend += @count * @ptrs
- 코드의 주석 내용이 잘못된 것 처럼 보이지만 @count는 -1된 상태로 운영된다.
- 예) count = 10 = @count + 1
- @iend += @count * @ptrs
- 코드 라인 11~14에서 가상 주소 @vstart를 @shift 만큼 우측 시프트하여 @ptrs 엔트리 수 이내로 제한하면 현재 테이블의 시작 인덱스인 @istart가 산출된다.
- @istart = (@vstart >> @shift) & (@ptrs – 1)
- 코드 라인 16에서 끝 인덱스 번호 – 시작 인덱스 번호를 @count에 대입한다. 산출된 엔트리 수에서 기본 테이블 1개를 제외하여 추가 필요로하는 테이블 수를 @count 값으로 출력한다.
- @count = @iend – @istart
- 예) @istart=0, @iend=9인 경우 엔트리 개수는 10개지만 기본 1개 테이블을 제외하고 추가로 필요한 테이블 수 @count=9를 출력한다.
다음 그림은 compute_indices가 init 페이지 테이블에 대해 단계별로 3번 호출되는 모습을 보여준다.
다음 그림은 compute_indices가 init 페이지 테이블에 대해 단계별로 3번 호출되며 3개의 테이블이 더 추가된 모습을 보여준다.
다음 그림은 compute_indices가 작은 크기의 idmap 페이지 테이블에 대해 단계별로 3번 호출되며 2개의 테이블이 더 추가된 모습을 보여준다.
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: start address to map * vend: end address to map - we map [vstart, vend] * flags: flags to use to map last level entries * phys: physical address corresponding to vstart - physical memory is contiguous * pgds: the number of pgd entries * * Temporaries: istart, iend, tmp, count, sv - these need to be different registers * Preserves: vstart, vend, flags * Corrupts: tbl, rtbl, istart, iend, tmp, count, sv */
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv add \rtbl, \tbl, #PAGE_SIZE mov \sv, \rtbl mov \count, #0 compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \count populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp mov \tbl, \sv mov \sv, \rtbl #if SWAPPER_PGTABLE_LEVELS > 3 compute_indices \vstart, \vend, #PUD_SHIFT, #PTRS_PER_PUD, \istart, \iend, \count populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp mov \tbl, \sv mov \sv, \rtbl #endif #if SWAPPER_PGTABLE_LEVELS > 2 compute_indices \vstart, \vend, #SWAPPER_TABLE_SHIFT, #PTRS_PER_PMD, \istart, \iend, \count populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp mov \tbl, \sv #endif compute_indices \vstart, \vend, #SWAPPER_BLOCK_SHIFT, #PTRS_PER_PTE, \istart, \iend, \count bic \count, \phys, #SWAPPER_BLOCK_SIZE - 1 populate_entries \tbl, \count, \istart, \iend, \flags, #SWAPPER_BLOCK_SIZE, \tmp .endm
pgd 테이블 @tbl에 가상 주소 영역 [@vstart, @vend]을 필요한 전체 단계의 테이블에 매핑한다. 4K 페이지를 지원하는 경우 2M 단위로 블럭 매핑한다.
- 코드 라인 2~8에서 다음 단계의 페이지 테이블을 pgd 테이블의 [@vstart, @vend] 가상 주소에 해당하는 인덱스 엔트리에 연결한다.
- pgd 테이블은 기본 사용한다.
- 코드 라인 11~14에서 다음 단계의 페이지 테이블을 pud 테이블의 [@vstart, @vend] 가상 주소에 해당하는 인덱스 엔트리에 연결한다.
- SWAPPER_PGTABLE_LEVELS이 4단계 이상에서만 pud 테이블을 사용한다.
- 코드 라인 18~20에서 다음 단계의 페이지 테이블을 pmd 테이블의 [@vstart, @vend] 가상 주소에 해당하는 인덱스 엔트리에 연결한다.
- SWAPPER_PGTABLE_LEVELS이 3단계 이상에서만 pmd 테이블을 사용한다.
- 코드 라인 23~25에서 페이지 또는 2M 섹션(블럭)을 pte 테이블의 [@vstart, @vend] 가상 주소에 해당하는 인덱스 엔트리에 매핑할 때 @flags 속성을 추가하여 매핑한다.
- pte 테이블은 기본 사용한다.
다음 그림은 커널 이미지 영역을 map_memory 매크로를 통해 init_pg_dir에 매핑하는 모습을 보여준다.
페이지 테이블 생성
__create_page_tables:
init 페이지 테이블에 커널 이미지를 매핑하고, idmap 페이지 테이블에 idmap 섹션 영역을 identity 매핑한다. identity 매핑을 위해 매핑할 가상 주소 공간 크기가 부족한 경우 idmap 페이지 테이블의 단계를 상향 시키거나 최상위 idmap 페이지 테이블의 엔트리를 확대한다.
arch/arm64/kernel/head.S -1/3-
/* * Setup the initial page tables. We only setup the barest amount which is * required to get the kernel running. The following sections are required: * - identity mapping to enable the MMU (low address, TTBR0) * - first few MB of the kernel linear mapping to jump to once the MMU has * been enabled */
SYM_FUNC_START_LOCAL(__create_page_tables) mov x28, lr /* * Invalidate the init page tables to avoid potential dirty cache lines * being evicted. Other page tables are allocated in rodata as part of * the kernel image, and thus are clean to the PoC per the boot * protocol. */ adrp x0, init_pg_dir adrp x1, init_pg_end sub x1, x1, x0 bl __inval_dcache_area /* * Clear the init page tables. */ adrp x0, init_pg_dir adrp x1, init_pg_end sub x1, x1, x0 1: stp xzr, xzr, [x0], #16 stp xzr, xzr, [x0], #16 stp xzr, xzr, [x0], #16 stp xzr, xzr, [x0], #16 subs x1, x1, #64 b.ne 1b mov x7, SWAPPER_MM_MMUFLAGS
- 코드 라인 10~13에서 init 페이지 테이블 영역에 대해 캐시를 무효화한다.
- x0에는 init 테이블 시작 주소
- x1에는 init 테이블 사이즈
- 코드 라인18~26에서 init 페이지 테이블 영역을 모두 0으로 클리어한다.
- 성능향상을 위해 캐시 사이즈(64 바이트)를 기준으로 8바이트 페어를 4번 연속으로 수행한다.
- 참고: Loop unrolling | WIKIPEDIA
- 성능향상을 위해 캐시 사이즈(64 바이트)를 기준으로 8바이트 페어를 4번 연속으로 수행한다.
- 코드 라인 28에서 x7 레지스터에 매핑 시 사용할 속성 플래그를 담아둔다.
arch/arm64/kernel/head.S -2/3-
/* * Create the identity mapping. */ adrp x0, idmap_pg_dir adrp x3, __idmap_text_start // __pa(__idmap_text_start) #ifdef CONFIG_ARM64_VA_BITS_52 mrs_s x6, SYS_ID_AA64MMFR2_EL1 and x6, x6, #(0xf << ID_AA64MMFR2_LVA_SHIFT) mov x5, #52 cbnz x6, 1f #endif mov x5, #VA_BITS_MIN 1: adr_l x6, vabits_actual str x5, [x6] dmb sy dc ivac, x6 // Invalidate potentially stale cache line /* * VA_BITS may be too small to allow for an ID mapping to be created * that covers system RAM if that is located sufficiently high in the * physical address space. So for the ID map, use an extended virtual * range in that case, and configure an additional translation level * if needed. * * 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 __idmap_text_end. */ adrp x5, __idmap_text_end clz x5, x5 cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough? b.ge 1f // .. then skip VA range extension adr_l x6, idmap_t0sz str x5, [x6] dmb sy dc ivac, x6 // Invalidate potentially stale cache line #if (VA_BITS < 48) #define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3) #define EXTRA_PTRS (1 << (PHYS_MASK_SHIFT - EXTRA_SHIFT)) /* * 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 mov x4, EXTRA_PTRS create_table_entry x0, x3, EXTRA_SHIFT, x4, x5, x6 #else /* * If VA_BITS == 48, we don't have to configure an additional * translation level, but the top-level table has more entries. */ mov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT) str_l x4, idmap_ptrs_per_pgd, x5 #endif 1: ldr_l x4, idmap_ptrs_per_pgd mov x5, x3 // __pa(__idmap_text_start) adr_l x6, __idmap_text_end // __pa(__idmap_text_end) map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
idmap_pg_dir 테이블에 __idmap_text_start 주소부터 __idmap_text_end 영역까지 가상 주소와 물리 주소가 일치하는 identity 매핑을 생성한다.
- 코드 라인 7~16에서 커널에 설정된 vabits를 전역 변수 vabits_actual에 저장한다. 만일 커널이 52bit 가상 주소 영역을 지원하고 mmfr_el1 레지스터에서 lva 기능이 지원되는 것을 확인한 경우에는 유저 가상 주소를 표현하는 비트 수를 52로하여 저장한다.
- 코드 라인 17~18에서 모든 메모리 읽기/쓰기 작업이 완료될 때 까지 기다린 후 vabits_actual에 저장된 캐시 라인에 대해 clean & invalidate 한다.
- 코드 라인 32~35에서 idmap 코드의 마지막 주소가 설정된 커널용 가상 주소 공간보다 크거나 같은 경우 정상적으로 identity 매핑을 하기 위해 1: 레이블로 이동한다.
- clz(count leading zero) 명령을 사용하여 idmap 코드의 마지막 주소를 대상으로 0으로 시작하는 비트가 몇 개인지 센다.
- 예) clz(0x0000_00f1_1234_0000) = 24
- 커널에 설정된 VABITS=48일 때 가상 주소 공간의 크기는 256T이다.
- 예) TCR_T0SZ(48)=16
- clz(count leading zero) 명령을 사용하여 idmap 코드의 마지막 주소를 대상으로 0으로 시작하는 비트가 몇 개인지 센다.
- 코드 라인 37~38에서 가상 주소 영역의 확장을 위해 필요한 유저 비트 수를 변수 idmap_t0sz에 저장한다.
- 코드 라인 39~40에서 모든 메모리 읽기/쓰기 작업이 완료될 때 까지 기다린 후 idmap_t0sz이 저장된 캐시 라인에 대해 clean & invalidate 한다.
- 코드 라인 42~58에서 커널이 VA_BITS<48과 같은 작은 설정을 사용하는 경우 페이지 테이블 단계를 1 단계 더 상향한다.
- 4K 페이지, VABITS=39일 떄 PGDIR_SHIFT=30이다. 이 때 EXTRA_SHIFT=39와 같이 엔트리가 커버하는 공간이 1 단계 더 상향된다.
- 최상위 페이지 테이블이 사용할 엔트리 수인 EXTRA_PTRS에는 1 << (ARM64_PA_BITS – EXTRA_SHIFT) 이므로 1 << (48 – 39) = 512 이다.
- 코드 라인 59~66에서 커널이 VA_BITS=48과 같은 설정을 사용하는 경우 페이지 테이블 단계를 더 상향시키지는 못하므로 최상위 pgd 테이블의 엔트리 수를 추가한다.
- 코드 라인 67~72에서 1: 레이블이다. idmap_pg_dir 테이블에 __idmap_text_start 주소부터 __idmap_text_end 영역까지 가상 주소와 물리 주소가 일치하는 identity 매핑을 생성한다.
arch/arm64/kernel/head.S -3/3-
/* * Map the kernel image (starting with PHYS_OFFSET). */ adrp x0, init_pg_dir mov_q x5, KIMAGE_VADDR // compile time __va(_text) add x5, x5, x23 // add KASLR displacement mov x4, PTRS_PER_PGD 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) map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14 /* * 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, idmap_pg_dir adrp x1, idmap_pg_end sub x1, x1, x0 bl __inval_dcache_area adrp x0, init_pg_dir adrp x1, init_pg_end sub x1, x1, x0 dmb sy bl __inval_dcache_area ret x28 SYM_CODE_END(__create_page_tables) .ltorg
- 코드 라인 4~13에서 물리 주소에 위치한 커널 이미지를 init_pg_dir 테이블의 가상 주소 __text ~ _end 범위에 매핑한다.
- KASLR이 동작하는 경우 이 함수에 두 번째 진입 시 x23 레지스터에 KASLR offset이 담겨 들어온다.
- 코드 라인 20~25에서모든 메모리 읽기/쓰기 작업이 완료될 때 까지 기다린 후 idmap_pg_dir ~ idmap_pg_end 범위에 대해 캐시를 무효화한다.
- 코드 라인 27~31에서 모든 메모리 읽기/쓰기 작업이 완료될 때 까지 기다린 후 init_pg_dir ~ init_pg_end 범위에 대해 캐시를 무효화한다.
다음 그림은 커널 이미지의 리니어 매핑과 identity 매핑을 비교하여 보여준다.
부트 CPU 스위치
다음 그림은 KASLR의 활성화 여부와 관련된 처리 흐름을 보여준다.
- KASLR이 활성화된 경우 __primary_switched: 및 __create_page_tables: 레이블이 한 번 더 호출되는 모습을 볼 수 있다.
MMU 스위치 전
__primary_switch:
arch/arm64/kernel/head.S
SYM_FUNC_START_LOCAL(__primary_switch) #ifdef CONFIG_RANDOMIZE_BASE mov x19, x0 // preserve new SCTLR_EL1 value mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value #endif adrp x1, init_pg_dir bl __enable_mmu #ifdef CONFIG_RELOCATABLE #ifdef CONFIG_RELR mov x24, #0 // no RELR displacement yet #endif bl __relocate_kernel #ifdef CONFIG_RANDOMIZE_BASE ldr x8, =__primary_switched adrp x0, __PHYS_OFFSET blr x8 /* * If we return here, we have a KASLR displacement in x23 which we need * to take into account by discarding the current kernel mapping and * creating a new one. */ pre_disable_mmu_workaround msr sctlr_el1, x20 // disable the MMU isb bl __create_page_tables // recreate kernel mapping tlbi vmalle1 // Remove any stale TLB entries dsb nsh msr sctlr_el1, x19 // re-enable the MMU isb ic iallu // flush instructions fetched dsb nsh // via old mapping isb bl __relocate_kernel #endif #endif ldr x8, =__primary_switched adrp x0, __PHYS_OFFSET br x8 SYM_FUNC_END(__primary_switch)
MMU를 활성화 한 후 __primary_switched로 점프한다. MMU 활성화 후 커널 리로케이션 옵션을 사용하는 경우 잠시 mmu를 껀채로 리로케이션을 수행 후 다시 페이지 테이블을 매핑하고 mmu를 켠다.
- 코드 라인 2~5에서 KASLR(커널 랜덤 위치) 옵션이 지정된 경우 기존 sctlr_el1과 현재 sctlr_el1을 각각 x20, x19에 보존해둔다.
- 코드 라인 7~8에서 init 페이지 테이블을 사용하여 mmu를 활성화한다.
- 코드 라인 13에서 재배치 정보를 담고 있는 .rela.dyn 섹션에 위치한 엔트리들을 옮긴다.
- KASLR 옵션 설정 시에도 CONFIG_RELOCATABLE이 설정된다.
- 참고: arm64: add support for building vmlinux as a relocatable PIE binary (2016, v4.6-rc1)
- 코드 라인 15~17에서 CONFIG_RANDOMIZE_BASE 커널 옵션을 사용하는 경우 x0에 커널 이미지의 물리 주소 위치 담긴 주소를 담고 __primary_switched()를 수행한 후 돌아온다.
- 코드 라인 24에서 qualcom사의 FALKOR SoC에 Speculative 명령이 발생하는 case가 있어서 isb 명령을 워크어라운드 코드로 추가하였다.
- 코드 라인 25~27에서 잠시 mmu를 끈 상태로 페이지 테이블을 다시 만든다.
- 코드 라인 29~30에서 모든 tlb 엔트리를 무효화후 dsb 명령을 통해 페이지 테이블의 변화가 모든 cpu들에 적용되게 한다.
- 코드 라인 32~36에서 mmu를 다시 켜고 명령 캐시를 모두 모효화하고, 페이지 테이블의 변화가 모든 cpu들에 적용되게 한다.
- 중간에 isb를 사용하는 경우 isb 전후로 명령 실행 순서가 바뀌지 않아야 하는 경우 사용된다.
- 코드 라인 38에서 재배치 정보를 담고 있는 .rela.dyn 섹션에 위치한 엔트리들을 옮긴다.
- 코드 라인 41~43에서 x0에 커널 이미지의 물리 주소 위치(offset 구간 포함)가 담긴 주소를 담고 __primary_switched로 점프한다.
- __primary_switch() 함수와 enable_mmu() 등의 함수는 idmap 테이블에 매핑되어 있고, __primary_switched() 함수로 점프할 때부터 init 페이지 테이블을 사용한다.
MMU 활성화
__enable_mmu:
arch/arm64/kernel/head.S
/* * Enable the MMU. * * x0 = SCTLR_EL1 value for turning on the MMU. * x1 = TTBR1_EL1 value * * 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 x2, ID_AA64MMFR0_EL1 ubfx x2, x2, #ID_AA64MMFR0_TGRAN_SHIFT, 4 cmp x2, #ID_AA64MMFR0_TGRAN_SUPPORTED b.ne __no_granule_support update_early_cpu_boot_status 0, x2, x3 adrp x2, idmap_pg_dir phys_to_ttbr x1, x1 phys_to_ttbr x2, x2 msr ttbr0_el1, x2 // load TTBR0 offset_ttbr1 x1, x3 msr ttbr1_el1, x1 // load TTBR1 isb msr sctlr_el1, x0 isb /* * Invalidate the local I-cache so that any instructions fetched * speculatively from the PoC are discarded, since they may have * been dynamically patched at the PoU. */ ic iallu dsb nsh isb ret SYM_FUNC_END(__enable_mmu)
MMU를 enable 한다. MMU를 enable 한 후에는 init 페이지 테이블을 사용하는데, enable 하는 순간의 현재 코드들은 idmap 페이지 테이블을 사용한다.
- 코드 라인 1~4에서 MMFR0_EL1 (Memory Model Feature Register 0 Register – EL1)을 통해 커널이 설정한 페이지 타입을 지원하는지 확인하고, 지원하지 않는 경우 __no_granule_support 레이블로 이동한다.
- 코드 라인 5에서 boot cpu 상태를 저장하는 변수 __early_cpu_boot_status의 값을 0으로 초기화한다.
- 코드 라인 6~14에서 ttbr0 레지스터에 idmap 페이지 테이블을 지정하고, ttbr1 레지스터에 init 페이지 테이블을 지정한다. 그 후 mmu를 enable 한다. mmu를 enable 하기 전/후로 isb 명령을 사용하여 명령 파이프를 비운다.
- ttbr offset
- VA_BITS=52로 설정된 커널이 VA48만 지원하는 시스템에서 동작하는 경우 ttbr offset을 추가한다.
- 1024개의 pgd 엔트리로 동작하는 va52와 다르게 64개의 pgd 엔트리로 동작하는 va48을 위해 offset((1024-64) * 8 바이트)을 추가한다.
- 참고:
- arm64: mm: Offset TTBR1 to allow 52-bit PTRS_PER_PGD (2018, v5.0-rc1)
- arm64: mm: Logic to make offset_ttbr1 conditional (2019, v5.4-rc1)
- ttbr offset
- 코드 라인 20~23에서 명령 캐시를 모두 무효화 시키고, 페이지 테이블 등이 변경되었으므로 dsb 명령을 사용하여 모든 cpu들에 대해 반영되도록 한다. 그런 후 다시 명령 파이프를 비운 후 리턴한다.
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
LVA(VA=52 support)가 지원되지 않는 시스템의 경우 VA48로 동작시키는데 PGD 테이블 위치를 offset(0x1E00) 만큼 더한 주소를 적용시킨다.
- 이렇게 더한 주소를 사용하는 경우 리눅스의 pgd_index()등의 함수가 유저 페이지 테이블이든 커널 페이지 테이블이든 1개의 API로 통일하여 사용할 수 있는 잇점이 있다.
다음 그림은 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이 적용된 모습을 보여준다.
재배치 엔트리 리로케이션
__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. */ ldr w9, =__rela_offset // offset to reloc table ldr w10, =__rela_size // size of reloc table mov_q x11, KIMAGE_VADDR // default virtual offset add x11, x11, x23 // actual virtual offset add x9, x9, x11 // __va(.rela) add x10, x9, x10 // __va(.rela) + sizeof(.rela) 0: cmp x9, x10 b.hs 1f ldp x12, x13, [x9], #24 ldr x14, [x9, #-8] cmp w12, #R_AARCH64_RELATIVE b.ne 0b add x14, x14, x23 // relocate str x14, [x12, x23] b 0b
커널 코드의 재배치가 일어나는 경우 재배치 정보를 담고 있는 .rela.dyn 섹션에 위치한 엔트리 정보들을 사용해 엔트리가 상대 주소(#R_AARCH64_RELATIVE)를 사용하는 타입인 경우 이들이 가리키는 주소의 값을 변경된 offset 만큼 추가하여 변경한다.
- 코드 라인 7~13에서 x9 레지스터에 __rela_offset + KIMAGE_VADDR + relocation offset(x23) 값인 시작 주소를 산출한다. 그리고 x10 레지스터에 __rela_size를 더한 끝 주소를 대입한다.
- 코드 라인 15~16에서 x9 레지스터 값이 끝 주소 이상이면 리로케이션이 모두 완료되었으므로 함수를 빠져나간다.
- 코드 라인 17~20에서 24바이트로 구성된 다이나믹 리로케이션 엔트리를 읽어온다.
- x9주소의 16바이트를 Offset(x12)와 인포타입(x13) 레지스터로 읽고, 다음 엔트리를 위해 x9 주소에 #24를 더 한다. 그리고 x9 – 8 주소 위치의 값을 Addend 값(x14) 레지스터로 읽는다. 인포 타입(w12) 레지스터의 값이 #R_AARCH64_RELATIVE가 아닌 경우 skip 하고 다시 0 레이블로 반복한다.
- ldp x12, x13, [x9], #24 의 경우 post-indexed 어드레싱을 사용했다.
- ldr x14, [x9, #-8]의 경우 offset 어드레싱을 사용했다.
- x9주소의 16바이트를 Offset(x12)와 인포타입(x13) 레지스터로 읽고, 다음 엔트리를 위해 x9 주소에 #24를 더 한다. 그리고 x9 – 8 주소 위치의 값을 Addend 값(x14) 레지스터로 읽는다. 인포 타입(w12) 레지스터의 값이 #R_AARCH64_RELATIVE가 아닌 경우 skip 하고 다시 0 레이블로 반복한다.
- 코드 라인 21~23에서 다이나믹 리로케이션 엔트리의 Offset(x12) + relocation offset(x23)가 가리키는 주소에 엔트리의 Addend 값(x14) + relocation offset(x23) 값으로 갱신하고 0레이블로 이동하여 반복한다.
- 참고: ELF Relocations (AArch64) | 문c
다음 그림은 하나의 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. * * Because addends are stored in place in the binary, RELR relocations * cannot be applied idempotently. We use x24 to keep track of the * currently applied displacement so that we can correctly relocate if * __relocate_kernel is called twice with non-zero displacements (i.e. * if there is both a physical misalignment and a KASLR displacement). */
ldr w9, =__relr_offset // offset to reloc table ldr w10, =__relr_size // size of reloc table add x9, x9, x11 // __va(.relr) add x10, x9, x10 // __va(.relr) + sizeof(.relr) sub x15, x23, x24 // delta from previous offset cbz x15, 7f // nothing to do if unchanged mov x24, x23 // save new offset 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, x15 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, x15 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 재배치 기능을 지원한다.
- 이 기능은 CONFIG_RELOCATABLE 커널 옵션이 동작할 때에만 유효하다.
- defconfig 기준 비압축 커널 이미지의 경우 3.5M(16%)가 줄어들고, 압축(lz4) 커널 이미지의 경우 550K(5%)가 줄어드는 효과가 있다.
- 참고:
- arm64: Add support for relocating the kernel with RELR relocations (2019, v5.4-rc1)
- System V Application Binary Interface – DRAFT – 10 June 2013
.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 바이트)으로 구성되어 있다.
- Offset 주소(8 바이트) + Info(8 바이트) + Addend 값(8 바이트)
$ 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 = __PHYS_OFFSET */
SYM_FUNC_START_LOCAL(__primary_switched) adrp x4, init_thread_union add sp, x4, #THREAD_SIZE adr_l x5, init_task msr sp_el0, x5 // Save thread_info #ifdef CONFIG_ARM64_PTR_AUTH __ptrauth_keys_init_cpu x5, x6, x7, x8 #endif adr_l x8, vectors // load VBAR_EL1 with virtual msr vbar_el1, x8 // vector table address isb stp xzr, x30, [sp, #-16]! mov x29, sp #ifdef CONFIG_SHADOW_CALL_STACK adr_l scs_sp, init_shadow_call_stack // Set shadow call stack #endif 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 // 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 #ifdef CONFIG_KASAN bl kasan_early_init #endif #ifdef CONFIG_RANDOMIZE_BASE tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized? b.ne 0f mov x0, x21 // pass FDT address in x0 bl kaslr_early_init // parse FDT for KASLR options cbz x0, 0f // KASLR disabled? just proceed orr x23, x23, x0 // record KASLR offset ldp x29, x30, [sp], #16 // we must enable KASLR, return ret // to __primary_switch() 0: #endif add sp, sp, #16 mov x29, #0 mov x30, #0 b start_kernel SYM_FUNC_END(__primary_switched)
MMU를 켠 후 동작하는 코드로 커널용 스택과 벡터 포인터를 지정하고 BSS 영역을 클리어한 후 start_kernel() 함수로 점프한다.
- 코드 라인 2~3에서 스택 레지스터에 커널 스택 용도로 사용할 메모리 위치를 지정한다.
- init_thread_union
- include/asm-generic/vmlinux.lds.h 에 심볼이 정의되어 있고 커널 스택의 사이즈는 THREAD_SIZE이다.
- init_thread_union
- 코드 라인 4~5에서 컴파일 타임에 준비된 최초 커널용 태스크인 init_task의 주소를 임시로 sp_el0에 저장해둔다.
- sp_el0는 유저 공간으로 context switch 된 후 유저용 스택 위치를 가리키는 용도로 사용된다.
- 그러나 커널(el1)에서는 사용하지 않는 스크래치 레지스터 용도일 뿐이므로 이를 활용하여 thread_info를 가리키는 레지스터로 사용한다.
- arm64: Store struct thread_info in sp_el0 (2015, v4.5-rc1)
- 코드 라인 8에서 pointer authentication 키를 초기화한다.
- 코드 라인 11~13에서 vbar_el1 레지스터에 vector 위치를 지정한 후 isb를 수행하여 이후 실행되는 명령이 isb 전에 변경한 컨텍스트가 적용되어 동작하도록 한다.
- 코드 라인 15~16에서 0과 x30 내용을 스택에 보관한다.
- 코드 라인 19에서 shadow call stack을 초기화한다.
- CONFIG_SHADOW_CALL_STACK
- Shadow Call Stack은 스택을 사본으로 복제한 후 Stack buffer overflow 공격을 막기 위해 사용된다.
- 예) 사본에 저장된 리턴 주소(return address)가 변경되어 오염된 경우 이를 막기 위해 사용된다.
- 참고: Shadow Stack | WIKIPEDIA
- Shadow Call Stack은 스택을 사본으로 복제한 후 Stack buffer overflow 공격을 막기 위해 사용된다.
- CONFIG_SHADOW_CALL_STACK
- 코드 라인 22에서 fdt 시작 물리 주소를 담고 있는 x21 레지스터를 변수 __fdt_pointer에 저장한다.
- 코드 라인 24~26에서 커널 시작 가상 주소에서 커널 시작 물리 주소(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
- 코드 라인 29~34에서 BSS 영역을 0으로 모두 클리어한 후 기록된 0 값이 다른 inner-share 영역의 cpu들이 볼 수 있도록 반영한다.
- 참고: arm64: head.S: use memset to clear BSS (2016, v4.5-rc1)
- 코드 라인 40~41에서 CONFIG_RANDOMIZE_BASE 옵션이 사용되는 경우 커널이 이미 relocation된 경우 0 레이블로 전진한다.
- 코드 라인 42~44에서 kaslr_early_init()을 수행하는데 /chosen 노드의 bootargs에서 “nokaslr” 커널 파라미터가 사용된 경우 0 레이블로 전진한다. kaslr_early_init() 함수는 64바이트의 kaslr-seed 속성 값을 사용한 내부 연산을 통해 커널의 시작 위치가 변동된 offset 값을 반환한다. KASLR offset이 결정되었으므로 루틴을 빠져나간뒤 매핑을 다시 해야 한다.
- 예) kaslr-seed = <0xfeedbeef 0xc0def00d>;
- 이후 C 루틴으로 동작하는 최초 커널 함수인 start_kernel()부터 랜덤 가상 주소에서 실행된다.
- 코드 라인 45~47에서 KASLR offset 값을 x23 레지스터에 저장하고, 스택에 보관해둔 값을 x29(0 값)와 x30에는 반환 받은 후 프로시져를 리턴한다.
- 코드 라인 50~53에서 사용 했던 16바이트의 스택 포인터를 원위치시키고, x29, x30 레지스터에 0을 담은 후 start_kernel() 함수로 점프한다.
__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바이트 주소를 레지스터에 읽어들이는 코드를 생성한다.
- 예) ldr <Rd>, =<label>과 같은 특수한 pesudo instruction을 사용
__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_offset의 변경 과정
1) 첫 번째 primary_switched로 이동하기 전
- bl __relocate_kernel
- 심볼들의 리로케이션을 수행한다.
- __primary_switched 심볼 위치에는 심볼 주소를 저장하는 대신 처음에 0을 기록해두고 있다. 대신 .rela 섹션의 리로케이션 엔트리에 addend 필드에 실제 주소가 담기고, 이 값이 리로케이션 수행 후 __primary_siwtched 주소에 저장된다.
- ldr x8, =__primary_switched
- 리로케이션 작업에 의해 설정된 __primary_switched에 저장된 주소 값을 읽어온다.
- adrp x0, __PHYS_OFFSET
- 현재 수행되는 코드는 .idmap 섹션에 있으므로 mmu를 켯다하더라도 pc 위치는 물리 주소와 같이 낮은 주소를 사용한다.
- __PHYS_OFFSET 심볼의 위치는 _head(=_text)이고 컴파일 타임에 주어진 값은 0xffff_8000_1000_0000 이지만 adrp와 같이 사용되면 런타임에 pc 기준으로 relative offset 값이 적용되어 읽어오므로 실제 커널의 물리 시작 주소(예: 0x4020_0000)를 읽어온다.
2) 랜더마이즈 적용 후 두 번째 primary_switched로 이동하기 전
- bl __relocate_kernel
- 심볼들의 리로케이션을 수행한다. (=__primary_switched에 저장된 심볼 주소 역시 랜더마이즈한 주소로 다시 바뀐다.)
- ldr x8, =__primary_switched
- 바뀐 __primary_switched 값을 읽어온다. (예: 0xffff_abcd_ef00_0000)
- adrp x0, __PHYS_OFFSET
- 1)번에서 설명한바와 동일하여 생략.
kimage_voffset 저장 과정
primary_switched 내부에는 아래와 같이 kimage_voffset를 저장하는 루틴이 있고, 역시 랜더마이즈 적용되는 경우엔 두 번 호출된다.
- ldr_l x4, kimage_vaddr
- kimage_vaddr에는 처음 컴파일 타임에 저장해둔 _text(0xffff_8000_1000_0000) 주소가 담겨있지만 두번째 호출시엔 리로케이션된 커널 시작주소로 변경된다.
- 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를 사용한다.
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 el2_setup // Drop to EL1 bl set_cpu_boot_mode_flag 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. */ bl __cpu_secondary_check52bitva bl __cpu_setup // initialise processor adrp x1, swapper_pg_dir bl __enable_mmu ldr x8, =__secondary_switched br x8 SYM_FUNC_END(secondary_startup)
프로세서를 초기화하고 MMU를 켠 후 __secondary_switched()루틴으로 점프한다.
- 코드 라인 5에서 부트 cpu가 52bit 유저 가상 주소를 사용한 경우 secondary cpu가 이를 지원하는지 여부를 체크하는데, 지원하지 않는 경우 stuck한다.
- 코드 라인 6에서 프로세서를 초기화한다.
- 코드 라인 7~8에서 swapper_pg_dir 에서 커널이 동작하도록 MMU를 켠다.
- 코드 라인 9~10에서 __secondary_switched 루틴으로 점프한다.
__cpu_secondary_check52bitva:
arch/arm64/kernel/head.S
SYM_FUNC_START(__cpu_secondary_check52bitva) #ifdef CONFIG_ARM64_VA_BITS_52 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)한다.
__secondary_switched:
arch/arm64/kernel/head.S
SYM_FUNC_START_LOCAL(__secondary_switched) adr_l x5, vectors msr vbar_el1, x5 isb adr_l x0, secondary_data ldr x1, [x0, #CPU_BOOT_STACK] // get secondary_data.stack cbz x1, __secondary_too_slow mov sp, x1 ldr x2, [x0, #CPU_BOOT_TASK] cbz x2, __secondary_too_slow msr sp_el0, x2 scs_load x2, x3 mov x29, #0 mov x30, #0 #ifdef CONFIG_ARM64_PTR_AUTH ptrauth_keys_init_cpu x2, x3, x4, x5 #endif b secondary_start_kernel SYM_FUNC_END(__secondary_switched)
커널용 벡터 포인터와 스택을 지정한 후 C 루틴인 secondary_start_kernel() 루틴으로 점프한다.
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에는 파괴되도 상관이 없는 임시 레지스터를 지정한다.
- 주석 내용에서 인자가 잘못되어 있음을 확인할 수 있다.
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 한다.
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 레지스터 값을 기록한다.
참고
- ARM64 시스템 주요 레지스터 | 문c
- ARM64 페이지 테이블 -1- (Basic) | 문c
- ARM64 페이지 테이블 -2- (매핑) | 문c
- ARM64 페이지 테이블 -3- (API) | 문c
- Addressing Mode (AArch64) | 문c
- ELF Relocations (AArch64) | 문c
- Memory Layout on AArch64 Linux | Kernel.org
- KAISER: hiding the kernel from user space | LWN.net
- SDEI: Software Delegated Exception Interface | Trusted Firmware-A
- Why MMU and D-Cache must be off at Startup point in ARM64 | more or less insightful
- Booting AArch64 Linux | Kernel.org
- UEFI(Unified Extensible Firmware Interface) Specification | uefi.org
- ACPI on ARMv8 Servers | Kernel.org
- Open Firmware and Devicetree | Kernel.org
- [ELF] ELF Header | Developer’s Delight
- ELF Specification | TIS Committee – 다운로드 pdf
- ELF for the ARM® 64-bit Architecture (AArch64) | ARM – 다운로드 pdf
안녕하세요,
__relocate_kernel: 처럼 이러한 재배치 작업은 왜 하는건가요?
코드는 얼추 이해되는데 굳이 이러한 작업을 왜 할까 싶은 물음이 해소되지 않네요.. 구글 검색해도 재배치 관련 설명만 있고요..
검색 키워드라도 알려주시면 감사드립니다.
안녕하세요?
커널이 부팅할 때 가상 주소에서 커널 위치를 고정시키지 않고 랜덤하게 바꾸는 것으로 커널을 숨겨 보안을 높이는 과정에서, 커널의 시작 주소가 변경되므로 모든 export된 심볼들의 주소도 기존 주소로부터 변경된 offset 주소만큼 모두 갱신을 하기 위해 사용합니다.
다음을 참고하시기 바랍니다.
http://jake.dothome.co.kr/a64-elf-relocations/
안녕하세요, 영일님.
설명 너무 감사드립니다. 덕분에 이해하였습니다!
후배들을 위해 좋은 글 써주셔서 늘 감사드립니다.
좋은 밤 되세요.
감사합니다. 좋은 하루 되세요. ^^
커널은 자신이 사용가능한 물리 메모리의 크기를 어떤 변수를 통해서 전달 받는 것인가요?
KERNEL_START , KERNEL_END (memory.h)를 보면 알수 있을까요?
안녕하세요?
UEFI 펌웨어가 있는 PC나 서버들은 펌웨어가 인식해서 ACPI 테이블 형식으로 커널에게 전달하고,
ARM 같은 시스템의 경우 디바이스 트리 바이너리(DTB)형식으로 커널에게 전달합니다.
그 후, memblock 자료에서 관리합니다. (arm64_memblock_init() 함수부터 살펴보세요.)
감사합니다.
안녕하세요.
내용중에 ,
다음 그림과 같이 ARM64 시스템에서 커널 공간을 담당하는 TTBR1과 유저 공간을 담당하는 TTBR1을 사용하여 각각의 커널 모드와 유저 모드에서 상대방의 영역을 사용하지 못하게 분리하는 방법을 보여준다.=>ttbr1이 중복 기술된것 같습니다.
안녕하세요? 방랑객님,
말씀하신 바와 같이 오타를 수정하였습니다.
대단히 감사합니다. 즐거운 하루 되세요. ^^