<kernel v5.10>
커널 페이지 테이블 지정(변경)
커널에서 사용하는 페이지 테이블을 가리키는 레지스터가 TTBR1 레지스터다. 이 레지스터에 다른 페이지 테이블을 설정하기 위해서는 특별한 처리 방법이 요구되는데, 곧 이어질 cpu_replace_ttbr1( ) 함수에서 자세히 설명하기로 한다.
다음 그림은 TTBR1 레지스터의 값을 바꿀 때 함수 간의 흐름을 보여준다.
cpu_replace_ttbr1()
arch/arm64/include/asm/mmu_context.h
/* * Atomically replaces the active TTBR1_EL1 PGD with a new VA-compatible PGD, * avoiding the possibility of conflicting TLB entries being allocated. */
static inline void cpu_replace_ttbr1(pgd_t *pgdp) { typedef void (ttbr_replace_func)(phys_addr_t); extern ttbr_replace_func idmap_cpu_replace_ttbr1; ttbr_replace_func *replace_phys; /* phys_to_ttbr() zeros lower 2 bits of ttbr with 52-bit PA */ phys_addr_t ttbr1 = phys_to_ttbr(virt_to_phys(pgdp)); if (system_supports_cnp() && !WARN_ON(pgdp != lm_alias(swapper_pg_dir))) { /* * cpu_replace_ttbr1() is used when there's a boot CPU * up (i.e. cpufeature framework is not up yet) and * latter only when we enable CNP via cpufeature's * enable() callback. * Also we rely on the cpu_hwcap bit being set before * calling the enable() function. */ ttbr1 |= TTBR_CNP_BIT; } replace_phys = (void *)__pa_symbol(idmap_cpu_replace_ttbr1); cpu_install_idmap(); replace_phys(ttbr1); cpu_uninstall_idmap(); }
페이지 테이블 물리 주소를 커널용 페이지 테이블 레지스터인 TTBR1에 어토믹(atomic)하게 설정한다. 가상 주소에서 이미 커널 코드가 동작하고 있기 때문에 TTBR1 레지스터를 곧바로 변경할 수 없으므로 idmap 페이지 테이블을 사용하여 어토믹하게 TTBR1 레지스터를 변경해야 한다. idmap 페이지 테이블을 사용할 때 유저용 가상 페이지 테이블을 가리키는 TTBR0 레지스터를 임시로 잠시 사용한다.
- 코드 라인 8에서 @pgdp에 해당하는 물리 주소를 구한다.
- 코드 라인 10~20에서 ARMv8.2 확장에 적용된 CNP(Common Not Private) capability가 동작하는 시스템인 경우 CNP 비트를 추가한다.
- 코드 라인 22에서 1:1 아이덴티티 매핑되어 있는 위치의 idmap_cpu_replace_ttbr1( ) 함수 가상 주소를 구한다.
- 코드 라인 24~26에서 1:1 아이덴티티 매핑 영역을 활성화한 후 idmap_cpu_replace_ttbr1( ) 함수를 호출하여 페이지 테이블의 주소를 TTBR1에 설정하고 그 후 1:1 아이덴티티 매핑을 해제한다.
다음 그림은 커널 페이지 테이블을 지정하는 TTBR1을 atomic하게 변경하는 모습을 보여준다.
idmap_cpu_replace_ttbr1()
arch/arm64/mm/proc.S
/* * void idmap_cpu_replace_ttbr1(phys_addr_t ttbr1) * * This is the low-level counterpart to cpu_replace_ttbr1, and should not be * called by anything else. It can only be executed from a TTBR0 mapping. */
ENTRY(idmap_cpu_replace_ttbr1) save_and_disable_daif flags=x2 __idmap_cpu_set_reserved_ttbr1 x1, x3 offset_ttbr1 x0 msr ttbr1_el1, x0 isb restore_daif x2 ret ENDPROC(idmap_cpu_replace_ttbr1) .popsection
.idmap.text 섹션에 위치한 이 함수의 코드는 head.S에서 이미 가상 주소와 물리 주소가 1:1 매핑이 된 상태로 구동될 수 있는 코드가 위치한다. TTBR1에 zero 페이지를 설정하고 TLB flush 및 isb를 수행하여 TTBR1을 먼저 깨끗하게 비운다. 그런 후 TTBR1에 요청한 페이지 테이블 물리 주소를 설정한다. 이 함수는 TTBR0를 사용하여 1:1 아이덴티티 매핑이 된 상태에서 동작된다.
- 코드 라인 2에서 상태 레지스터의 D, A, I, F 비트 값을 x2 레지스터로 백업한 후 D, A, I, F 비트를 설정한다.
- 코드 라인 4에서 커널 페이지 테이블을 담당하는 ttbr1을 제로 페이지에 매핑한다. ttbr1 제로 페이지를 담을 x1 레지스터와 ttbr1에 기록할 값이 x3 레지스터에 담긴다.
- 코드 라인 6~8에서 52 비트 주소를 사용하는 경우를 위해 ttbr1에 offset 값을 추가 적용한다.
- 코드 라인 10~12에서 상태 레지스터의 D, A, I, F 비트를 복원한 후 리턴한다.
save_and_disable_daif 매크로
arch/arm64/include/asm/assembler.h
.macro save_and_disable_daif, flags mrs \flags, daif msr daifset, #0xf .endm
상태 레지스터의 D, A, I, F 비트 값을 @flags 레지스터로 백업한 후 D, A, I, F 비트를 설정한다.
__idmap_cpu_set_reserved_ttbr1 매크로
arch/arm64/mm/proc.S
.macro __idmap_cpu_set_reserved_ttbr1, tmp1, tmp2 adrp \tmp1, empty_zero_page phys_to_ttbr \tmp2, \tmp1 offset_ttbr1 \tmp2 msr ttbr1_el1, \tmp2 isb tlbi vmalle1 dsb nsh isb .endm
커널 페이지 테이블을 담당하는 ttbr1을 제로 페이지에 매핑한다. ttbr1 제로 페이지를 담을 @tmp1 레지스터의 물리 주소를 사용하여 ttbr1에 기록할 값인 @tmp2 레지스터에 담는다. 그런 후 ttbr1에 기록한다.
- 코드 라인 2~3에서 zero 페이지를 @tmp1 레지스터에 설정한다.
- 코드 라인 4에서 ttbr1 레지스터에 사용할 값으로 @tmp2 레지스터에 물리 주소 @tmp1 레지스터 값을 대입한다.
- 코드 라인 5에서 ttbr1 레지스터에 @tmp2를 설정한다.
- 코드 라인 6~9에서 명령 파이프라인을 비우고, tlb 플러쉬를 수행하고 완료된 후 다시 명령 파이프라인을 비운다.
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
ttbr 레지스터에 사용할 값으로 @ttbr 레지스터에 물리 주소 @phys 레지스터 값을 대입한다. 만일 52bit 물리 주소를 사용하는 시스템인 경우 52 비트를 지원하는 format으로 변경한다.
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 #ifdef CONFIG_ARM64_USER_VA_BITS_52 orr \ttbr, \ttbr, #TTBR1_BADDR_4852_OFFSET #endif .endm
52비트 주소를 사용하는 경우를 위해 ttbr offset 값을 @ttbr에 반환한다.
Identity 매핑 테이블 사용
cpu_install_idmap()
arch/arm64/include/asm/mmu_context.h
static inline void cpu_install_idmap(void) { cpu_set_reserved_ttbr0(); local_flush_tlb_all(); cpu_set_idmap_tcr_t0sz(); cpu_switch_mm(lm_alias(idmap_pg_dir), &init_mm); }
아이덴티티 매핑을 이용하기 위해 TTBR0에 idmap 페이지 테이블을 설정한다. TTBR0를 유저용 페이지 테이블 변환 레지스터로 사용하지만 ARM64 부트업 과정에서는 idmap 페이지 테이블에 먼저 사용한다
- 코드 라인 3에서 zero 페이지 주소를 TTBR0 레지스터에 설정한다.
- 코드 라인 4에서 현재 cpu의 TLB를 flush한다.
- 코드 라인 5에서 idmap 페이지 테이블을 사용하기 전에 offset 지정을 위해 TCR 레지스터 T0SZ 필드 값을 설정한다.
- 코드 라인 7에서 idmap 페이지 테이블의 가상 주소를 물리 주소로 변환하여 TTBR0에 설정한다.
cpu_set_reserved_ttbr0()
arch/arm64/include/asm/mmu_context.h
/* * Set TTBR0 to empty_zero_page. No translations will be possible via TTBR0. */
static inline void cpu_set_reserved_ttbr0(void) { unsigned long ttbr = phys_to_ttbr(__pa_symbol(empty_zero_page)); write_sysreg(ttbr, ttbr0_el1); isb(); }
TTBR0를 zero 페이지를 가리키도록 설정한다. 주로 COW(Copy On Write)에 사용되는 특수 목적의 zero 페이지를 물리 주소로 변환하여 TTBR0 레지스터에 설정하고 인스트럭션 파이프라인을 비우는 것을 알 수 있다.
write_sysreg() 매크로 함수
arch/arm64/include/asm/sysreg.h
/* * The "Z" constraint normally means a zero immediate, but when combined with * the "%x0" template means XZR. */
#define write_sysreg(v, r) do { \ u64 __val = (u64)(v); \ asm volatile("msr " __stringify(r) ", %x0" \ : : "rZ" (__val)); \ } while (0)
레지스터 @r에 @v 값을 기록한다.
local_flush_tlb_all()
arch/arm64/include/asm/tlbflush.h
static inline void local_flush_tlb_all(void) { dsb(nshst); __tlbi(vmalle1); dsb(nsh); isb(); }
현재 cpu에 대해 TLB를 flush한다. flush 앞뒤로 완료되지 않은 캐시 등의 처리를 완료하고 함수를 나가기 전에 인스트럭션 파이프라인을 비워 다음 명령과 분리하는 배리어 작업을 한다.
cpu_set_idmap_tcr_t0sz() 매크로
arch/arm64/include/asm/mmu_context.h
#define cpu_set_idmap_tcr_t0sz() __cpu_set_tcr_t0sz(idmap_t0sz)
TCR 레지스터의 T0SZ 필드 값을 idmap_t0sz 값으로 설정하는 매크로 함수다. 전역 변수 idmap_t0sz는 컴파일 타임에 64 – VA_BITS 값이 설정되지만 커널 진입 전 head.S에서 idmap을 확장하여 만든 경우 설정된다. idmap 확장은 가상 주소가 사용하는 비트가 48비트보다 작은 커널에서 실제 물리 RAM 주소가 가상 주소보다 더 큰 경우 1:1로 아이덴티티 매핑을 할 수 없는 경우를 위해 사용한다. 따라서 이러한 설계를 사용하지 않는 시스템 구성에서는 기본적으로 사용하지 않는다.
__cpu_set_tcr_t0sz()
arch/arm64/include/asm/mmu_context.h
/* * Set TCR.T0SZ to its default value (based on VA_BITS) */
static inline void __cpu_set_tcr_t0sz(unsigned long t0sz) { unsigned long tcr; if (!__cpu_uses_extended_idmap()) return; tcr = read_sysreg(tcr_el1); tcr &= ~TCR_T0SZ_MASK; tcr |= t0sz << TCR_T0SZ_OFFSET; write_sysreg(tcr, tcr_el1); isb(); }
TCR.T0SZ에 t0sz를 설정한다. TCR 레지스터를 읽은 값의 0번 비트에 t0sz 값의 lsb 16비트를 복사하여 다시 TCR 레지스터에 기록한다.
__cpu_uses_extended_idmap()
arch/arm64/include/asm/mmu_context.h
static inline bool __cpu_uses_extended_idmap(void) { if (IS_ENABLED(CONFIG_ARM64_USER_VA_BITS_52)) return false; return unlikely(idmap_t0sz != TCR_T0SZ(VA_BITS)); }
확장 idmap을 사용하는 커널 설정인 경우에는 true를 리턴한다. 52 비트 가상 주소를 지원하는 커널의 경우 false를 리턴한다.
Identity 매핑 테이블 사용 해제
cpu_uninstall_idmap()
arch/arm64/include/asm/mmu_context.h
/* * Remove the idmap from TTBR0_EL1 and install the pgd of the active mm. * * The idmap lives in the same VA range as userspace, but uses global entries * and may use a different TCR_EL1.T0SZ. To avoid issues resulting from * speculative TLB fetches, we must temporarily install the reserved page * tables while we invalidate the TLBs and set up the correct TCR_EL1.T0SZ. * * If current is a not a user task, the mm covers the TTBR1_EL1 page tables, * which should not be installed in TTBR0_EL1. In this case we can leave the * reserved page tables in place. */
static inline void cpu_uninstall_idmap(void) { struct mm_struct *mm = current->active_mm; cpu_set_reserved_ttbr0(); local_flush_tlb_all(); cpu_set_default_tcr_t0sz(); if (mm != &init_mm && !system_uses_ttbr0_pan()) cpu_switch_mm(mm->pgd, mm); }
아이덴티티 매핑을 다 사용한 경우 현재 태스크에 사용하는 페이지 테이블을 다시 TTBR0에 설정한다.
- 코드 라인 5에서 zero 페이지 주소를 TTBR0 레지스터에 설정한다.
- 코드 라인 6에서 현재 cpu의 TLB를 flush한다.
- 코드 라인 7에서 idmap 페이지 테이블을 사용하지 않을 때 커널 기본 t0sz 설정 값을 tcr_el1에 설정한다.
- 코드 라인 9~10에서 mm 스위칭을 한다. mm_struct 구조체가 커널 초기화 시 사용한 init_mm이 아닌 경우 mm에서 사용하는 페이지 테이블로 TTBR0 레지스터를 설정한다.
cpu_set_default_tcr_t0sz()
arch/arm64/include/asm/mmu_context.h
#define cpu_set_default_tcr_t0sz() __cpu_set_tcr_t0sz(TCR_T0SZ(VA_BITS))
TCR 레지스터 T0SZ 필드의 기본 값으로 ‘64 – 가상 주소가 사용하는 비트 수’를 설정한다(64 – VA_BITS(39) = 25).
TCR_T0SZ() 매크로
arch/arm64/include/asm/pgtable-hwdef.h
#define TCR_T0SZ(x) ((UL(64) - (x)) << TCR_T0SZ_OFFSET)
ARM64 커널에서 64 – x 값을 갖는다(TCR_T0SZ_OFFSET = 0)
mm 스위칭
cpu_switch_mm()
arch/arm64/include/asm/mmu_context.h”
static inline void cpu_switch_mm(pgd_t *pgd, struct mm_struct *mm) { BUG_ON(pgd == swapper_pg_dir); cpu_set_reserved_ttbr0(); cpu_do_switch_mm(virt_to_phys(pgd),mm); }
요청한 유저용 가상 주소 공간으로 스위칭하기 위해 인자로 받은 페이지 테이블 가상 주소를 물리 주소로 변환하여 유저용 페이지 테이블 주소 레지스터에 설정하고 이어서 cpu_do_switch_mm( ) 함수를 호출한다.
mm 스위칭은 주로 스케줄러의 _ _schedule( ) 함수를 통해 컨텍스트 스위칭이 일어날 때 mm 스위칭 파트와 태스크 스위칭 두 파트가 동작하게 된다. 그중 태스크 스위칭이 다음 태스크를 위해 레지스터 백업/복구 및 스택을 준비하는 과정이다. 그리고 mm 스위칭은 유저 태스크에 대해서만 유저가 사용하는 가상 주소 환경을 준비하기 위해 사용한다. 해당 유저 태스크가 사용하는 pgd 테이블을 TTBR0 레지스터에 지정하는 것이 핵심이다.
cpu_do_switch_mm()
arch/arm64/mm/context.c
void cpu_do_switch_mm(phys_addr_t pgd_phys, struct mm_struct *mm) { unsigned long ttbr1 = read_sysreg(ttbr1_el1); unsigned long asid = ASID(mm); unsigned long ttbr0 = phys_to_ttbr(pgd_phys); /* Skip CNP for the reserved ASID */ if (system_supports_cnp() && asid) ttbr0 |= TTBR_CNP_BIT; /* SW PAN needs a copy of the ASID in TTBR0 for entry */ if (IS_ENABLED(CONFIG_ARM64_SW_TTBR0_PAN)) ttbr0 |= FIELD_PREP(TTBR_ASID_MASK, asid); /* Set ASID in TTBR1 since TCR.A1 is set */ ttbr1 &= ~TTBR_ASID_MASK; ttbr1 |= FIELD_PREP(TTBR_ASID_MASK, asid); write_sysreg(ttbr1, ttbr1_el1); isb(); write_sysreg(ttbr0, ttbr0_el1); isb(); post_ttbr_update_workaround(); }
두 번째 인자로 받은 @mm의 context.id(x1)에서 하위 16비트를 ASID 값으로 하여, 첫 번째 인자로 받은 페이지 테이블 물리 주소 @pgd_phys의 비트 [63:48]에 복사하고 이를 ttbr0_el1에 설정한다. ttbr0_el1 레지스터는 상위 16비트는 ASID를 저장하고 나머지 하위 비트들은 페이지 테이블의 물리 주소를 가리킨다.
- 코드 라인 3에서 ttbr1 값을 읽어온다.
- 코드 라인 4에서 두 번째 인자로 받은 @mm의 context.id를 알아와서 asid에 대입한다.
- 코드 라인 5에서 첫 번째 인자로 받은 pgd 엔트리의 물리주소 @pgd_phys pgd를 사용하여 ttbr0 값으로 변환한다.
- 코드 라인 8~9에서 ARMv8.2 확장에 적용된 CNP(Common Not Private) capability가 동작하는 시스템인 경우 asid 값이 있으면 ttbr 값에서 CNP 비트를 추가 설정한다.
- 코드 라인 12~13에서 PAN SW 에뮬레이션 기능이 사용되는 경우 ASID 필드를 ttbr0에도 설정한다.
- 코드 라인 16~17에서 ttbr1의 ASID 필드도 갱신한다.
- 코드 라인 19~22에서 ttbr1과 ttbr0를 갱신하고 명령 파이프라인을 비운다.
- 코드 라인 23에서 TTBR을 갱신한 후 아키텍처마다 추가적으로 수행해야할 루틴이 있으면 수행한다.
- cavium SoC를 사용한 경우 TTBR을 갱신한 후 명령 캐시 및 파이프 라인을 추가적으로 flush 한다.
ASID() 매크로
arch/arm64/include/asm/mmu.h
/* * This macro is only used by the TLBI and low-level switch_mm() code, * neither of which can race with an ASID change. We therefore don't * need to reload the counter using atomic64_read(). */
#define ASID(mm) ((mm)->context.id.counter & 0xffff)
mm_struct 포인터인 @mm을 사용하여 mm->context.id.counter의 하위 16비트값인 asid를 알아온다.
post_ttbr_update_workaround()
arch/arm64/mm/context.c
/* Errata workaround post TTBRx_EL1 update. */
asmlinkage void post_ttbr_update_workaround(void) { if (!IS_ENABLED(CONFIG_CAVIUM_ERRATUM_27456)) return; asm(ALTERNATIVE("nop; nop; nop", "ic iallu; dsb nsh; isb", ARM64_WORKAROUND_CAVIUM_27456, CONFIG_CAVIUM_ERRATUM_27456)); }
cavium SoC를 사용한 경우 TTBRx를 갱신한 후 명령 캐시 및 파이프 라인을 추가적으로 flush 한다.
안녕하세요. 궁금한게 있는데.. ttbr 을 변경할때 왜 zero page 에 맵핑이 먼저 필요한가요..?
안녕하세요?
ARM64 아키텍처의 TTBRx을 변경 시 반드시 그렇게 해야 하기 때문입니다.
ARM64 아키텍처에서 순간적으로 잘못된 TLB 엔트리로 인해 충돌가능성이 있어
zero 페이지에 매핑 후 TLB 엔트리를 모두 flush하여 비워둔 후
다시 TTBRx에 테이블을 지정하는 방식으로 사용해야
SError (시스템 에러)로 정지하는 일이 없도록 하기 위함입니다.
참고: arm64: mm: add code to safely replace TTBR1_EL1
– https://github.com/torvalds/linux/commit/50e1881ddde2a986c7d0d2150985239e5e3d7d96#diff-06e4e0038f65519bf8ab7562398c81be
감사합니다.
isb 를 사용하는데도 불구하고 필요하다는 말씀이시군요..
zero page 로의 mapping이 asid 0번을 사용하지 않는 이유와도 동일한 것이겠죠..?
isb 를 사용하는데도 불구하고 필요하다는 말씀이시군요..
-> 네 그렇습니다.
zero page 로의 mapping이 asid 0번을 사용하지 않는 이유와도 동일한 것이겠죠..?
-> TTBR 레지스터에 asid 0번 + zero 페이지를 가리키는 물리주소가 되는 것입니다.
답변 정말 감사합니다. TLB를 이해하는데 도움이 되었어요 정말 감사합니다!
저도 다시 한번 보게되니 좋습니다. 감사합니다. ^^