ARM64 페이지 테이블 -3- (API)

<kernel v5.0>

ARM64 리눅스에서 사용하는 페이지 테이블 관련 명령(pgd, pud, pmd, pte)을 알아본다.

  • ARM64는 2~4 레벨 페이지 테이블을 사용한다.
    • 4 레벨 사용 시: pgd -> pud -> pmd -> pte
    • 3 레벨 사용 시: pgd(pud) -> pmd -> pte
    • 2 레벨 사용 시: pgd(pud, pmd) -> pte
  • 페이지 테이블은 4K 바이트를 사용하고, 각 엔트리는 8바이트이다.

 

엔트리 타입

다음 4 가지 테이블 엔트리들이 사용하는 타입 들(pgdval_t, pudval_t, pmdval_t, pteval_t)은 모두 8바이트를 사용하는 u64 타입이다.

  • pgd_t
    • typedef struct { pgdval_t pgd; } pgd_t;
  • pud_t
    • typedef struct { pudval_t pud; } pud_t;
  • pmd_t
    • typedef struct { pmdval_t pmd; } pmd_t;
  • pte_t
    • typedef struct { pteval_t pte; } pte_t;

 

주요 매크로 상수

다음은 ARM64 커널에서 사용되는 각종 페이지 테이블 관련 매크로 상수 값이다.

 


 

APIs

엔트리 값

p*d_val()

arch/arm64/include/asm/pgtable-types.h

#define pgd_val(x)      ((x).pgd)
#define pud_val(x)      ((x).pud)
#define pmd_val(x)      ((x).pmd)
#define pte_val(x)      ((x).pte)

pgd | pud | pmd | pte 엔트리 값을 반환한다.

 

다음 그림은 p*d_val() 함수의 처리 과정을 보여준다.

 

엔트리 인덱스

p*d_index()

arch/arm64/include/asm/pgtable.h

/* to find an entry in a page-table-directory */
#define pgd_index(addr)         (((addr) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
/* Find an entry in the frst-level page table. */
#define pud_index(addr)         (((addr) >> PUD_SHIFT) & (PTRS_PER_PUD - 1))
/* Find an entry in the second-level page table. */
#define pmd_index(addr)         (((addr) >> PMD_SHIFT) & (PTRS_PER_PMD - 1))
/* Find an entry in the third-level page table. */
#define pte_index(addr)         (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))

주어진 가상 주소에서 pgd | pud | pmd | pte 인덱스 값을 추출하여 리턴한다.

 

다음 그림은 4K 페이지, VA_BITS=39 커널 옵션을 가진 시스템에서 p*d_index() 함수의 처리 과정을 보여준다.


오프셋 (엔트리 포인터)

다음 그림은 엔트리 포인터를 알아오는 다양한 offset API들이 동작하는 과정을 보여준다.

 

pgd_offset()

arch/arm64/include/asm/pgtable.h

#define pgd_offset(mm, addr)    (pgd_offset_raw((mm)->pgd, (addr)))

@mm이 가리키는 pgd 테이블에서 가상 주소 @addr에 해당하는 pgd 엔트리 포인터를 알아온다.

 

pgd_offset_k()

arch/arm64/include/asm/pgtable.h

#define pgd_offset_k(addr)      pgd_offset(&init_mm, addr)

“_k” suffix(접미) 는 커널이라는 뜻이다. 즉 커널이 사용하는 pgd 테이블에서 가상 주소 @addr에 해당하는 pgd 엔트리 포인터를 알아온다.

 

pgd_offset_raw()

arch/arm64/include/asm/pgtable.h

#define pgd_offset_raw(pgd, addr)       ((pgd) + pgd_index(addr))

@pgd 테이블에서 가상 주소 @addr에 해당하는 pgd 엔트리 포인터를 알아온다.

 

pud_offset()

arch/arm64/include/asm/pgtable.h

#define pud_offset(dir, addr)           ((pud_t *)__va(pud_offset_phys((dir), (addr))))

pgd 엔트리 포인터 @dir이 가리키는 pud 테이블에서 @addr에 해당하는 pud 엔트리 포인터를 알아온다.

 

pud_offset_phys()

arch/arm64/include/asm/pgtable.h

#define pud_offset_phys(dir, addr)      (pgd_page_paddr(READ_ONCE(*(dir))) + pud_index(addr) * sizeoo
f(pud_t))

pgd 엔트리 포인터 @dir이 가리키는 pud 테이블에서 @addr에 해당하는 pud 엔트리의 물리 주소를 알아온다.

 

pmd_offset()

arch/arm64/include/asm/pgtable.h

#define pmd_offset(dir, addr)           ((pmd_t *)__va(pmd_offset_phys((dir), (addr))))

pud 엔트리 포인터 @dir이 가리키는 pmd 테이블에서 @addr에 해당하는 pmd 엔트리 포인터를 알아온다.

 

pmd_offset_phys()

arch/arm64/include/asm/pgtable.h

#define pmd_offset_phys(dir, addr)      (pud_page_paddr(READ_ONCE(*(dir))) + pmd_index(addr) * sizeoo
f(pmd_t))

pud 엔트리 포인터 @dir이 가리키는 pmd 테이블에서 @addr에 해당하는 pmd 엔트리의 물리 주소를 알아온다.

 

pte_offset_kernel()

arch/arm64/include/asm/pgtable.h

#define pte_offset_kernel(dir,addr)     ((pte_t *)__va(pte_offset_phys((dir), (addr))))

pmd 엔트리 포인터 @dir이 가리키는 pte 테이블에서 @addr에 해당하는 pte 엔트리 포인터를 알아온다.

 

pte_offset_phys()

arch/arm64/include/asm/pgtable.h

#define pte_offset_phys(dir,addr)       (pmd_page_paddr(READ_ONCE(*(dir))) + pte_index(addr) * sizeoo
f(pte_t))

pmd 엔트리 포인터 @dir이 가리키는 pte 테이블에서 @addr에 해당하는 pte 엔트리의 물리 주소를 알아온다.

 

pte_offset_kimg()

arch/arm64/include/asm/pgtable.h

#define pte_offset_kimg(dir,addr)       ((pte_t *)__phys_to_kimg(pte_offset_phys((dir), (addr))))

static하게 할당된 커널 이미지용 pmd 엔트리 포인터 @dir이 가리키는 pte 테이블에서 @addr에 해당하는 pte 엔트리 포인터를 알아온다.


페이지 변환

다음과 같이 pgd, pud, pmd 및 pte 엔트리 포인터가 가리키는 다음 레벨의 테이블, 블록 및 페이지에 해당하는 페이지 디스크립터를 알아온다.

 

pgd_page()

arch/arm64/include/asm/pgtable.h

#define pgd_page(pgd)           pfn_to_page(__phys_to_pfn(__pgd_to_phys(pgd)))

@pgd 엔트리 포인터에서 읽은 물리 주소에 해당하는 페이지 디스크립터를 알아온다.

  • pgd 엔트리 포인터 -> pgd 엔트리 물리 주소 -> pfn -> page 포인터

 

__pgd_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pgd_to_phys(pgd)      __pte_to_phys(pgd_pte(pgd))

@pgd 엔트리 포인터에서 읽은 pgd 엔트리 값을 물리 주소로 변환해온다.

 

pgd_pte()

arch/arm64/include/asm/pgtable.h

static inline pte_t pgd_pte(pgd_t pgd)
{
        return __pte(pgd_val(pgd));
}

pgd_t 타입을 pte_t 타입으로 변환한다.

 

__pte_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pte_to_phys(pte)      (pte_val(pte) & PTE_ADDR_MASK)

@pgd 엔트리 포인터에서 읽은 pgd 엔트리 값에서 물리 주소 값만을 읽어온다.

  • 위 소스는 52비트 미만의 물리 주소를 사용하는 경우에 한정한다.

 

pud_page()

arch/arm64/include/asm/pgtable.h

#define pud_page(pud)           pfn_to_page(__phys_to_pfn(__pud_to_phys(pud)))

@pud 엔트리 포인터에서 읽은 물리 주소에  해당하는 페이지 디스크립터를 알아온다.

 

__pud_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pud_to_phys(pud)      __pte_to_phys(pud_pte(pud))

@pud 엔트리 포인터에서 읽은 pud 엔트리 값에서 물리 주소 값만을 읽어온다.

 

pud_pte()

arch/arm64/include/asm/pgtable.h

static inline pte_t pud_pte(pud_t pud)
{
        return __pte(pud_val(pud));
}

pud_t 타입을 pte_t 타입으로 변환한다.

 

pmd_page()

arch/arm64/include/asm/pgtable.h

#define pmd_page(pmd)           pfn_to_page(__phys_to_pfn(__pmd_to_phys(pmd)))

@pmd 엔트리 포인터에서 읽은 물리 주소에 해당하는 페이지 디스크립터를 알아온다.

 

__pmd_to_phys()

arch/arm64/include/asm/pgtable.h

#define __pmd_to_phys(pmd)      __pte_to_phys(pmd_pte(pmd))

@pmd 엔트리 포인터에서 읽은 pmd 엔트리 값에서 물리 주소 값만을 읽어온다.

 

pmd_pte()

arch/arm64/include/asm/pgtable.h

static inline pte_t pmd_pte(pmd_t pmd)
{
        return __pte(pmd_val(pmd));
}

pmd_t 타입을 pte_t 타입으로 변환한다.

 

pte_page()

arch/arm64/include/asm/pgtable.h

#define pte_page(pte)           (pfn_to_page(pte_pfn(pte)))

@pte 엔트리 포인터에서 읽은 물리 주소에 해당하는 페이지 디스크립터를 알아온다.

 

pte_pfn()

arch/arm64/include/asm/pgtable.h

#define pte_pfn(pte)            (__pte_to_phys(pte) >> PAGE_SHIFT)

@pte 엔트리 포인터에서 읽은 pte 물리 주소에 해당하는 pfn 값을 반환한다.

 


테이블 엔트리의 범위

pgd_addr_end()

include/asm-generic/pgtable.h

/*
 * When walking page tables, get the address of the next boundary,
 * or the end address of the range if that comes earlier.  Although no
 * vma end wraps to 0, rounded up __boundary may wrap to 0 throughout.
 */

#define pgd_addr_end(addr, end)                                         \
({      unsigned long __boundary = ((addr) + PGDIR_SIZE) & PGDIR_MASK;  \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})

가상 주소 @addr가 주어질 때 @end 범위내에서 pgd  엔트리가 담당하는 사이즈 단위로 정렬된 끝 주소를 반환한다. 반환 값이 @end를 넘어가면 @end 주소를 반환한다.

 

다음 그림은 4K 페이지, VA_BITS=48 커널 옵션을 사용한 경우 pgd_addr_end() 함수가 차례로 호출될 때 반환되는 값을 보여준다.

  • @addr=0x1234_5678_9000, @end=0x1280_0000_0000
    • 결과는 0x1280_0000_0000
  • @addr=0x1280_0000_0000, @end=0x1300_0000_0000
    • 결과는 0x1300_0000_0000
  • @addr=0x1300_0000_0000, @end=0x1334_5678_9000
    • 결과는 0x1334_5678_9000

 

pud_addr_end()

include/asm-generic/pgtable.h

#define pud_addr_end(addr, end)                                         \
({      unsigned long __boundary = ((addr) + PUD_SIZE) & PUD_MASK;      \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})

가상 주소 @addr가 주어질 때 @end 범위내에서 pud  엔트리가 담당하는 사이즈 단위로 정렬된 끝 주소를 반환한다. 반환 값이 @end를 넘어가면 @end 주소를 반환한다.

 

pmd_addr_end()

include/asm-generic/pgtable.h

#define pmd_addr_end(addr, end)                                         \
({      unsigned long __boundary = ((addr) + PMD_SIZE) & PMD_MASK;      \
        (__boundary - 1 < (end) - 1)? __boundary: (end);                \
})

가상 주소 @addr가 주어질 때 @end 범위내에서 pmd  엔트리가 담당하는 사이즈 단위로 정렬된 끝 주소를 반환한다. 반환 값이 @end를 넘어가면 @end 주소를 반환한다.

 

참고

 

ARM64 페이지 테이블 -2- (매핑)

<kernel v5.10>

ARM64 페이지 테이블 -2- (매핑)

 

ARM64 커널 메모리 맵

ARM64 커널에서 커널용 가상 주소 영역을 좀 더 자세히 알아보면, 그 영역의 절반을 잘라서 2개로 나누어 이용한다. 하위 절반은 물리 메모리를 1:1로 미리 리니어 매핑하여 사용하고, 나머지 상위 절반은 fixmap, vmalloc, vmemmap, pci-iomap 영역 및 커널 이미지 영역으로 나누어 사용한다.

 

그중에서 fixmap과 vmalloc 영역 동적으로 매핑하여 사용할 수 있는 공간으로 사용한다. 커널 주소 공간을 제외한 그 외 매핑 영역별 용도는 다음과 같다.

  • fixmap: 컴파일 타임에 목적에 따라 가상 주소 공간이 이미 결정된 매핑 영역이다.
  • vmalloc: 런타임에 연속된 가상 주소 공간을 자유롭게 매핑할 수 있는 영역이다.
  • vmalloc( ) 함수가 vmap( ) 함수를 통해 이용하는 곳이고, ioremap( ) 함수 역시 사용하는 영역이다.
  • pci-iomap: PCI 디바이스의 메모리 맵 I/O 영역으로 사용하는 영역이다. pci-ioremap 영역은 PCI 디바이스만 사용하는 데 반해 일반 ioremap 함수를 통해 매핑하는 곳은 vmalloc 영역임을 주의해야 한다.
  • vmemmap: 빠른 접근을 위해 분산된 page 구조체들을 이 영역에 매핑하기 위해 사용한다.

 


페이징 초기화

페이징에 사용하는 커널용 페이지 테이블을 초기화한다. 아직 커널 메모리 영역이 매핑되지 않은 상태이므로 memblock을 사용하여 페이지를 할당받아도 이에 해당하는 가상 주소가 없으므로 곧바로 할당받은 페이지에 접근할 수 없다. 커널의 부트업 과정 초반에는 이렇게 메모리 관리 및 매핑 관리가 동작하기 전 상태이므로 고정 매핑(fixmap) 주소 공간을 활용한 early 매핑에 대한 방법도 제공한다. 이 과정이 완료되면 정규 메모리 할당은 아직 동작하지 않지만, memblock을 이용한 early 메모리 할당이 가능해진다.

 

paging_init() – ARM64

arch/arm64/mm/mmu.c

/*
 * paging_init() sets up the page tables, initialises the zone memory
 * maps and sets up the zero page.
 */
void __init paging_init(void)
{
        pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir));

        map_kernel(pgdp);
        map_mem(pgdp);

        pgd_clear_fixmap();

        cpu_replace_ttbr1(lm_alias(swapper_pg_dir));
        init_mm.pgd = swapper_pg_dir;

        memblock_free(__pa_symbol(init_pg_dir),
                      __pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir));

        memblock_allow_resize();
}

커널 부트업 과정에서 커널 이미지를 모두 RW가 가능한 페이지 테이블인 init_pg_dir을 사용하여왔다. 이를 버리고 커널 코드 등이 read only등이 가능한 정규 커널 페이지 테이블인 swapper_pg_dir 체제로 변경한다. 즉 커널 코드 및 데이터 영역, DRAM 영역을 커널용 페이지 테이블을 가리키는 swapper_pg_dir에 새롭게 매핑하고 메모리를 관리하기 위한 메모리 맵(mem_map)을 구성한다.  그런 후 swapper_pg_dir 페이지 테이블 체제를 활성화시킨다.

  • 코드 라인 3에서 커널 페이지 테이블로 사용할 pgd 테이블로 컴파일 타임에 static하게 생성한 swapper_pg_dir 테이블을 사용하여 fixmap 영역의 pgd 엔트리에 매핑한다. fixmap에 매핑 하였으므로 swapper_pg_dir 페이지 테이블에 access가 가능해졋다.
    • 어셈블리(head.S)로 구동된 커널 부트업을 통해 init_pg_dir 페이지 테이블에 커널을 임시 매핑하여 사용하고 있는 중이다.
    • 임시로 사용 중인 init_pg_dir 페이지 테이블을 버리고 정식으로 swapper_pg_dir 페이지 테이블로 변경할 예정이다.
  • 코드 라인 5~6에서 새로 사용할 swapper_pg_dir 페이지 테이블에 커널(코드 및 데이터) 영역과 메모리 영역을 매핑한다.
  • 코드 라인 8에서 fixmap에 매핑한 swapper_pg_dir은 곧 ttbr 레지스터를 통해 정식으로 사용될 예정이므로 fixmap에서 분리하기 위해 fixmap 영역의 pgd 엔트리에 매핑한 pgd 테이블을 매핑 해제한다.
  • 코드 라인 10~11에서 커널용 페이지 테이블을 가리키는 ttbr1이 새로 준비한 swapper_pg_dir을 가리키도록 한다.
  • 코드 라인 13~14에서 임시로 사용한 init_pg_dir 페이지 테이블 영역을 memblock에 할당 해제 한다. (소멸)
  • 코드 라인 16에서 지금부터 memblock이 확장될 수 있게 설정한다.

 

다음 그림은 커널이 부트 타임 pgd를 사용하다 fixmap을 이용하여 커널과 메모리를 매핑 후 정규 pgd로 전환하는 과정이다.

 

커널 매핑

map_kernel()

arch/arm64/mm/mmu.c

/*
 * Create fine-grained mappings for the kernel.
 */
static void __init map_kernel(pgd_t *pgdp)
{
        static struct vm_struct vmlinux_text, vmlinux_rodata, vmlinux_inittext,
                                vmlinux_initdata, vmlinux_data;

        /*
         * External debuggers may need to write directly to the text
         * mapping to install SW breakpoints. Allow this (only) when
         * explicitly requested with rodata=off.
         */
        pgprot_t text_prot = rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;

        /*
         * If we have a CPU that supports BTI and a kernel built for
         * BTI then mark the kernel executable text as guarded pages
         * now so we don't have to rewrite the page tables later.
         */
        if (arm64_early_this_cpu_has_bti())
                text_prot = __pgprot_modify(text_prot, PTE_GP, PTE_GP);

        /*
         * Only rodata will be remapped with different permissions later on,
         * all other segments are allowed to use contiguous mappings.
         */
        map_kernel_segment(pgdp, _text, _etext, text_prot, &vmlinux_text, 0,
                           VM_NO_GUARD);
        map_kernel_segment(pgdp, __start_rodata, __inittext_begin, PAGE_KERNEL,
                           &vmlinux_rodata, NO_CONT_MAPPINGS, VM_NO_GUARD);
        map_kernel_segment(pgdp, __inittext_begin, __inittext_end, text_prot,
                           &vmlinux_inittext, 0, VM_NO_GUARD);
        map_kernel_segment(pgdp, __initdata_begin, __initdata_end, PAGE_KERNEL,
                           &vmlinux_initdata, 0, VM_NO_GUARD);
        map_kernel_segment(pgdp, _data, _end, PAGE_KERNEL, &vmlinux_data, 0, 0);

        if (!READ_ONCE(pgd_val(*pgd_offset_pgd(pgdp, FIXADDR_START)))) {
                /*
                 * The fixmap falls in a separate pgd to the kernel, and doesn't
                 * live in the carveout for the swapper_pg_dir. We can simply
                 * re-use the existing dir for the fixmap.
                 */
                set_pgd(pgd_offset_pgd(pgdp, FIXADDR_START),
                        READ_ONCE(*pgd_offset_k(FIXADDR_START)));
        } else if (CONFIG_PGTABLE_LEVELS > 3) {
                pgd_t *bm_pgdp;
                p4d_t *bm_p4dp;
                pud_t *bm_pudp;
                /*
                 * The fixmap shares its top level pgd entry with the kernel
                 * mapping. This can really only occur when we are running
                 * with 16k/4 levels, so we can simply reuse the pud level
                 * entry instead.
                 */
                BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
                bm_pgdp = pgd_offset_pgd(pgdp, FIXADDR_START);
                bm_p4dp = p4d_offset(bm_pgdp, FIXADDR_START);
                bm_pudp = pud_set_fixmap_offset(bm_p4dp, FIXADDR_START);
                pud_populate(&init_mm, bm_pudp, lm_alias(bm_pmd));
                pud_clear_fixmap();
        } else {
                BUG();
        }

        kasan_copy_shadow(pgdp);
}

커널 코드를 의도하지 않은 수정으로부터 보호하고 실행 영역과 비실행 영역 또한 보호하기 위해 커널 코드와 데이터의 읽기 전용 영역과 읽고 쓰기 영역을 나누어 각각의 적절한 매핑 속성으로 매핑한다. 매핑할 페이지 테이블은 요청한 pgd 테이블의 포인터 @pgdp 이며, 이곳에 매핑을 수행한다.

  • 코드 라인 11에서 커널 코드의 매핑 속성을 RW 또는 Read only(디폴트) 둘 중하나로 선택한다.
    • external 디버거를 사용 시 sw 브레이크 포인터를 사용하여 커널 코드가 매핑된 영역에 직접 기록할 수 있다. 이러한 경우 커멘드라인 커널 파라메터에 “rodata=off”를 사용하여야 커널 영역을 read only로 하지 않고 기록도 가능하게 매핑할 수 있다.
  • 코드 라인 18~19에서 cpu가 BTI(byte top ignore) 기능을 지원하는 경우 커널 코드의 매핑 속성에 PTE_GP 플래그를 추가한다.
  • 코드 라인 25~26에서 커널 이미지의 일반 코드 영역을 커널 실행 페이지 타입으로 매핑한다.
    • arm64 예) 메모리가 0x4000_0000에 위치한 커널 이미지에 대해 0x8_0000 offset만큼 떨어진 커널 코드를 커널용 가상 주소 중 kimage 영역에 매핑한다.
      • phys=0x4008_0000, virt=0xffff_0000_1008_0000
  • 코드 라인 27~28에서 커널 이미지의 읽기 전용 데이터 영역을 임시로 읽기 쓰기 가능한 커널 페이지 타입으로 매핑하되 contiguous 매핑을 하지 않도록 한다. rodata 섹션에 위치한 데이터들은 잠시 뒤 map_mem() 함수를 통해 PAGE_KERNEL 속성으로 재 매핑될 예정인데 contiguous 매핑 상태에서 속성을 바꾸는 매핑을 수행하면 TLB conflict가 발생하는 버그가 발견되었다. 따라서 이 영역에 대해서 contiguous 매핑을 하지 않도록 수정하였다.
  • 코드 라인 29~30에서 커널 이미지의 초기화 코드 영역을 커널 실행 페이지 타입으로 매핑한다.
  • 코드 라인 31~32에서 커널 이미지의 초기화 데이터 영역을 커널 페이지 타입으로 매핑한다.
  • 코드 라인 33에서 커널 이미지의 일반 데이터 영역을 커널 페이지 타입으로 매핑한다.
    • 커널 영역간에는 가드 페이지를 추가하지 않지만, 커널의 마지막 리니어 매핑 영역은 일반 vmalloc 할당 공간과 분리하기 위해 가드 페이지를 추가한다.
  • 코드 라인 35~42에서 커널 이미지와 fixmap 공간이 같은 pgd 엔트리를 사용하지 않는 일반적인 경우이다. 이러한 경우 init_pg_dir에 구성되었던 fixmap 테이블을 swapper_pg_dir로 옮긴다.
    • early_fixmap_init() 함수를 통해 init_pg_dir을 통해 이미 구성하였던 fixmap용 bm_pud, bm_pmd 및 bm_pud 간의 연결 중 init_pg_dir을 제외하고 swapper_pg_dir 쪽으로 연결을 옮긴다.
  • 코드 라인 43~58에서 커널 이미지와 fixmap 공간이 같은 pgd 엔트리를 사용하는 특별한 경우이다. 이 경우는 4 단계의 16K 페이지 테이블 구성을 사용하는 경우에만 발생한다. 이 경우 fixmap이 구성된 bm_pmd 테이블을 swapper_pg_dir의 새 pgd(pud) 엔트리에 연결한다. 그런 후 pud 테이블의 매핑을 해제한다.
  • 코드 라인 63에서 페이지 테이블을 복제하여 KASAN용 shodow 페이지 테이블을 만든다.

 

다음과 같이 커널 이미지를 덤프하여 head.text 섹션에 위치한 head.S 어셈블리 커널 코드가 시작됨을 알 수 있다. 또한 .text 섹션은 커널 코드가 담기는 영역이며 .rodata 섹션에는 읽기 전용 데이터가 담긴다.

  • KASLR 커널 옵션을 사용하지 않는 디폴트 상태의 커널 이미지(.head.text)가 위치할 가상 주소의 위치는 0xffff_0000_1008_0000 임을 확인할 수 있다. 0x1000_0000을 띄운 이유는 이사이에 모듈과 bpf_jit 영역으로 각각 128M씩 사용한다. 또한 x08_0000은 로드된 커널 이미지의 시작 offset이다.
$ aarch64-linux-gnu-objdump -x vmlinux 
vmlinux:     file format elf64-littleaarch64
vmlinux
architecture: aarch64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0xffff000010080000

Program Header:
    LOAD off    0x0000000000010000 vaddr 0xffff000010080000 paddr 0xffff000010080000 align 2**16
         filesz 0x0000000000a7c7c0 memsz 0x0000000000a7c7c0 flags r-x
    LOAD off    0x0000000000a90000 vaddr 0xffff000010b00000 paddr 0xffff000010b00000 align 2**16
         filesz 0x0000000000831a00 memsz 0x00000000008d91a8 flags rwx
    NOTE off    0x0000000000fc8598 vaddr 0xffff000011038598 paddr 0xffff000011038598 align 2**2
         filesz 0x000000000000003c memsz 0x000000000000003c flags r--
   STACK off    0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
         filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
private flags = 0:

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .head.text    00001000  ffff000010080000  ffff000010080000  00010000  2**12
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .text         00a7b7c0  ffff000010081000  ffff000010081000  00011000  2**11
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .rodata       004e78a0  ffff000010b00000  ffff000010b00000  00a90000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  3 .pci_fixup    00002170  ffff000010fe78a0  ffff000010fe78a0  00f778a0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 __ksymtab     00009920  ffff000010fe9a10  ffff000010fe9a10  00f79a10  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 __ksymtab_gpl 0000b2c0  ffff000010ff3330  ffff000010ff3330  00f83330  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 __ksymtab_strings 00033801  ffff000010ffe5f0  ffff000010ffe5f0  00f8e5f0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 __param       00003660  ffff000011031df8  ffff000011031df8  00fc1df8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 __modver      00000ba8  ffff000011035458  ffff000011035458  00fc5458  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 __ex_table    00002598  ffff000011036000  ffff000011036000  00fc6000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .notes        0000003c  ffff000011038598  ffff000011038598  00fc8598  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 11 .init.text    000649a4  ffff000011040000  ffff000011040000  00fd0000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  (...생략...)
 29 .debug_loc    00c6b58f  0000000000000000  0000000000000000  0d5146a0  2**0

 

map_kernel_segment()

arch/arm64/mm/mmu.c

static void __init map_kernel_segment(pgd_t *pgdp, void *va_start, void *va_end,
                                      pgprot_t prot, struct vm_struct *vma,
                                      int flags, unsigned long vm_flags)
{
        phys_addr_t pa_start = __pa_symbol(va_start);
        unsigned long size = va_end - va_start;

        BUG_ON(!PAGE_ALIGNED(pa_start));
        BUG_ON(!PAGE_ALIGNED(size));

        __create_pgd_mapping(pgdp, pa_start, (unsigned long)va_start, size, prot,
                             early_pgtable_alloc, flags);

        if (!(vm_flags & VM_NO_GUARD))
                size += PAGE_SIZE;

        vma->addr       = va_start;
        vma->phys_addr  = pa_start;
        vma->size       = size;
        vma->flags      = VM_MAP | vm_flags;
        vma->caller     = __builtin_return_address(0);

        vm_area_add_early(vma);
}

요청 가상 주소 범위를 가상 주소에 해당하는 물리 주소에 prot 메모리 타입으로 매핑하고, 이 영역은 vm_struct 구조체에 담아 전역 vmlist에 담아둔다.

  • 코드 라인 11~12에서 요청 가상 주소 범위 @va_start ~ @va_end에 해당하는 물리 주소에 prot 메모리 타입으로 매핑한다.
  • 코드 라인 14~23에서 구성된 vm_struct 구조체를 전역 vmlist에 담아둔다. 담아둔 리스트는 나중에 슬랩 메모리 할당자가 활성화된 이후에 vmalloc_init( ) 함수가 호출되면서 이를 활용한다.

 


메모리 매핑

map_mem()

arch/arm64/mm/mmu.c

static void __init map_mem(pgd_t *pgdp)
{
        phys_addr_t kernel_start = __pa_symbol(_text);
        phys_addr_t kernel_end = __pa_symbol(__init_begin);
        struct memblock_region *reg;
        int flags = 0;
        u64 i;

        if (rodata_full || debug_pagealloc_enabled())
                flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;

        /*
         * Take care not to create a writable alias for the
         * read-only text and rodata sections of the kernel image.
         * So temporarily mark them as NOMAP to skip mappings in
         * the following for-loop
         */
        memblock_mark_nomap(kernel_start, kernel_end - kernel_start);
#ifdef CONFIG_KEXEC_CORE
        if (crashk_res.end)
                memblock_mark_nomap(crashk_res.start,
                                    resource_size(&crashk_res));
#endif

        /* map all the memory banks */
        for_each_mem_range(i, &start, &end) {
                if (start >= end)
                        break;
                /*
                 * The linear map must allow allocation tags reading/writing
                 * if MTE is present. Otherwise, it has the same attributes as
                 * PAGE_KERNEL.
                 */
                __map_memblock(pgdp, start, end, PAGE_KERNEL, flags);
        }

        /*
         * Map the linear alias of the [_text, __init_begin) interval
         * as non-executable now, and remove the write permission in
         * mark_linear_text_alias_ro() below (which will be called after
         * alternative patching has completed). This makes the contents
         * of the region accessible to subsystems such as hibernate,
         * but protects it from inadvertent modification or execution.
         * Note that contiguous mappings cannot be remapped in this way,
         * so we should avoid them here.
         */
        __map_memblock(pgdp, kernel_start, kernel_end,
                       PAGE_KERNEL, NO_CONT_MAPPINGS);
        memblock_clear_nomap(kernel_start, kernel_end - kernel_start);

#ifdef CONFIG_KEXEC_CORE
        /*
         * Use page-level mappings here so that we can shrink the region
         * in page granularity and put back unused memory to buddy system
         * through /sys/kernel/kexec_crash_size interface.
         */
        if (crashk_res.end) {
                __map_memblock(pgdp, crashk_res.start, crashk_res.end + 1,
                               PAGE_KERNEL,
                               NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS);
                memblock_clear_nomap(crashk_res.start,
                                     resource_size(&crashk_res));
        }
#endif
}

memory memblock에 등록된 각 메모리 영역을 pgd에 매핑한다.

  • 코드 라인 9~10에서 “rodata=full” 커멘드 라인 파라메터가 주어지거나 CONFIG_DEBUG_PAGEALLOC 커널 옵션과 “debug_pagealloc=”이 설정된 경우 커널  영역은 writable 페이지가 된다. 이러한 경우 블럭(huge: pmd 타입 2M 섹션, pud 타입 1G 섹션) 매핑과 리니어 매핑을 하지 못하도록 플래그를 설정한다.
    • CONFIG_DEBUG_PAGEALLOC 및 “debug_pagealloc=”
      •  KASAN(Kernel Address Sanitizer)보다는 빠른 페이지 메모리 할당에 대한 디버그를 사용하게 한다.
      • 참고: Debugging | kernel.org
    • NO_BLOCK_MAPPINGS
      • 블럭(huge) 매핑을 하지 못하게 제한하는 플래그
        • pmd 타입은 2M 섹션
        • pud 타입은 1G 섹션
    • NO_CONT_MAPPINGS
      • 연속된 물리 페이지의 매핑 시 TLB 엔트리의 contiguous 비트를 설정하여 TLB 엔트리를 절약할 수 있는데 이를 못하게 제한하는 플래그
  • 코드 라인 18에서 임시로 커널 memblock 영역을 nomap 플래그를 설정하여 아래 루프에서 매핑하지 못하도록 한다.
    • 예) 4G의 물리 메모리가 1건이 memory 타입의 memblock에 등록되어 있는 경우 잠시 3 건의 영역으로 isolation 된다.
      • 수행 전
        • regions[0].base=0x4000_0000. size=0x1_0000_0000, flag=0
      • 수행 후
        • regions[0].base=0x4000_0000. size=0x8_0000, flag=0
        • regions[0].base=0x4008_0000. size: 0xfc_0000, flag=4(no-map)
        • regions[0].base=0x4104_0000. size: 0xfefc_0000, flag=0
  • 코드 라인 20~22에서 crash kernel 영역도 nomap 플래그를 설정한다.
  • 코드 라인 26~35에서 memory memblock에 등록된 각 영역에 대해 루프를 돌며 해당 영역을 태그(MTE)가 포함된 커널 페이지 속성(PAGE_KERNEL_TAGGED)으로 페이지 테이블에 매핑한다.
  • 코드 라인 47~48에서 커널 영역을 커널 페이지 속성으로 리니어 영역에 매핑하되 contiguous 매핑을 허용하지 않는다.
    • 리니어 매핑 영역에 커널 이미지를 매핑하여 놔두는 이유
      • hibernate와 같은 다른 서브 시스템은 여전히 리니어 매핑 주소를 통해 커널 텍스트 나 데이터 세그먼트를 참조 할 필요가 있기 때문에 리니어 매핑 영역에도 매핑한다.
      • 리니어 매핑 영역의 커널 이미지는 부주의한 수정이나 실행을 방지하기 위해 읽기 전용/비실행 가능으로 매핑한다.
      • 참고: arm64: move kernel image to base of vmalloc area
  • 코드 라인 49에서 임시로 커널 memblock 영역에 nomap 플래그를 설정한 것을 제거한다.
  • 코드 라인 57~63에서 crash kernel 영역을 커널 페이지로 매핑하되 블럭 매핑과 리니어 매핑을 하지 않는다. 그런 후 memblock에 임시로 설정한 nomap 플래그를 제거한다.

 

다음 그림은 물리 주소의 메모리들과 커널이미지가 커널 영역에 해당하는 각각의 가상 주소에 매핑되는 것을 알 수 있다.

  • 녹색으로 표시된 메모리 영역의 블럭들은 리니어 매핑 영역에 매핑된다.
  • 황색으로 표시된 커널 영역의 블럭은 리니어 매핑 영역과 kimage 영역에 이중 매핑된다.

 

_ _map_memblock()

arch/arm64/mm/mmu.c

static void __init __map_memblock(pgd_t *pgdp, phys_addr_t start,
                                  phys_addr_t end, pgprot_t prot, int flags)
{
        __create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start,
                             prot, early_pgtable_alloc, flags);
}

pgd 페이지 테이블을 가리키는 포인터 @pgdp에 물리 주소 범위 @start ~ @end를 @flags 값을 사용하여 @prot 속성으로 매핑한다.  커널 부트업 타임에 버디 시스템 등의 정규 페이지 할당자가 아직 활성화되지 않았을 때 early 메모리 할당자인 memblock을 사용하여 페이지 테이블을 할당한다.

 

early_pgtable_alloc()

arch/arm64/mm/mmu.c

static phys_addr_t __init early_pgtable_alloc(int shift)
{
        phys_addr_t phys;
        void *ptr;

        phys = memblock_phys_alloc(PAGE_SIZE, PAGE_SIZE);
        if (!phys)
                panic("Failed to allocate page table page\n");

        /*
         * The FIX_{PGD,PUD,PMD} slots may be in active use, but the FIX_PTE
         * slot will be free, so we can (ab)use the FIX_PTE slot to initialise
         * any level of table.
         */
        ptr = pte_set_fixmap(phys);

        memset(ptr, 0, PAGE_SIZE);

        /*
         * Implicit barriers also ensure the zeroed page is visible to the page
         * table walker
         */
        pte_clear_fixmap();

        return phys;
}

페이지 테이블 용도로 사용할 싱글 페이지를 할당하여 0으로 초기화한 후 페이지의 물리 주소를 리턴한다. 0으로 초기화할 때 커널이 사용할 메모리들은 아직 매핑되어 있지 않으므로 접근할 수가 없다. 따라서 임시로 가상 주소를 사용하여 0으로 초기화하기 위해 fixmap 영역의 FIX_PTE 주소에 임시로 매핑하는 방법을 사용한다

  • 코드 라인 6~8에서 커널 페이지 테이블로 사용할 하나의 페이지를 memblock으로부터 할당받는다.
  • 코드 라인 15~25에서 fixmap의 FIX_PTE 주소에 매핑한 후 해당 페이지를 0으로 클리어하고, 다시 매핑 해제한 후 물리 주소를 리턴한다.

 


페이지 테이블 매핑

pgd 테이블에 매핑

 

다음 그림은 물리 주소를 가상 주소로 요청하는 사이즈만큼 매핑하는 create_pgd_mapping() 함수의 호출 관계를 보여준다.

 

create_pgd_mapping()

arch/arm64/mm/mmu.c

void __init create_pgd_mapping(struct mm_struct *mm, phys_addr_t phys,
                               unsigned long virt, phys_addr_t size,
                               pgprot_t prot, bool page_mappings_only)
{
        int flags = 0;

        BUG_ON(mm == &init_mm);

        if (page_mappings_only)
                flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;

        __create_pgd_mapping(mm->pgd, phys, virt, size, prot,
                             pgd_pgtable_alloc, flags);
}

유저 페이지 테이블의 가상 주소 @virt에서 @size에 해당하는 엔트리에 물리 주소 @phy를 @prot 속성으로 매핑한다. 인자로 주어진 @page_mappings_only가 true인 경우 개별 페이지 매핑만 지원한다. 즉 블럭(섹션) 매핑과 리니어 연속 매핑을 지원하지 않는다.

 

__create_pgd_mapping()

arch/arm64/mm/mmu.c

static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,
                                 unsigned long virt, phys_addr_t size,
                                 pgprot_t prot,
                                 phys_addr_t (*pgtable_alloc)(int),
                                 int flags)
{
        unsigned long addr, length, end, next;
        pgd_t *pgdp = pgd_offset_raw(pgdir, virt);

        /*
         * If the virtual and physical address don't have the same offset
         * within a page, we cannot map the region as the caller expects.
         */
        if (WARN_ON((phys ^ virt) & ~PAGE_MASK))
                return;

        phys &= PAGE_MASK;
        addr = virt & PAGE_MASK;
        length = PAGE_ALIGN(size + (virt & ~PAGE_MASK));

        end = addr + length;
        do {
                next = pgd_addr_end(addr, end);
                alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,
                               flags);
                phys += next - addr;
        } while (pgdp++, addr = next, addr != end);
}

페이지 테이블 @pgdir에서 가상 주소 @virt 부터 @size 만큼에 해당하는 pgd 테이블 엔트리에 물리 주소 @phys 부터 매핑한다. 요청 범위에 대해 pgd 사이즈 단위로 순회하며 각각의 단위 매핑을 처리하기 위해 다음 레벨인 pud 테이블 매핑을 수행하러 alloc_init_pud() 함수를 호출한다. 연결될 하위 페이지 테이블을 할당받아야 할 때 인자로 전달받은 pgtable_alloc( ) 함수를 호출하여 페이지 테이블을 할당한다.

  • 코드 라인 8에서 pgd 페이지 테이블 @pgdir 에서 가상 주소 @virt에 해당하는 pgd 엔트리 포인터를 알아온다.
  • 코드 라인 17~21에서 물리 주소를 페이지 단위로 내림 정렬하고, 가상 주소를 페이지 단위로 내림 정렬하여 addr에 설정한다. 그리고 length에 매핑할 페이지 바이트 수를 계산하여 담고, end에 매핑의 끝 가상 주소를 담는다.
  • 코드 라인 22~23에서 다음 처리할 pgd 엔트리를 구해둔다
    • 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pgd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 24~25에서 가상 주소(addr)에 해당하는 pgd 엔트리가 없으면 pud 테이블을 생성하고 이를 가리키게 한다.
  • 코드 라인 26에서 루프를 순회하기 위해 다음에 매핑할 pgd 엔트리의 물리 주소를 구한다.
  • 코드 라인 27에서 다음 pgd 엔트리를 가리키도록 포인터를 증가시키고, 처리할 가상 주소(addr)에 next를 설정한다. 그런 후 매핑이 아직 다 완료되지 않았으면 루프를 돈다.

 

다음 그림은 4K 페이지, VA_BITS = 39를 사용한 구성으로 3 레벨의 페이지 테이블을 사용한다. __create_pgd_mapping() 함수를 통해 virt 주소부터 size만큼 pgd 엔트리를 구성하고 각 pgd 엔트리마다 alloc_init_pud( ) 함수를 호출하는 모습을 보여준다.

  • pgd 엔트리는 pud 엔트리와 동일하며 하나당 1G 가상 공간을 관리한다.

 

다음 그림은 4K 페이지, VA_BITS = 48을 사용한 구성으로 4레벨의 페이지 테이블을 사용한다. __create_pgd_mapping() 함수를 통해 virt 주소부터 size만큼 pgd 엔트리를 구성하고 각 pgd 엔트리마다 alloc_init_pud( ) 함수를 호출하는 모습을 보여준다.

  • pgd 엔트리 하나당 512G 가상 공간을 관리한다.

 


pud 테이블 할당 및 초기화

alloc_init_pud()

arch/arm64/mm/mmu.c

static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,
                           phys_addr_t phys, pgprot_t prot,
                           phys_addr_t (*pgtable_alloc)(int),
                           int flags)
{
        unsigned long next;
        pud_t *pudp;
        p4d_t *p4dp = p4d_offset(pgdp, addr);
        p4d_t p4d = READ_ONCE(*p4dp);

        if (p4d_none(p4d)) {
                phys_addr_t pud_phys;
                BUG_ON(!pgtable_alloc);
                pud_phys = pgtable_alloc(PUD_SHIFT);
                __p4d_populate(p4dp, pud_phys, PUD_TYPE_TABLE);
                p4d = READ_ONCE(*p4dp);
        }
        BUG_ON(p4d_bad(p4d));

        pudp = pud_set_fixmap_offset(p4dp, addr);
        do {
                pud_t old_pud = READ_ONCE(*pudp);

                next = pud_addr_end(addr, end);

                /*
                 * For 4K granule only, attempt to put down a 1GB block
                 */
                if (use_1G_block(addr, next, phys) &&
                    (flags & NO_BLOCK_MAPPINGS) == 0) {
                        pud_set_huge(pudp, phys, prot);

                        /*
                         * After the PUD entry has been populated once, we
                         * only allow updates to the permission attributes.
                         */
                        BUG_ON(!pgattr_change_is_safe(pud_val(old_pud),
                                                      READ_ONCE(pud_val(*pudp))));
                } else {
                        alloc_init_cont_pmd(pudp, addr, next, phys, prot,
                                            pgtable_alloc, flags);

                        BUG_ON(pud_val(old_pud) != 0 &&
                               pud_val(old_pud) != READ_ONCE(pud_val(*pudp)));
                }
                phys += next - addr;
        } while (pudp++, addr = next, addr != end);

        pud_clear_fixmap();
}

가상 주소  @addr ~ @end 범위에 물리 주소 @phys부터 @prot 속성으로 매핑한다. 요청 범위에 대해 pud 사이즈 단위로 순회하며 각각의 단위 매핑 공간이 1G 블럭 단위로 정렬되는 경우 블럭 매핑을 수행하고, 그렇지 않은 경우 pud 사이즈에 해당하는 공간을 처리하기 위해 다음 레벨인 pmd 테이블 매핑을 수행하러 alloc_init_cont_pmd()를 호출한다.

  • 코드 라인 8~17에서 p4d 엔트리가 매핑되어 있지 않아 NULL인 경우에는 pud 테이블을 할당받아 연결한다.
    • arm64의 경우 pgd 테이블과 p4d 테이블은 동일하다.
  • 코드 라인 20에서 fixmap에 pud 테이블을 매핑한다.
    • fixmap에서 pgd, pud, pmd, pte 테이블용으로 각각의 페이지가 준비되어 있는데, 할당받은 페이지 테이블이 memblock으로부터 막 할당받아 아직 가상 주소에 매핑되어 사용하지 않는 경우 임시로 가상 주소에 매핑시켜 사용할 수 있도록 준비된 페이지로 페이지 테이블의 첫 구성 시에 사용한다.
  • 코드 라인 21~24에서 처리할 pud 엔트리에 대한 범위를 알아온다. 그 다음 주소는 next에 반환한다.
    • 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pud 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 29~38에서 4K 페이지 테이블을 사용하면서 1G 사이즈이면서 가상 주소, 물리 주소가 모두 1G 단위로 정렬된 경우 pud 타입 섹션 매핑을 한다.
  • 코드 라인 39~45에서 가상 주소(addr)에 해당하는 pud 엔트리가 없으면 pmd 테이블을 생성하고 이를 가리키게 한다.
  • 코드 라인 46~47에서 다음 가상 주소 addr에 해당하는 pud 엔트리를 처리하기 위해 루프를 돈다.
  • 코드 라인 49에서 pud용 fixmap 페이지를 매핑 해제한다.

 

다음 그림은 4K 페이지, VA_BITS =39 커널 옵션을 사용한 경우 alloc_init_pud( ) 함수를 통해 addr~end 가상 주소 범위에 해당하는 pud 섹션 페이지가 매핑되거나 alloc_init_cont_pmd() 함수를 호출하는 과정을 보여준다(최종 pmd 및 pte 테이블 구성은 생략)

 

다음 그림은 4K 페이지, VA_BITS =48 커널 옵션을 사용한 경우 alloc_init_pud( ) 함수를 통해 addr~end 가상 주소 범위에 해당하는 pud 섹션 페이지가 매핑되거나 alloc_init_cont_pmd() 함수를 호출하는 과정을 보여준다(최종 pmd 및 pte 테이블 구성은 생략)

 


pmd 테이블 할당 및 초기화 -1-

pmd 및 pte 테이블은 pgd 및 pud 테이블과 다르게 contiguous 매핑이 가능하다. 이 함수에서는 pmd contiguous 매핑 사이즈 단위(pmd contiguous 최대 횟수 * pmd 사이즈)로 루프를 돌며 init_pmd() 함수를 호출한다.

alloc_init_cont_pmd()

arch/arm64/mm/mmu.c

static void alloc_init_cont_pmd(pud_t *pudp, unsigned long addr,
                                unsigned long end, phys_addr_t phys,
                                pgprot_t prot,
                                phys_addr_t (*pgtable_alloc)(int), int flags)
{
        unsigned long next;
        pud_t pud = READ_ONCE(*pudp);

        /*
         * Check for initial section mappings in the pgd/pud.
         */
        BUG_ON(pud_sect(pud));
        if (pud_none(pud)) {
                phys_addr_t pmd_phys;
                BUG_ON(!pgtable_alloc);
                pmd_phys = pgtable_alloc(PMD_SHIFT);
                __pud_populate(pudp, pmd_phys, PUD_TYPE_TABLE);
                pud = READ_ONCE(*pudp);
        }
        BUG_ON(pud_bad(pud));

        do {
                pgprot_t __prot = prot;

                next = pmd_cont_addr_end(addr, end);

                /* use a contiguous mapping if the range is suitably aligned */
                if ((((addr | next | phys) & ~CONT_PMD_MASK) == 0) &&
                    (flags & NO_CONT_MAPPINGS) == 0)
                        __prot = __pgprot(pgprot_val(prot) | PTE_CONT);

                init_pmd(pudp, addr, next, phys, __prot, pgtable_alloc, flags);

                phys += next - addr;
        } while (addr = next, addr != end);
}

가상 주소  @addr ~ @end 범위에 물리 주소 @phys부터 @prot 속성으로 매핑한다. 요청 범위에 대해 cont pmd 사이즈 단위로 순회하며 cont pmd 사이즈에 해당하는 매핑을 수행하기 위해 init_pmd()를 호출한다. 만일 각각의 단위 매핑 공간이 cont pmd 단위로 사이즈와 가상 주소 및 물리 주소가 정렬되는 경우 속성에 연속(contiguous) 비트를 추가한다. 이렇게 연속 매핑 비트를 사용하는 경우 pmd 엔트리에 해당하는 TLB 엔트리를 절약할 수 있다.

  • 코드 라인 13~19에서 pud 엔트리가 매핑되어 있지 않아 NULL이거나 pud 섹션 페이지 매핑(64K 페이지가 아니면서 3레벨 이상의 변환 테이블에서만 유효)된 경우 pmd 테이블을 할당받아 연결한다.
  • 코드 라인 22~25에서 처리할 cont pmd 엔트리에 대한 범위를 알아온다. 그 다음 주소는 next에 반환한다.
    • 루프를 돌며 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 cont pmd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
    • 예) 4K 페이지, VA_BITS=48 시스템에서 cont pmd 사이즈는 CONT_PMDS(16번) x PMD_SIZE(2M) = CONT_PMD_SIZE(32M)이다.
  • 코드 라인 28~30에서 cont pmd 사이즈 이면서 가상 주소, 물리 주소가 모두 cont pmd 사이즈로 정렬된 경우 연속 매핑 플래그를 설정한다.
  • 코드 라인 32에서 pmd 엔트리 아래에 연결된 pte 테이블을 생성하고 이를 가리키게 한다.
  • 코드 라인 34~35에서 다음 가상 주소 addr에 해당하는 pmd 엔트리를 처리하기 위해 루프를 돈다.

 

다음 그림은 4K 페이지, VA_BITS =39 커널 옵션을 사용한 경우 alloc_init_cont_pmd( ) 함수를 통해 addr~end 가상 주소에 해당하는 pmd 엔트리들에 pmd 섹션 페이지가 매핑되거나 alloc_init_pte() 함수를 호출하는 과정을 보여준다(최종 pte 테이블 구성은 생략).

 

다음 그림은 4K 페이지, VA_BITS =48 커널 옵션을 사용한 경우 alloc_init_cont_pmd() 함수를 통해 addr~end 가상 주소에 해당하는 pmd 엔트리들에 pmd 섹션 페이지가 매핑되거나 alloc_init_pte() 함수를 호출하는 과정을 보여준다(최종 pte 테이블 구성은 생략).

 

pmd 테이블 할당 및 초기화 -2-

init_pmd()

arch/arm64/mm/mmu.c

static void init_pmd(pud_t *pudp, unsigned long addr, unsigned long end,
                     phys_addr_t phys, pgprot_t prot,
                     phys_addr_t (*pgtable_alloc)(int), int flags)
{
        unsigned long next;
        pmd_t *pmdp;

        pmdp = pmd_set_fixmap_offset(pudp, addr);
        do {
                pmd_t old_pmd = READ_ONCE(*pmdp);

                next = pmd_addr_end(addr, end);

                /* try section mapping first */
                if (((addr | next | phys) & ~SECTION_MASK) == 0 &&
                    (flags & NO_BLOCK_MAPPINGS) == 0) {
                        pmd_set_huge(pmdp, phys, prot);

                        /*
                         * After the PMD entry has been populated once, we
                         * only allow updates to the permission attributes.
                         */
                        BUG_ON(!pgattr_change_is_safe(pmd_val(old_pmd),
                                                      READ_ONCE(pmd_val(*pmdp))));
                } else {
                        alloc_init_cont_pte(pmdp, addr, next, phys, prot,
                                            pgtable_alloc, flags);

                        BUG_ON(pmd_val(old_pmd) != 0 &&
                               pmd_val(old_pmd) != READ_ONCE(pmd_val(*pmdp)));
                }
                phys += next - addr;
        } while (pmdp++, addr = next, addr != end);

        pmd_clear_fixmap();
}

가상 주소  @addr ~ @end 범위에 물리 주소 @phys부터 @prot 속성으로 매핑한다. 요청 범위에 대해 pmd 사이즈 단위로 순회하며 각각의 단위 매핑 공간이 섹션 단위로 정렬되는 경우 섹션 매핑을 수행하고, 그렇지 않은 경우 alloc_init_cont_pte() 함수를 호출하여 pte 레벨에서 매핑을 계속한다.

요청한 가상 주소  @addr ~ @end 범위의 pmd 엔트리에 대해 초기화를 수행한다. 필요시 pmd 섹션 블럭이나 pte 테이블을 생성하여 연결한다.

  • 코드 라인 8에서 처리할 pmd 테이블을 fixmap에 매핑하고 가상주소 @addr에 해당하는 pmd 엔트리 포인터를 알아온다.
  • 코드 라인 9~10에서 루프를 돌며 pmd 엔트리 값을 읽어 old_pmd에 저장해둔다.
  • 코드 라인 12에서 다음 처리할 pmd 엔트리를 구해둔다.
    • 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pmd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 15~24에서 pmd 사이즈 이면서 가상 주소, 물리 주소가 모두 pmd 사이즈로 정렬된 경우 pmd 타입 섹션 매핑을수행한다.
    • 예) 4K 페이지, VA_BITS=48인 경우 pmd 사이즈는 2M이다.
  • 코드 라인 25~31에서 다음 레벨 pte 테이블을 할당하고 범위 내의 페이지들을 매핑한다.
  • 코드 라인 32~33에서 다음 처리할 pmd 엔트리를 위해 루프를 돈다.
  • 코드 라인 35에서 pmd 테이블을 fixmap에서 해제한다.

 


pte 테이블 할당 및 초기화 -1-

pte 테이블도 contiguous 매핑이 가능하다.

alloc_init_cont_pte()

arch/arm64/mm/mmu.c

static void alloc_init_cont_pte(pmd_t *pmdp, unsigned long addr,
                                unsigned long end, phys_addr_t phys,
                                pgprot_t prot,
                                phys_addr_t (*pgtable_alloc)(int),
                                int flags)
{
        unsigned long next;
        pmd_t pmd = READ_ONCE(*pmdp);

        BUG_ON(pmd_sect(pmd));
        if (pmd_none(pmd)) {
                phys_addr_t pte_phys;
                BUG_ON(!pgtable_alloc);
                pte_phys = pgtable_alloc(PAGE_SHIFT);
                __pmd_populate(pmdp, pte_phys, PMD_TYPE_TABLE);
                pmd = READ_ONCE(*pmdp);
        }
        BUG_ON(pmd_bad(pmd));

        do {
                pgprot_t __prot = prot;

                next = pte_cont_addr_end(addr, end);

                /* use a contiguous mapping if the range is suitably aligned */
                if ((((addr | next | phys) & ~CONT_PTE_MASK) == 0) &&
                    (flags & NO_CONT_MAPPINGS) == 0)
                        __prot = __pgprot(pgprot_val(prot) | PTE_CONT);

                init_pte(pmdp, addr, next, phys, __prot);

                phys += next - addr;
        } while (addr = next, addr != end);
}

가상 주소  @addr ~ @end 범위에 물리 주소 @phys부터 @prot 속성으로 매핑한다. 요청 범위에 대해 cont pte 사이즈 단위로 순회하며 cont pte 사이즈에 해당하는 매핑을 수행하기 위해 init_pte()를 호출한다. 만일 각각의 단위 매핑 공간이 cont pte 단위로 사이즈와 가상 주소 및 물리 주소가 정렬되는 경우 속성에 연속(contiguous) 비트를 추가한다. 이렇게 연속 매핑 비트를 사용하는 경우 pte 엔트리에 해당하는 TLB 엔트리를 절약할 수 있다.

  • 코드 라인 8~17에서 pmd 엔트리가 매핑되어 있지 않아 NULL인 경우 pte 테이블을 할당받아 연결한다.
  • 코드 라인 20~23에서 루프를 돌며 다음 처리할 pmd 엔트리를 구해둔다
    • 매핑 진행 중인 가상 주소(addr)와 가상 끝 주소(end) 범위 내에서 다음 pmd 엔트리에 해당하는 가상 주소를 구한다. 만일 더 이상 처리할 수 없으면 가상 끝 주소(end)를 리턴한다.
  • 코드 라인 26~28에서 cont pte사이즈 이면서 가상 주소, 물리 주소가 모두 cont pte 사이즈로 정렬된 경우 연속 매핑 플래그를 설정한다.
    • 4K 페이지를 사용하는 경우 4K 페이지 * 16 = 64KB 단위
    • 16K 페이지를 사용하는 경우 16K 페이지 * 128 = 2MB 단위
    • 64K 페이지를 사용하는 경우 64K 페이지 * 32 = 2MB 단위
  • 코드 라인 30에서 pte 엔트리와 해당 범위의 페이지들을 매핑한다.
  • 코드 라인 32~33에서 다음 처리할 pmd 엔트리를 위해 루프를 돈다.

 

다음 그림은 4K 페이지, VA_BITS =39 커널 옵션을 사용한 경우 alloc_init_cont_pte( ) 함수를 통해 addr~end 가상 주소에 해당하는 pte 엔트리를 구성하고 최종 페이지와 매핑하는 것을 보여준다.

 

다음 그림은 4K 페이지, VA_BITS =48 커널 옵션을 사용한 경우 alloc_init_cont_pte( ) 함수를 통해 addr~end 가상 주소에 해당하는 pte 엔트리를 구성하고 최종 페이지와 매핑하는 것을 보여준다.

 

pte 테이블 할당 및 초기화 -2-

init_pte()

arch/arm64/mm/mmu.c

static void init_pte(pmd_t *pmdp, unsigned long addr, unsigned long end,
                     phys_addr_t phys, pgprot_t prot)
{
        pte_t *ptep;

        ptep = pte_set_fixmap_offset(pmdp, addr);
        do {
                pte_t old_pte = READ_ONCE(*ptep);

                set_pte(ptep, pfn_pte(__phys_to_pfn(phys), prot));

                /*
                 * After the PTE entry has been populated once, we
                 * only allow updates to the permission attributes.
                 */
                BUG_ON(!pgattr_change_is_safe(pte_val(old_pte),
                                              READ_ONCE(pte_val(*ptep))));

                phys += PAGE_SIZE;
        } while (ptep++, addr += PAGE_SIZE, addr != end);

        pte_clear_fixmap();
}

요청한 가상 주소  @addr ~ @end 범위의 pte 엔트리를 페이지와 매핑한다.

  • 코드 라인 6에서 처리할 pte 테이블을 fixmap에 매핑하고 가상주소 @addr에 해당하는 pte 엔트리 포인터를 알아온다.
  • 코드 라인 7~9에서 루프를 돌며 pte 엔트리 값을 읽어 old_pte에 저장해둔다.
  • 코드 라인 11에서 pte 엔트리에 매핑할 페이지에 해당하는 물리 주소와 속성을 기록하여 매핑한다.
  • 코드 라인 19~20에서 다음 처리할 pte 엔트리를 위해 루프를 돈다.
  • 코드 라인 22에서 pte 테이블을 fixmap에서 해제한다.

 


 

테이블 연결(population)

pgd_populate()

include/asm-generic/pgtable-nop4d.h

#define pgd_populate(mm, pgd, p4d)              do { } while (0)

 

p4d_populate()

arch/arm64/include/asm/pgalloc.h

static inline void p4d_populate(struct mm_struct *mm, p4d_t *p4dp, pud_t *pudp)
{
        __p4d_populate(p4dp, __pa(pudp), PUD_TYPE_TABLE);
}

pgd 페이지 테이블 엔트리 포인터 @pgdp에 다음 레벨의 pud 테이블을 연결한다.

 

__p4d_populate()

arch/arm64/include/asm/pgalloc.h

static inline void __p4d_populate(pgd_t *p4dp, phys_addr_t pudp, pgdval_t prot)
{
        set_p4d(p4dp, __p4d(__phys_to_p4d_val(pudp) | prot));
}

pgd 페이지 테이블 엔트리 포인터 @pgdp에 다음 레벨의 pud 테이블을 연결할 때 @prot 속성을 사용한다.

 

pud_populate()

arch/arm64/include/asm/pgalloc.h

static inline void pud_populate(struct mm_struct *mm, pud_t *pudp, pmd_t *pmdp)
{
        __pud_populate(pudp, __pa(pmdp), PMD_TYPE_TABLE);
}

pud 페이지 테이블 엔트리 포인터 @pudp에 다음 레벨의 pmd 테이블을 연결한다.

 

__pud_populate()

arch/arm64/include/asm/pgalloc.h

static inline void __pud_populate(pud_t *pudp, phys_addr_t pmdp, pudval_t prot)
{
        set_pud(pudp, __pud(__phys_to_pud_val(pmdp) | prot));
}

pud 페이지 테이블 엔트리 포인터 @pudp에 다음 레벨의 pmd 테이블을 연결할 때 @prot 속성을 사용한다.

 

pmd_populate()

arch/arm64/include/asm/pgalloc.h

static inline void
pmd_populate(struct mm_struct *mm, pmd_t *pmdp, pgtable_t ptep)
{
        __pmd_populate(pmdp, page_to_phys(ptep), PMD_TYPE_TABLE);
}

pmd 페이지 테이블 엔트리 포인터 @pmdp에 다음 레벨의 pte 테이블을 연결한다.

 

__pmd_populate()

arch/arm64/include/asm/pgalloc.h

static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t ptep,
                                  pmdval_t prot)
{
        set_pmd(pmdp, __pmd(__phys_to_pmd_val(ptep) | prot));
}

pmd 페이지 테이블 엔트리 포인터 @pmdp에 다음 레벨의 pte 테이블을 연결할 때 @prot 속성을 사용한다.

 


테이블 엔트리 매핑/해제

테이블 엔트리 매핑

set_pgd()

include/asm-generic/pgtable-nop4d.h

/*
 * (p4ds are folded into pgds so this doesn't get actually called,
 * but the define is needed for a generic inline function.)
 */
#define set_pgd(pgdptr, pgdval) set_p4d((p4d_t *)(pgdptr), (p4d_t) { pgdval })

arm64의 경우 pgd 테이블 역할을 p4d 테이블이 수행하도록 곧바로 set_p4d() 함수를 호출한다.

 

set_p4d()

arch/arm64/include/asm/pgtable.h

static inline void set_p4d(p4d_t *p4dp, p4d_t p4d)
{
        if (in_swapper_pgdir(p4dp)) {
                set_swapper_pgd((pgd_t *)p4dp, __pgd(p4d_val(pgd)));
                return;
        }

        WRITE_ONCE(*p4dp, p4d);
        dsb(ishst);
        isb();
}

pgd 테이블 엔트리 포인터 @pgdp에 @pgd 값을 기록하여 매핑한다.

  • 커널 영역은 내부 공유(inner share) 영역에 포함된 cpu들이 공유하여 사용 중인 영역이다. 그러므로 이 cpu들을 한꺼번에 TLB 및 인스트럭션 캐시 등에 대해 동기화하는 작업이 필요하다. 따라서 TLB 캐시 작업이 완료될 때까지 기다리도록 dsb 배리어를 수행한다. 그 후 isb 베리어를 사용하여 파이프라인을 비운다.

 

set_swapper_pgd()

arch/arm64/mm/mmu.c

void set_swapper_pgd(pgd_t *pgdp, pgd_t pgd)
{
        pgd_t *fixmap_pgdp;

        spin_lock(&swapper_pgdir_lock);
        fixmap_pgdp = pgd_set_fixmap(__pa_symbol(pgdp));
        WRITE_ONCE(*fixmap_pgdp, pgd);
        /*
         * We need dsb(ishst) here to ensure the page-table-walker sees
         * our new entry before set_p?d() returns. The fixmap's
         * flush_tlb_kernel_range() via clear_fixmap() does this for us.
         */
        pgd_clear_fixmap();
        spin_unlock(&swapper_pgdir_lock);
}

 

set_pud()

arch/arm64/include/asm/pgtable.h

static inline void set_pud(pud_t *pudp, pud_t pud)
{
#ifdef __PAGETABLE_PUD_FOLDED
        if (in_swapper_pgdir(pudp)) {
                set_swapper_pgd((pgd_t *)pudp, __pgd(pud_val(pud)));
                return;
        }
#endif /* __PAGETABLE_PUD_FOLDED */

        WRITE_ONCE(*pudp, pud);

        if (pud_valid(pud)) {
                dsb(ishst);
                isb();
        }
}

pud 테이블 엔트리 포인터 @pudp에 @pud 값을 기록하여 매핑한다.

 

set_pmd()

arch/arm64/include/asm/pgtable.h

static inline void set_pmd(pmd_t *pmdp, pmd_t pmd)
{
#ifdef __PAGETABLE_PMD_FOLDED
        if (in_swapper_pgdir(pmdp)) {
                set_swapper_pgd((pgd_t *)pmdp, __pgd(pmd_val(pmd)));
                return;
        }
#endif /* __PAGETABLE_PMD_FOLDED */

        WRITE_ONCE(*pmdp, pmd);

        if (pmd_valid(pmd)) {
                dsb(ishst);
                isb();
        }
}

pmd 테이블 엔트리 포인터 @pmdp에 @pmd 값을 기록하여 매핑한다.

 

set_pte()

arch/arm64/include/asm/pgtable.h

static inline void set_pte(pte_t *ptep, pte_t pte)
{
        WRITE_ONCE(*ptep, pte);

        /*
         * Only if the new pte is valid and kernel, otherwise TLB maintenance
         * or update_mmu_cache() have the necessary barriers.
         */
        if (pte_valid_not_user(pte)) {
                dsb(ishst);
                isb();
        }
}

pte 테이블 엔트리 포인터 @ptep에 @pte 값을 기록하여 매핑한다.

 

테이블 엔트리 매핑 해제

pgd_clear()

include/asm-generic/pgtable-nop4d.h

static inline void pgd_clear(pgd_t *pgd)        { }

arm64의 경우 pgd 테이블 역할을 p4d 테이블이 수행하므로 pgd 테이블의 작업을 아무것도 하지 못하게 한다.

 

p4d_clear()

arch/arm64/include/asm/pgtable.h

static inline void p4d_clear(pgd_t *p4dp)
{
        set_p4d(p4dp, __p4d(0));
}

@pgdp 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 

pud_clear()

arch/arm64/include/asm/pgtable.h

static inline void pud_clear(pud_t *pudp)
{
        set_pud(pudp, __pud(0));
}

@pudp 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 

pmd_clear()

arch/arm64/include/asm/pgtable.h

static inline void pmd_clear(pmd_t *pmdp)
{
        set_pmd(pmdp, __pmd(0));
}

@pmdp 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 

pte_clear()

arch/arm64/include/asm/pgtable.h

#define pte_clear(mm,addr,ptep) set_pte(ptep, __pte(0))

@ptet 엔트리 포인터에 0을 기록하여 매핑을 해제한다.

 


pte 엔트리 활성화 시 메모리 타입 속성

set_pte( ) 함수에서 물리 주소를 매핑할 때 메모리 타입 속성을 추가하여 매핑한 페이지에 대해 캐시 속성을 지정할 수 있다.

ARM64에서 캐시 속성은 ARM에서와 달리 단순하게 normal 메모리 타입과 device 타입을 두었다.

  • Normal 타입
    • 메모리 타입에 사용하고, 캐시 policy를 적용할 수 있다.
  • Device 타입
    • 예측 접근을 허용하지 않고, 캐시를 사용하지 않는 특징이 있다.

 

다음과 같은 타입의 매크로 상수 값을 사용한다.

  • MT_DEVICE_nGnRnE
    • ARM에서의 strongly-ordered 같은 타입으로 버퍼 및 캐시를 사용할 수 없어 디바이스에서 가장 느린 특성을 갖고 있다.
  • MT_DEVICE_nGnRE
    • ARM에서의 device 타입처럼 버퍼 및 캐시를 사용하지 않는다.
  • MT_DEVICE_GRE
    • ARMv8에서 새로 소개된 device 타입으로, 예측(predict) 접근을 허용하지 않고, 캐시를 사용하지 않는 normal 메모리와 유사하다.
  • MT_NORMAL_NC
    • 버퍼만 사용하고 캐시는 사용하지 않는 타입이다.
  • MT_NORMAL
    • 버퍼 및 캐시를 사용하는 타입이다.
  • MT_NORMAL_WT
    • 버퍼 및 일부 기능 제한된 캐시(write-through)를 사용한다.
  • MT_NORMAL_TAGGED
    • MT_NORMAL과 동일하지만 MTE를 지원한다.
    • 유저 페이지에서만 MTE를 지원한다.

 

디바이스 타입에 사용하는 세 가지 타입을 알아본다. n이 앞에 붙으면 non을 의미한다.

  • G(Gather)
    •  성능을 높이기 위해 다중 액세스를 병합하여 하나의 버스 트랜잭션으로 처리하는 것을 허용한다.
    • 명시적으로 요청한 사이즈와 횟수만큼의 데이터 기록을 위해 버스에 전달될 때 한 번에 요청할 수 있다.
      • 예) 1 바이트 문자를 이어진 주소에서 연달아 4번 기록하라고 했고, 쓰기 버퍼에서 이러한 요청을 4 바이트 워드로 병합하여 한 번에 처리한다.
  • R(Reorder)
    • 성능을 높이기 위해 버스를 통해 같은 디바이스에 요청될 때 프로그램 순서가 재정렬되는 것을 허용한다.
  • E(Early Write Acknowledgement)
    • 성능을 높이기 위해 버스에 정상 요청한 경우 응답을 기다리지 않고 다음 처리를 위해 완료한다.
      • 예) 높은 신뢰성을 가진 버스와 디바이스에서 사용되는 방법이다.

 


유저 공간 활성화

유저 가상 주소 공간에 해당하는 매핑을 체크하고, 매핑되지 않은 경우 유저용 메모리를 할당 후 매핑한다. 단계별로 매핑에 필요한 페이지 테이블이 추가로 필요한 경우 생성하여 연결한다.

 

mm_populate()

include/linux/mm.h

static inline void mm_populate(unsigned long addr, unsigned long len)
{
        /* Ignore errors */ 
        (void) __mm_populate(addr, len, 1);
}

가상 시작 주소부터 길이만큼  유저 페이지를 활성화(물리 메모리 할당)한다.

  • mlocked 페이지들은 swap되지 않는 속성이 있으며 fault 핸들러에 의해 메모리를 매핑하는 lazy 할당을 사용하지 않고, 처음부터 매핑되어 사용된다.

 

__mm_populate()

mm/mlock.c

/*
 * __mm_populate - populate and/or mlock pages within a range of address space.
 *
 * This is used to implement mlock() and the MAP_POPULATE / MAP_LOCKED mmap
 * flags. VMAs must be already marked with the desired vm_flags, and
 * mmap_lock must not be held.
 */
int __mm_populate(unsigned long start, unsigned long len, int ignore_errors)
{
        struct mm_struct *mm = current->mm;
        unsigned long end, nstart, nend;
        struct vm_area_struct *vma = NULL;
        int locked = 0;
        long ret = 0;

        end = start + len;

        for (nstart = start; nstart < end; nstart = nend) {
                /*
                 * We want to fault in pages for [nstart; end) address range.
                 * Find first corresponding VMA.
                 */
                if (!locked) {
                        locked = 1;
                        mmap_read_lock(mm);
                        vma = find_vma(mm, nstart);
                } else if (nstart >= vma->vm_end)
                        vma = vma->vm_next;
                if (!vma || vma->vm_start >= end)
                        break;
                /*
                 * Set [nstart; nend) to intersection of desired address
                 * range with the first VMA. Also, skip undesirable VMA types.
                 */
                nend = min(end, vma->vm_end);
                if (vma->vm_flags & (VM_IO | VM_PFNMAP))
                        continue;
                if (nstart < vma->vm_start)
                        nstart = vma->vm_start;
                /*
                 * Now fault in a range of pages. populate_vma_page_range()
                 * double checks the vma flags, so that it won't mlock pages
                 * if the vma was already munlocked.
                 */
                ret = populate_vma_page_range(vma, nstart, nend, &locked);
                if (ret < 0) {
                        if (ignore_errors) {
                                ret = 0;
                                continue;       /* continue at next VMA */
                        }
                        break;
                }
                nend = nstart + ret * PAGE_SIZE;
                ret = 0;
        }
        if (locked)
                mmap_read_unlock(mm);
        return ret;     /* 0 or negative error code */
}

가상 시작 주소부터 길이만큼 유저 페이지 활성(물리 메모리 할당)하고 성공하는 경우 활성화한 페이지 수를 반환한다. ignore_errors를 설정한 경우 중간 페이지가 할당 실패하여도 계속 진행한다. mlocked 페이지는 swap 되지 않는다.

  • 코드 라인 9에서 가상 끝 주소를 산출한다.
    • 가상 시작 주소와 길이는 페이지 단위로 정렬되어 있다.
  • 코드 라인 11에서 시작 페이지부터 끝 페이지까지 루프를 돈다.
  • 코드 라인 16~19에서 락이 걸리지 않은 경우 메모리 디스크립터의 mmap_read_lock을 걸고 진행 페이지가 속한 vma 영역을 알아온다.
  • 코드 라인 20~21에서 요청 영역이 현재 vma 영역을 벗어난 경우 다음 vma를 준비한다.
  • 코드 라인 22~23에서 요청 영역을 벗어난 vma들에 대해 더 이상 검색할 필요 없으므로 검색을 멈추고 루프를 벗어난다.
  • 코드 라인 28에서 영역의 끝과 현재 vma의 끝 중 낮은 주소를 nend에 대입한다.
  • 코드 라인 29~30에서 vma의 플래그에 VM_IO 또는 VM_PFNMAP이 있는 경우 skip 한다.
  • 코드 라인 31~32에서 현재 진행 시작 주소가 vma의 시작 주소보다 작은 경우 진행 시작 주소를 vma 시작 주소로 대입한다.
  • 코드 라인 38에서 nstart ~ nend까지의 영역에 대해 swap되지 않도록 각 페이지를 mlock 처리한다.
  • 코드 라인 39~45에서 mlock 처리가 실패하는 경우 에러 코드를 가지고 루프를 탈출한다. 만일 인수 ignore_errors 가 설정된 경우 skip 한다.
  • 코드 라인 46~48에서 nstart + mlock 처리된 페이지만큼 nend를 설정하고 계속 루프를 돈다.
  • 코드 라인 49~51에서 락이 걸려 있는 경우 해제하고 ret를 반환한다.

 

참고

 

ARM64 페이지 테이블 -1- (Basic)

<kernel v5.10>

가상 주소 공간

ARM64는 32비트 주소 공간을 갖고 있던 ARM과 달리 대폭 커진 64비트의 주소 공간을 갖고 있다.

 

다음 그림과 같이 ARM64 커널에서는 64비트 가상 주소의 시작 부분과 끝부분의 가상 주소 공간을 사용한다.

 

MMU 디바이스 내부에 있는 2개의 TTBR 레지스터(Translation Table Base Register)가 가리키는 페이지 테이블을 통해 커널 주소 공간과 유저 주소 공간에 매핑된 물리 자원에 접근할 수 있다. 커널 주소 공간과 유저 주소 공간은 각자 페이지 테이블을 갖고 있다.
커널 주소 공간을 위한 페이지 테이블은 TTBR1 레지스터가 가리킨다. 유저 주소 공간을 위한 페이지 테이블은 TTBR0 레지스터가 가리킨다. 각 레지스터를 사용하면 CPU는 두 주소 공간에 접근할 수 있다.

 

 

페이지 테이블은 여러 개의 엔트리로 구성되어 있다. 페이지 테이블 엔트리 하나는 페이지 1개에 대한 물리 주소로 변환하는 데 사용한다. ARM64 커널에서는 페이지 크기로 4KB, 16KB, 64KB 중 하나를 사용할 수 있다(기본 크기는 4KB). 페이지 테이블의 유지 관리 편의를 위해 커널을 설정할 때 페이지 크기를 혼용하여 사용하지 않고 1개의 페이지 크기를 선택하여 사용한다.

 

 

 

ARM64 커널이 64비트 가상 주소 전부를 사용하는 것이 아니라 36, 39, 42, 47, 48 및 최대 52 비트와 같이 아키텍처가 지원하는 비트만을 선택해 사용할 수 있고 이 범위를 가상 주소로 사용한다.

 

예를 들어, 48비트를 선택해 ARM64 커널을 빌드하여 사용할 경우 주소 공간의 크기를 계산하기 위해 2^48을 연산하면 256TB임을 알 수 있다. 그리고 이 공간에 필요한 페이지 테이블의 엔트리 수를 계산하기 위해서는 페이지 크기로 나누어야 한다. 예를 들어, 커널이 4K 페이지를 사용하는 경우 256TB/4KB를 연산하면 640억 개(64G)의 엔트리 수가 산출됨을 알 수 있다.

다음 그림을 보면 VA_BITS = 48 커널 옵션을 사용해 48비트를 선택하여 주소 공간으로 0x00000000_00000000~0x0000ffff_ffffffff 범위를 사용할 수 있다.

  • ARM64 시스템이 지원하는 세 가지 페이지 크기(4KB, 16KB, 64KB)에 따라 페이지 테이블 엔트리 수가 결정됨을 알 수 있다.

 

각 태스크에 주어지는 가상 주소 공간마다 64G개의 엔트리를 제공하기에는 너무 많은 메모리가 낭비되므로 ARM64 커널은 테이블을 최소 2 레벨부터 최대 4 레벨로 나누어 사용한다.

다음 그림은 36비트(VA_BITS)를 사용하는 주소 공간을 나타내는 2 레벨 페이지 테이블을 구성한 것이다. 필요한 페이지 테이블 엔트리가 2048개로 줄어듦을 알 수 있다

  • 페이지 테이블 인덱스에 해당하는 비트가 페이지 오프셋에 사용하는 비트 – 3인 것에 주목한다.

 

페이지 테이블 개요

커널이 지원하는 페이지 테이블은 5 단계이며, 아키텍처마다 최대 적용 레벨이 다르다.

  • x86의 경우 최대 5 단계까지 지원한다. PGD -> P4D -> PUD -> PMD -> PTE
  • ARM64의 경우 최대 4 단계까지 지원한다. PGD -> PUD -> PMD -> PTE
    • ARM64 커널 코드에서는 PGD 함수에서 아무것도 하지 않고 곧장 P4D 함수를 호출하는 것을 종종 볼 수 있다.

 

다음은 ARM64 시스템에서 사용하는 페이지 테이블들이다.

  • PGD(Page Global Directory)
    • 모든 단계의 레벨 구성에서 사용된다.
      • 2 단계 레벨 구성에서는 2 레벨 테이블이다.
      • 3 단계 레벨 구성에서는 1 레벨 테이블이다.
      • 4 단계 레벨 구성에서는 0 레벨 테이블이다.
  • PUD(Page Upper Directory)
    • 총 단계 3 레벨 구성에서는 제외되며, 구성되어 사용될 때에는 1 레벨 테이블이다.
  • PMD(Page Mid-level Directory)
    • 총 단계 2 레벨 구성에서는 제외되며, 구성되어 사용될 때에는 2 레벨 테이블이다.
  • PTE(Page Table Entry)
    • 모든 단계의 레벨 구성에서 3 레벨 테이블로 사용된다.

 

ARM64 커널에서 사용할 수 있는 페이지 크기(4K, 16K, 64K)와 가상 주소 비트 수(36, 39, 42, 47, 48, 52)에 따른 조합을 알아본다. defconfig 기준으로 ARM64 커널은 4KB의 페이지와 가상 주소 비트 수(이하 VA_BITS)가 48로 구성된 4레벨 테이블을 사용한다.

  • 주의: 커널 v4.7-rc1 이전에는 디폴트 값으로 VA_BITS=39, 3 레벨로 구성된 테이블을 사용하였다.

 

페이지 크기와 VA_BITS 조합에 의한 각 페이지 테이블 구성

2단계 페이지 테이블

다음 그림은 2 레벨로 구성된 페이지 테이블(pgd와 pte)을 사용해서 페이지 프레임에 연결되는 두 가지 방법을 보여준다.

  • ARM 아키텍처에서 pgd는 2 레벨 테이블, pte는 3 레벨 테이블이다.
  • pmd 사이즈 블럭을 직접 연결하여 사용할 수 있다.

VA_BITS = 36인 경우를 알아보자. 이 경우에는 pgd와 pte 테이블이 각각 11비트를 사용하므로 엔트리는 2048개가 된다. 최종 엔트리가 16K 페이지를 가리키면 64G(2048 * 2048 * 16K) 주소에 해당하는 페이지 프레임을 변환할 수 있다.

 

3단계 페이지 테이블

다음 그림은 3 레벨로 구성된 페이지 테이블(pgd, pmd, pte)을 사용해서 페이지 프레임에 연결되는 세 가지 방법을 보여준다.

  • 커널 v5.0-rc1 부터 가장 아래 3 레벨 페이지 테이블 + 64K 페이지 프레임을 사용하는 경우 USER_VA_BITS=52도 사용할 수 있다.
  • ARM 아키텍처에서 pgd는 1 레벨 테이블, pmd는 2레벨 테이블, pte는 3 레벨 테이블이다.
  • pud 사이즈 및 pmd 사이즈 블럭을 직접 연결하여 사용할 수 있다. (16K 페이지의 경우는 pmd 사이즈 블럭만 가능하다)
  • 커널 메인라인에서 4T 블럭에 대한 블럭(pud 섹션 사이즈) 매핑은 아직 구현하지 않은 상태이다.

 

4단계 페이지 테이블

다음 그림은 4 레벨로 구성된 페이지 테이블(pgd, pud, pmd, pte)을 사용해서 페이지 프레임에 연결되는 두 가지 방법을 보여준다.

  • ARM 아키텍처에서 pgd는 0 레벨 테이블, pud는 1 레벨 테이블, pmd는 2레벨 테이블, pte는 3 레벨 테이블이다.
  • pud 사이즈 및 pmd 사이즈 블럭을 직접 연결하여 사용할 수 있다. (16K 페이지의 경우는 pmd 사이즈 블럭만 가능하다)

 

Contiguous Bits

pmd 및 pte 매핑 속성에만 사용할 수 있는 contiguous bit 속성이 있다. 연속된 PMD 또는 PTE 사이즈(CONT_PMD_SIZE, CONT_PTE_SIZE) 만큼의 공간을 매핑하는 경우 TLB 엔트리를 절약하여 성능을 높일 수 있는 매핑 방법이다.

  • 커널 v4.12-rc1에 채택되어 사용된다.
  • 연속되는 공간 사이즈
    •  CONT_PMD_SIZE
      • 4K 페이지: 16 * 2MB = 32MB
      • 16K 페이지: 32 * 32MB = 1GB
      • 64K 페이지: 32 * 512MB = 16G
    •  CONT_PTE_SIZE
      • 4K 페이지: 16 * 4K = 64KB
      • 16K 페이지: 128 * 16K = 2MB
      • 64K 페이지: 32 * 64K = 2MB

 

페이지 테이블 구성 예)

다음 그림은 대표적으로 구성하여 사용하는 세 가지 가상 주소 공간의 크기를 보여준다.

 

4K 페이지 + VA_BITS=39 예)

4K 페이지와 VA_BITS = 39로 구성된 3 레벨 페이지 테이블을 사용하는 경우에 대해 알아보자. 64비트 가상 주소에서 상위 25비트는 커널 또는 유저 주소 공간의 구분에 사용하고, 그다음 9비트 세트 3개는 각 레벨의 페이지 테이블 엔트리에 접근할 때 사용한다. 마지막 12비트는 페이지 내의 오프셋으로 사용한다.

 

각 테이블에 대해 9비트 인덱스를 사용하여 총 512개씩의 엔트리에 접근할 수 있음을 알 수 있다. 커널은 2개의 공간, 즉 커널과 유저 주소 공간을 나누어 사용하므로 64비트 가상 주소 중 변환에 사용하지 않는 최상위 비트 [63:39]를 체크하여 비트 값이 0인 경우에는 유저 페이지 테이블을 가리키는 TTBR0 레지스터를 사용한다. 그리고 그 비트들 값이 모두 1인 경우에는 커널 페이지 테이블을 가리키는 TTBR1 레지스터를 사용한다.

 

그러나 ARM64 커널에서는 그 비트들을 모두 비교할 필요 없이 최상위 비트 하나만을 비교하여 커널 및 유저의 가상 주소 영역을 구분한다.

 

다음 그림은 4K 페이지와 VA_BITS=39를 사용하는 페이지 테이블의 연결 구성 예를 보여준다.

 

4K 페이지 + VA_BITS=48 구성 예)

커널 v4.7-rc1부터 가상 주소 크기가 VA_BITS=39에서 VA_BITS=48로 변경되었다.

 

다음 그림은 4K 페이지와 VA_BITS=48을 사용하는 페이지 테이블의 연결 구성 예를 보여준다.

 


 

페이지 테이블 관련 주요 레지스터

TCR_EL1

다음 그림은 페이지 테이블 변환 관련된 TCR_EL1을 보여준다.

  • TxSZ (TTBRx Size offset)
    • TTBRx가 관리할 영역 크기는 2^(64-T0SZ)이다.
    • 예) VA_BITS=48의 경우 TxSZ=16이 필요하다.
  • IRGNx (Inner cacheability Normal Memory attribute for TTBRx)
    • Normal Memory Inner 캐시 속성
      • 0b00: NC (Non Cache)
      • 0b01: WB-WA(Write Back, Write Allocation)
      • 0b10: WT (Write Through)
      • 0b11: WB (Write Back)
  • ORGNx (Outer cacheability Normal Memory attribute for TTBRx)
    • Normal Memory Outer 캐시 속성 (상동)
  • SHx (Shareability attributer for TTBRx)
    • 0b00: Non-shareable
    • 0b10: Outer Shareable
    • 0b11: Inner Shareable
  • TGx (Granule size for TTBRx)
    • 0b00: 4KB
    • 0b01: 64KB
    • 0b10: 16KB
  • A1 (select TTBR0 & TTBR1 for ASID)
    • 0: TTBR0를 위해 ASID 사용
    • 1: TTBR1을 위해 ASID 사용
  • IPS (Intermediate Physical Address Size)
    • 중간 물리 주소(IPA) 사이즈
      • 0b000: 32 bits (4GB)
      • 0b001: 36 bits (64GB)
      • 0b010: 40 bits (1TB)
      • 0b011: 42 bits (4TB)
      • 0b100: 44 bits (16TB)
      • 0b101: 48 bits (256TB)
      • 0b110: 52 bits (4PB)
  • AS (ASID Size)
    • 0: TTBR0 및 TTBR1 양쪽에서 8 bit ASID 사용
    • 1: TTBR0 및 TTBR1 양쪽에서 16 bit ASID 사용
  • TBIx (Top Byte ignored for TTBRx)
    • 이 값을 1로 하는 경우 가상 주소의 최상위 8 비트를 주소 변환에 사용하지 않게 한다.

 

TTBRx_EL1

다음 그림은 두 개의 페이지 테이블의 base 주소를 가리키는 TTBR0_EL1 과 TTBR1_EL1을 보여준다.

  • ASID
    • mm->context.id의 하위 8 bit 또는 16 bit ASID를 저장한다.
    • TLB 캐시의 변환 엔트리 hit에 ASID + 가상 주소(VA) 를 사용하게하여 context 스위치 시 TLB 플러시를 최소화한다.
  • BADDR
    • 페이지 테이블의 물리 주소가 담긴다.
    • ARMv8.2 아키텍처 이상의 경우 52 비트 가상 주소 및 물리 주소를 지원한다. 이 때 52비트 물리 주소를 지원해야 하는 경우 52 비트 물리 주소의 msb를 사용한다.
  • CnP
    • Inner shable 영역의 코어에 공유할지 여부를 지정한다.

 


 

페이지 테이블 엔트리 포맷

ARMv8 엔트리 포맷

다음은 ARMv8 아키텍처 페이지 테이블의 4가지 유형의  엔트리 디스크립터 포맷이다.

  • Invalid
    • 모든 레벨에서 사용되며 bit[1:0] 값이 00 이다.
  • Block 디스크립터
    • 레벨 0~2에서 사용되며 bit[1:0] 값이 01 이다.
  • Table 디스크립터
    • 레벨 0~2에서 사용되며 bit[1:0] 값이 11 이다.
  • Page 디스크립터
    • 마지막 레벨 3에서 사용되며 bit[1:0] 값이 11 이다.

 

다음 그림은 각 레벨에서 사용하는 디스크립터들의 포맷을 보여준다.

  • ARMv8.2-LPA 옵션을 사용하는 시스템의 경우 52비트까지 확장된다.

Level 0, 1, 2의 블럭 및 테이블 관련 비트를 알아본다.

  • nT
    • Level 1 및 Level 2에서 이 비트가 설정된 엔트리가 MMU에 의해 사용될 떄 변환 fault를 유발한다. 또한 TLB 캐시에 캐시되지 않는다.
    • ARMv8.4-TTRem 기능이 구현된 시스템에서 사용한다.
  •  NSTable
    • 다음 레벨의 테이블이 Non-Secure State 인지 여부를 나타낸다.
  • APTable
    • 2 비트의 접근 권한을 지정한다.
      • 0b00: 다음 레벨의 테이블에 어떠한 영향도 없다.
      • 0b01: EL0에서 다음 레벨의 테이블에 접근을 금지한다.
      • 0b10: 다음 레벨의 테이블에서 수정을 금지한다.
      • 0b11: 위의 두 기능(0b01 & 0b10)이 동시에 동작한다.
  • UXN or XN
    • 이 비트를 1로 하면 유저 레벨이 다음 레벨의 테이블에서 실행 코드에 대한 주소 변환을 하지 못하게 한다.
      • 하이라키로 실행 금지를 제어한다.
  • PXNTable
    • 이 비트를 1로 하면 커널 레벨이 다음 레벨의 테이블에서 실행 코드에 대한 주소 변환을 하지 못하게 한다.
      • 하이라키로 실행 금지를 제어한다.

 

모든 레벨의 블럭 및 페이지 디스크립터에서 사용되는 Upper 속성 비트를 알아본다.

  • PBHA
    • Page 기반의 하드웨어 속성 비트로 지정한 디스크립터 비트의 사용 유무를 제어한다.
    • ARMv8.2에서만 사용된다.
  • SW
    • 하드웨어 아키텍처는 사용하지 않는 4 개의 비트이지만 소프트웨어에서 활용할 수 있는 비트들이다.
  • UXN 또는 XN
    • 이 비트를 1로 하면 유저 레벨이 다음 레벨의 테이블 또는 페이지에서 실행 코드에 대한 주소 변환을 하지 못하게 한다.
      • 하이라키로 실행 금지를 제어한다.
  • PXN
    • 이 비트를 1로 하면 커널 레벨이 다음 레벨의 테이블 또는 페이지에서 실행 코드에 대한 주소 변환을 하지 못하게 한다.
      • 하이라키로 실행 금지를 제어한다.
  • Contiguous
    • 이 비트를 1로 하면 하나의 엔트리로 이어지는 블럭또는 페이지가 동일한 매핑을 사용하게 된다.
  • DBM
    • ARMv8.1 아키텍처가 지원하는 기능으로 이 비트를 1로 하면  하드웨어가 Access 플래그와 Dirty 비트를 직접 관리하도록 한다.
    • 기존 ARM 아키텍처는 이 기능을 지원하지 않아 fault exception 후에 소프트웨어가 Access 플래그와 Dirty 비트를 기록하였다.

 

모든 레벨의 블럭 및 페이지 디스크립터에서 사용되는 Lower 속성 비트를 알아본다.

  • AttrIndx
    • MAIR_ELx를 위해 사용되는 메모리 속성이다.
  • NS
    • Non-Secure 비트이다.
  • AP
    • 데이터 접근 권한이다.
      • 0b00: 상위 Exception 레벨에서 RW, 유저 레벨에서 None 이다.
      • 0b01: 상위 Exception 레벨에서 RW, 유저 레벨에서 RW 이다.
      • 0b00: 상위 Exception 레벨에서 R, 유저 레벨에서 None 이다.
      • 0b00: 상위 Exception 레벨에서 R, 유저 레벨에서 R 이다.
  • SH
    • 공유 비트
      • 0b00: Non-Shareable
      • 0b01: REserved
      • 0b10: Outer Shareable
      • 0b11: Inner Shareable
  • AF
    • 페이지나 블럭에 접근한 경우 설정되는 Access 플래그이다.
    • ARMv8.1의 DBM 설정에 따라 하드웨어가 갱신할지 아니면 소프트웨어가 갱신할 지 선택할 수 있다.
  • nG
    • not Global 비트로 이 비트가 설정되는 경우 지정된 ASID의 TLB 엔트리에서만 변환을 시도한다.
    • 0으로 설정하는 경우 ASID와 상관 없이 이 엔트리를 사용하여 변환을 한다.

 

PTE 속성

커널 레벨(stage 1)에서 사용하는 리눅스 PTE  속성이다. 하이퍼 바이저 레벨(stage 2)는 생략한다.

arch/arm64/include/asm/pgtable-hwdef.h

#define PTE_VALID               (_AT(pteval_t, 1) << 0)
#define PTE_TYPE_MASK           (_AT(pteval_t, 3) << 0)
#define PTE_TYPE_PAGE           (_AT(pteval_t, 3) << 0)
#define PTE_TABLE_BIT           (_AT(pteval_t, 1) << 1)
#define PTE_USER                (_AT(pteval_t, 1) << 6)         /* AP[1] */
#define PTE_RDONLY              (_AT(pteval_t, 1) << 7)         /* AP[2] */
#define PTE_SHARED              (_AT(pteval_t, 3) << 8)         /* SH[1:0], inner shareable */
#define PTE_AF                  (_AT(pteval_t, 1) << 10)        /* Access Flag */
#define PTE_NG                  (_AT(pteval_t, 1) << 11)        /* nG */
#define PTE_GP                  (_AT(pteval_t, 1) << 50)        /* BTI guarded */
#define PTE_DBM                 (_AT(pteval_t, 1) << 51)        /* Dirty Bit Management */
#define PTE_CONT                (_AT(pteval_t, 1) << 52)        /* Contiguous range */
#define PTE_PXN                 (_AT(pteval_t, 1) << 53)        /* Privileged XN */
#define PTE_UXN                 (_AT(pteval_t, 1) << 54)        /* User XN */

 

참고

arm64_memblock_init()

<kernel v5.10>

Memblock 초기화

memblock은 커널 빌드 타임에 준비한 static 배열을 사용하여 운용하므로 memblock 자체적으로 별도의 초기화는 필요 없고,  시스템에서 사용하는 기본 영역들을 reserve 하는 준비하는 과정이 있다.

 

reserved 영역의 엔트리 등록은 초기 커널 부트업 과정에서 다음과 같은 영역을 reserved memblock에 등록하며 각 아키텍처 및 머신에 따라서 설정이 달라진다.

  • 커널 영역
  • initrd 영역
  • DTB 영역 및 DTB reserved-mem 노드가 요청하는 영역
  • CMA-DMA 영역
  • crash kernel 영역
  • elf core 헤더 영역

 

그 외에 시스템 메모리 영역을 초과하는 영역이나, DTB의 chosen 노드의 “linux,usable-memory-range” 속성으로 사용할 수 있는 메모리 영역을 제한한 경우 해당 영역을 memblock에서 제거한다.

 

다음 그림은 arm64_memblock_init() 함수에서 reserve하는 memblock들을 보여준다.

 

arm64_memblock_init()

arch/arm64/mm/init.c – 1/3-

void __init arm64_memblock_init(void)
{
        const s64 linear_region_size = BIT(vabits_actual - 1);

        /* Handle linux,usable-memory-range property */
        fdt_enforce_memory_region();

        /* Remove memory above our supported physical address size */
        memblock_remove(1ULL << PHYS_MASK_SHIFT, ULLONG_MAX);

        /*
         * Select a suitable value for the base of physical memory.
         */
        memstart_addr = round_down(memblock_start_of_DRAM(),
                                   ARM64_MEMSTART_ALIGN);

        /*
         * Remove the memory that we will not be able to cover with the
         * linear mapping. Take care not to clip the kernel which may be
         * high in memory.
         */
        memblock_remove(max_t(u64, memstart_addr + linear_region_size,
                        __pa_symbol(_end)), ULLONG_MAX);
        if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {
                /* ensure that memstart_addr remains sufficiently aligned */
                memstart_addr = round_up(memblock_end_of_DRAM() - linear_region_size,
                                         ARM64_MEMSTART_ALIGN);
                memblock_remove(0, memstart_addr);
        }

        /*
         * If we are running with a 52-bit kernel VA config on a system that
         * does not support it, we have to place the available physical
         * memory in the 48-bit addressable part of the linear region, i.e.,
         * we have to move it upward. Since memstart_addr represents the
         * physical address of PAGE_OFFSET, we have to *subtract* from it.
         */
        if (IS_ENABLED(CONFIG_ARM64_VA_BITS_52) && (vabits_actual != 52))
                memstart_addr -= _PAGE_OFFSET(48) - _PAGE_OFFSET(52);

        /*
         * Apply the memory limit if it was set. Since the kernel may be loaded
         * high up in memory, add back the kernel region that must be accessible
         * via the linear mapping.
         */
        if (memory_limit != PHYS_ADDR_MAX) {
                memblock_mem_limit_remove_map(memory_limit);
                memblock_add(__pa_symbol(_text), (u64)(_end - _text));
        }
  • 코드 라인 3에서 linear_region_size는 64비트 커널에서 사용할 가상 주소 크기의 절반을 담는다.
    • 예) 가상 주소 크기를 256T(vabits_actual=48)로 한 경우 이의 절반인 128T 크기가 연속된 영역으로 정의된다.
  • 코드 라인 6에서 디바이스 트리(FDT)가 지정한 사용 메모리 영역이 제한된 경우 그 영역 이외의 memblock 영역을 제거한다.
    • chosen 노드에 “linux,usable-memory-range” 속성으로 사용할 수 있는 메모리 영역을 제한할 수 있다.
  • 코드 라인 9에서 시스템 물리 메모리 영역을 초과하는 영역은 모두 제거한다.
  • 코드 라인 14~15에서 물리메모리의 시작 주소를 알아온다. ARM64 시스템에서 이 주소는 1G 섹션 단위로 정렬된다.
  • 코드 라인 22~29에서 lm(linear mapping) 가상 주소 영역을 초과하는 물리 주소 영역은 제거한다.
    • 커널 리니어 매핑 사이즈를 초과하는 물리 메모리의 끝을 memory memblock 영역에서 제거한다. 커널이 메모리의 끝 부분에 로드된 경우가 있으므로 이러한 경우 끝 부분을 기준으로 로드된 커널이 제거되지 않도록 제한한다.
      • 예) VA_BITS = 39, DRAM 크기 = 1TB인 경우에는 리니어 매핑 영역이 256GB로 제한되므로 768GB 메모리를 리니어 매핑으로 사용할 수 없게 된다.
    • 로드된 커널이 커널 리니어 매핑 사이즈보다 큰 메모리의 상위쪽에 로드된 경우 메모리의 상위에 위치한 커널을 보호하기 위해 커널 리니어 매핑 사이즈를 초과한 메모리의 아랫 부분을 제거한다.
  • 코드 라인 38~39에서 52bit vabits 커널이 실제 48bit vabits로 운영하는 시스템에서 동작하는 경우 lm 가상 주소와 물리 주소의 변환에 사용하는 memstart_addr 값을 조정해야 한다.
  • 코드 라인 46~49에서 DRAM 메모리 제한을 설정한 경우 제한 메모리 범위를 초과한 DRAM 메모리 영역을 memory memblock 영역에서 제거한다.

 

arch/arm64/mm/init.c – 2/3-

.       if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
                /*
                 * Add back the memory we just removed if it results in the
                 * initrd to become inaccessible via the linear mapping.
                 * Otherwise, this is a no-op
                 */
                u64 base = phys_initrd_start & PAGE_MASK;
                u64 size = PAGE_ALIGN(phys_initrd_start + phys_initrd_size) - base;

                /*
                 * We can only add back the initrd memory if we don't end up
                 * with more memory than we can address via the linear mapping.
                 * It is up to the bootloader to position the kernel and the
                 * initrd reasonably close to each other (i.e., within 32 GB of
                 * each other) so that all granule/#levels combinations can
                 * always access both.
                 */
                if (WARN(base < memblock_start_of_DRAM() ||
                         base + size > memblock_start_of_DRAM() +
                                       linear_region_size,
                        "initrd not fully accessible via the linear mapping -- please check your bootloader ...\n")) {
                        phys_initrd_size = 0;
                } else {
                        memblock_remove(base, size); /* clear MEMBLOCK_ flags */
                        memblock_add(base, size);
                        memblock_reserve(base, size);
                }
        }

        if (IS_ENABLED(CONFIG_RANDOMIZE_BASE)) {
                extern u16 memstart_offset_seed;
                u64 range = linear_region_size -
                            (memblock_end_of_DRAM() - memblock_start_of_DRAM());

                /*
                 * If the size of the linear region exceeds, by a sufficient
                 * margin, the size of the region that the available physical
                 * memory spans, randomize the linear region as well.
                 */
                if (memstart_offset_seed > 0 && range >= ARM64_MEMSTART_ALIGN) {
                        range /= ARM64_MEMSTART_ALIGN;
                        memstart_addr -= ARM64_MEMSTART_ALIGN *
                                         ((range * memstart_offset_seed) >> 16);
                }
        }
  • 코드 라인 1~28에서 램디스크(initrd) 영역을 reserved memblock에 추가한다.
  • 코드 라인 30~45에서 보안 목적으로 CONFIG_RANDOMIZE_BASE 커널 옵션을 사용하여 커널 시작 주소가 랜덤하게 바뀌는 경우 memstart_addr을 구한다.

 

arch/arm64/mm/init.c – 3/3-

        /*
         * Register the kernel text, kernel data, initrd, and initial
         * pagetables with memblock.
         */
        memblock_reserve(__pa_symbol(_text), _end - _text);
        if (IS_ENABLED(CONFIG_BLK_DEV_INITRD) && phys_initrd_size) {
                /* the generic initrd code expects virtual addresses */
                initrd_start = __phys_to_virt(phys_initrd_start);
                initrd_end = initrd_start + phys_initrd_size;
        }

        early_init_fdt_scan_reserved_mem();

        if (IS_ENABLED(CONFIG_ZONE_DMA)) {
                zone_dma_bits = ARM64_ZONE_DMA_BITS;
                arm64_dma_phys_limit = max_zone_phys(ARM64_ZONE_DMA_BITS);
        }

        if (IS_ENABLED(CONFIG_ZONE_DMA32))
                arm64_dma32_phys_limit = max_zone_phys(32);
        else
                arm64_dma32_phys_limit = PHYS_MASK + 1;

        reserve_crashkernel();

        reserve_elfcorehdr();

        high_memory = __va(memblock_end_of_DRAM() - 1) + 1;

        dma_contiguous_reserve(arm64_dma32_phys_limit);
}
  • 코드 라인 5에서 커널 영역을 reserve한다.
  • 코드 라인 6~10에서 램디스크(initrd) 영역 주소를 가상 주소로 변환하여 저장한다.
  • 코드 라인 12에서 DTB에 관련된 다음의 세 가지 영역을 추가한다.
    • DTB 자신의 영역
    • DTB 헤더의 off_mem_rsvmap 필드가 가리키는 memory reserve 블록(바이너리)에서 읽은 메모리 영역들
    • DTB reserved-mem 노드 영역이 요청하는 영역들
  • 코드 라인 14~22에서 디바이스 드라이버(dma for coherent/cma for dma)가 필요로 하는 DMA 및 DMA32 영역을 구한다.
  • 코드 라인 24에서  crash 커널 영역을 reserve 한다.
  • 코드 라인 26에서 elf core 헤더 영역을 reserve 한다.
  • 코드 라인 28에서 ARM64의 경우 highmem을 사용하지 않는다. 따라서 메모리의 끝 주소를 대입한다.
  • 코드 라인 30에서 dma 영역을 reserved memblock에 추가하고 CMA(Contiguous Memory Allocator)에도 추가한다. 전역 cma_areas[ ] 배열에 추가한 엔트리는 CMA 드라이버가 로드되면서 초기화될 때 사용한다. 또한 전역 dma_mmu_remap[ ] 배열에 추가된 엔트리는 추후 dma_contiguous_remap( ) 함수를 통해 지정된 영역에 대응하는 페이지 테이블 엔트리들을 IO 속성으로 매핑할 때 사용한다.

 

참고

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