cpu_replace_ttbr1

<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 한다.

 

6 thoughts to “cpu_replace_ttbr1”

    1. 안녕하세요?

      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

      감사합니다.

        1. isb 를 사용하는데도 불구하고 필요하다는 말씀이시군요..
          -> 네 그렇습니다.
          zero page 로의 mapping이 asid 0번을 사용하지 않는 이유와도 동일한 것이겠죠..?
          -> TTBR 레지스터에 asid 0번 + zero 페이지를 가리키는 물리주소가 되는 것입니다.

댓글 남기기