<kernel v5.4>
가상화 지원 (하이퍼 모드)
가상화를 지원하기 위해서 AVE(Architecture Virtualization Extension)가 필요하다.
- ARM32에서는 SoC 마다 탑재 여부가 다르다.
- ARM64에서는 대부분 내장되어 있다.
ARM32와 ARM64 시스템의에서 다음과 같은 하이퍼 바이저 운영 모드들이 지원된다. 대부분의 설명은 ARM64를 디폴트로 한다.
- ARM32
- 하이퍼 바이저는 HYP 모드에서 운영
- Guest OS는 SVC 모드에서 운영
- AMR64
- 하이퍼 바이저는 EL2에서 운영
- Guest OS는 EL1 모드에서 운영
하이퍼 바이저를 운영하는 방법은 두 가지가 있다. (Host OS & Guest OS = 리눅스 OS)
- Type 1
- EL2에서 하이퍼바이저용 전용 OS가 운영되고, EL1에서 Guest OS가 운영된다.
- Type 2
- EL2에서 Host OS가 운영되고, EL1에서 Guest OS가 운영된다.
ARM 리눅스는 Type 2에서 운영될 때 EL2 모드와 EL1 모드 두 가지를 모두 지원하기 위해 다음과 같은 방법을 고려하였고, 현재는 첫 번째 방법을 사용하고 있다. 두 번째 방법을 구현하려면 상당히 많은 양의 추가 코드들이 필요한 상태이다.
- 첫 번째, EL2 스위칭 + EL1 Host & Guest OS
- Host OS를 EL2로 부팅한 후 EL2에 관련 스위칭(irq 라우팅 포함) 코드만 남겨 놓고 EL1으로 전환하여 운영한다. 그 후 EL1에서 Guest OS를 동작시킨다.
- 예) Host용 리눅스 커널이 EL2에서 부팅되고, EL1으로 전환한 후 QEMU(enable-kvm)를 사용하여 EL1에서 Guest용 리눅스 커널을 동작시킨다.
- Host OS를 EL2로 부팅한 후 EL2에 관련 스위칭(irq 라우팅 포함) 코드만 남겨 놓고 EL1으로 전환하여 운영한다. 그 후 EL1에서 Guest OS를 동작시킨다.
- 두 번째, EL2 Host OS + EL1 Guest OS
- Host OS를 EL2로 부팅하여 운영하고, EL1에서 Guest OS를 동작시킨다.
하이퍼모드 지원 유무 확인
하이퍼모드로 부팅된 경우인지 아닌지 메시지(dmesg)를 출력하여 안내한다.
ARM64 예)
- EL2 부팅
- “CPU: All CPU(s) started at EL2“
- EL1 부팅
- “CPU: All CPU(s) started at EL1”
ARM32 예)
- HYP 모드 부팅
- “CPU: All CPU(s) started in HYP mode.”
- “CPU: Virtualization extensions available.”
- SVC 모드 부팅
- “CPU: All CPU(s) started in SVC mode.”
Secure EL2 support
- ARMv8.4 아키텍처부터 Secure EL2를 지원하기 시작하였다.
하이퍼 모드 체크
hyp_mode_check() – ARM64
arch/arm64/kernel/setup.c
static void __init hyp_mode_check(void) { if (is_hyp_mode_available()) pr_info("CPU: All CPU(s) started at EL2\n"); else if (is_hyp_mode_mismatched()) WARN_TAINT(1, TAINT_CPU_OUT_OF_SPEC, "CPU: CPUs started in inconsistent modes"); else pr_info("CPU: All CPU(s) started at EL1\n"); }
커널이 EL2로 부팅되었는지 EL1으로 부팅되었는지 여부를 메시지로 출력한다.
hyp_mode_check() – ARM32
arch/arm/kernel/setup.c
#ifndef ZIMAGE void __init hyp_mode_check(void) { #ifdef CONFIG_ARM_VIRT_EXT sync_boot_mode(); if (is_hyp_mode_available()) { pr_info("CPU: All CPU(s) started in HYP mode.\n"); pr_info("CPU: Virtualization extensions available.\n"); } else if (is_hyp_mode_mismatched()) { pr_warn("CPU: WARNING: CPU(s) started in wrong/inconsistent modes (primary CPU mode 0x%x)\n", __boot_cpu_mode & MODE_MASK); pr_warn("CPU: This may indicate a broken bootloader or firmware.\n"); } else pr_info("CPU: All CPU(s) started in SVC mode.\n"); #endif } #endif
커널이 HYP 모드로 부팅되었는지 SVC 모드로 부팅되었는지 여부를 메시지로 출력한다.
하이퍼 모드 운영 여부
is_hyp_mode_available() – ARM64
arch/arm64/include/asm/virt.h
/* Reports the availability of HYP mode */ static inline bool is_hyp_mode_available(void) { return (__boot_cpu_mode[0] == BOOT_CPU_MODE_EL2 && __boot_cpu_mode[1] == BOOT_CPU_MODE_EL2); }
boot cpu가 EL2로 부팅되었는지 여부를 알아온다.
- __boot_cpu_mode[] 저장 루틴은 다음을 참고한다.
- 참고: head.S 전체 | 문c
is_hyp_mode_available() – ARM32
arch/arm/include/asm/virt.h
/* Reports the availability of HYP mode */ static inline bool is_hyp_mode_available(void) { return ((__boot_cpu_mode & MODE_MASK) == HYP_MODE && !(__boot_cpu_mode & BOOT_CPU_MODE_MISMATCH)); }
boot cpu가 EL2로 부팅되었는지 여부를 알아온다.
기타
sync_boot_mode()
arch/arm/include/asm/virt.h
/* * __boot_cpu_mode records what mode the primary CPU was booted in. * A correctly-implemented bootloader must start all CPUs in the same mode: * if it fails to do this, the flag BOOT_CPU_MODE_MISMATCH is set to indicate * that some CPU(s) were booted in a different mode. * * This allows the kernel to flag an error when the secondaries have come up. */
extern int __boot_cpu_mode; static inline void sync_boot_mode(void) { /* * As secondaries write to __boot_cpu_mode with caches disabled, we * must flush the corresponding cache entries to ensure the visibility * of their writes. */ sync_cache_r(&__boot_cpu_mode); }
- 전역 __boot_cpu_mode 변수 영역에 대해 inner & outer 캐시 flush를 수행한다.
sync_cache_r()
arch/arm/include/asm/cacheflush.h
#define sync_cache_r(ptr) __sync_cache_range_r(ptr, sizeof *(ptr))
- long으로 선언된 전역 __boot_cpu_mode 변수 위치에 대해 inner & outer 캐시 flush를 수행한다.
- long 값이므로 32bit 시스템에서는 4 byte , 64bit 시스템에서는 8 byte 영역만큼에 대해 flush를 수행하게 된다.
__sync_cache_range_r()
arch/arm/include/asm/cacheflush.h
/* * Ensure preceding writes to *p by other CPUs are visible to * subsequent reads by this CPU. We must be careful not to * discard data simultaneously written by another CPU, hence the * usage of flush rather than invalidate operations. */
static inline void __sync_cache_range_r(volatile void *p, size_t size) { char *_p = (char *)p; #ifdef CONFIG_OUTER_CACHE if (outer_cache.flush_range) { /* * Ensure dirty data migrated from other CPUs into our cache * are cleaned out safely before the outer cache is cleaned: */ __cpuc_clean_dcache_area(_p, size); /* Clean and invalidate stale data for *p from outer ... */ outer_flush_range(__pa(_p), __pa(_p + size)); } #endif /* ... and inner cache: */ __cpuc_flush_dcache_area(_p, size); }
지정된 range에 대해 inner & outer 캐시 flush를 수행한다.
- CONFIG_OUTER_CACHE
- outer 캐시가 사용되는 시스템에서 사용하는 커널 옵션
- rpi2: outer 캐시를 사용하지 않는다.
- if (outer_cache.flush_range) {
- outer 캐시를 사용하는 시스템에서 flush_range에 연결된 함수가 존재하는 경우
- __cpuc_clean_dcache_area(_p, size);
- 지정 range의 outer 캐시를 flush하기 전에 먼저 cpu 캐시 즉 inner 캐시를 먼저 clean 작업을 해야한다.
- ARMv7:
- inner d-cache 영역에 대해 clean 오퍼레이션 대신 flush를 구현하였다.
- outer_flush_range(__pa(_p), __pa(_p + size));
- 지정 range의 outer cache에 대한 flush(clean & invalidate)를 수행한다.
- __cpuc_flush_dcache_area(_p, size);
- 지정 range의 inner d-cache에 대한 flush(clean & invalidate)를 수행한다.
arch/arm/include/asm/cacheflush.h
/* * There is no __cpuc_clean_dcache_area but we use it anyway for * code intent clarity, and alias it to __cpuc_flush_dcache_area. */
#define __cpuc_clean_dcache_area __cpuc_flush_dcache_area
- ARMv7에서는 clean 구현을 따로 준비하지 않았고 따라서 flush 구현을 사용한다.
arch/arm/include/asm/cacheflush.h
#define __cpuc_flush_dcache_area cpu_cache.flush_kern_dcache_area
- ARMv7: v7_flush_kern_dcache_area()
outer_flush_range()
arch/arm/include/asm/outercache.h
/** * outer_flush_range - clean and invalidate outer cache lines * @start: starting physical address, inclusive * @end: end physical address, exclusive */
static inline void outer_flush_range(phys_addr_t start, phys_addr_t end) { if (outer_cache.flush_range) outer_cache.flush_range(start, end); }
- outer_cache가 구현된 머신에서 지정 range의 outer cache에 대한 flush(clean & invalidate)를 수행한다.
is_hyp_mode_mismatched()
arch/arm/include/asm/virt.h
/* Check if the bootloader has booted CPUs in different modes */ static inline bool is_hyp_mode_mismatched(void) { return !!(__boot_cpu_mode & BOOT_CPU_MODE_MISMATCH); }
arch/arm/include/asm/virt.h
/* * Flag indicating that the kernel was not entered in the same mode on every * CPU. The zImage loader stashes this value in an SPSR, so we need an * architecturally defined flag bit here. */
#define BOOT_CPU_MODE_MISMATCH PSR_N_BIT
arch/arm/include/uapi/asm/ptrace.h
/* * PSR bits * Note on V7M there is no mode contained in the PSR */
#define USR26_MODE 0x00000000 #define FIQ26_MODE 0x00000001 #define IRQ26_MODE 0x00000002 #define SVC26_MODE 0x00000003 #if defined(__KERNEL__) && defined(CONFIG_CPU_V7M) /* * Use 0 here to get code right that creates a userspace * or kernel space thread. */ #define USR_MODE 0x00000000 #define SVC_MODE 0x00000000 #else #define USR_MODE 0x00000010 #define SVC_MODE 0x00000013 #endif #define FIQ_MODE 0x00000011 #define IRQ_MODE 0x00000012 #define ABT_MODE 0x00000017 #define HYP_MODE 0x0000001a #define UND_MODE 0x0000001b #define SYSTEM_MODE 0x0000001f #define MODE32_BIT 0x00000010 #define MODE_MASK 0x0000001f
arch/arm/include/uapi/asm/ptrace.h
/* * Use 0 here to get code right that creates a userspace * or kernel space thread. */
#define USR_MODE 0x00000000 #define SVC_MODE 0x00000000 #else #define USR_MODE 0x00000010 #define SVC_MODE 0x00000013 #endif #define FIQ_MODE 0x00000011 #define IRQ_MODE 0x00000012 #define ABT_MODE 0x00000017 #define HYP_MODE 0x0000001a #define UND_MODE 0x0000001b #define SYSTEM_MODE 0x0000001f #define MODE32_BIT 0x00000010 #define MODE_MASK 0x0000001f
전역 변수
__boot_cpu_mode – ARM64
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. */
extern u32 __boot_cpu_mode[2];
arch/arm64/kernel/hyp-stub.S
/* * 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. */
ENTRY(__boot_cpu_mode) .long BOOT_CPU_MODE_EL2 .long BOOT_CPU_MODE_EL1
__boot_cpu_mode – ARM32
arch/arm/include/asm/virt.h
/* * __boot_cpu_mode records what mode the primary CPU was booted in. * A correctly-implemented bootloader must start all CPUs in the same mode: * if it fails to do this, the flag BOOT_CPU_MODE_MISMATCH is set to indicate * that some CPU(s) were booted in a different mode. * * This allows the kernel to flag an error when the secondaries have come up. */
extern int __boot_cpu_mode;
arch/arm/kernel/hyp-stub.S
/* * For the kernel proper, 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. */
.data ENTRY(__boot_cpu_mode) .long 0
참고
- Next steps in KVM enablement on ARM (2015) | ARM
- Isolation using virtualization in the Secure world (2018) | ARM – 다운로드 pdf, 다운로드 pdf