Memory · Linux v6.6 · source + diagram note

paging_init

arm64 · Linux v6.6: paging_init()의 이미지·RAM 매핑과 TTBR1 인계를 원본 코드의 실제 순서로 읽는다.

전체 흐름

arm64 Linux v6.6의 setup_arch()는 arm64_memblock_init() 다음에 paging_init()을 호출한다. 여기서 다루는 것은 실행 중 프로세스의 page fault가 아니라 부팅 초기에 swapper_pg_dir에 커널 이미지 매핑과 RAM 선형 매핑을 만드는 작업이다.

두 주소 경로

map_kernel()은 커널 이미지의 가상 주소 구간을 섹션별로 매핑한다. map_mem()은 memblock RAM에 선형 별칭을 만든다.

root 인계

두 매핑 뒤 fixmap을 지우고 TTBR1을 새 root로 교체한다. init_mm.pgd를 설정하고 초기 표 메모리를 반환한 후 별도의 idmap을 만든다.

그림 1. 부팅 호출에서 TTBR1 교체까지
setup_arch()memblock 준비 뒤 paging_init 호출
fixmap PGDswapper_pg_dir에 임시 접근
map_kernel → map_mem이미지 주소, 이어서 RAM 선형 주소
TTBR1 교체init_mm.pgd = swapper_pg_dir

setup.c:345–347과 mmu.c:790–811의 실제 순서. create_idmap()은 이 뒤에 호출된다.

경로가상 주소입력이 단계의 결과
map_kernel()커널 이미지 섹션 주소_stext 등 linker 경계섹션별 이미지 매핑과 fixmap/KASAN shadow 복사
map_mem()__phys_to_virt() 선형 주소memblock RAM 범위일반 RAM과 이미지 구간의 별도 linear alias

소스 코드 위치

버전 차이를 섞지 않도록 Linux v6.6 arm64 원본을 고정해 읽는다. 호출 지점과 아래 세 함수의 범위를 차례로 비교한다.

원본 파일·범위확인할 내용
arch/arm64/kernel/setup.c:345–347memblock 준비 다음 paging_init() 호출
arch/arm64/mm/mmu.c:722–758map_kernel()의 이미지 섹션 매핑
arch/arm64/mm/mmu.c:566–625map_mem()의 RAM bank·이미지 별칭 매핑
arch/arm64/mm/mmu.c:790–811paging_init()의 호출·전환·마무리

대표 코드

세 상자는 각각 지정한 함수의 연속된 전체 본문이다. 서로 다른 함수의 앞뒤 줄을 이어 붙이지 않았다.

paging_init()

Linux v6.6 · arch/arm64/mm/mmu.c:790–811 · 연속된 전체 함수
void __init paging_init(void)
{
	pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir));
	extern pgd_t init_idmap_pg_dir[];

	idmap_t0sz = 63UL - __fls(__pa_symbol(_end) | GENMASK(VA_BITS_MIN - 1, 0));

	map_kernel(pgdp);
	map_mem(pgdp);

	pgd_clear_fixmap();

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

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

	memblock_allow_resize();

	create_idmap();
}

읽는 법: map_kernel()이 map_mem()보다 앞선다. pgd_clear_fixmap() 다음 TTBR1과 init_mm.pgd가 바뀌며 create_idmap()은 마지막이다.

map_kernel()

Linux v6.6 · arch/arm64/mm/mmu.c:722–758 · 연속된 전체 함수
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 = kernel_exec_prot();

	/*
	 * 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, _stext, _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);

	fixmap_copy(pgdp);
	kasan_copy_shadow(pgdp);
}

읽는 법: text_prot는 설정과 CPU 기능에 따라 결정된다. rodata의 최종 읽기 전용 권한을 이 함수가 모두 확정한다고 해석하지 않는다.

map_mem()

Linux v6.6 · arch/arm64/mm/mmu.c:566–625 · 연속된 전체 함수
static void __init map_mem(pgd_t *pgdp)
{
	static const u64 direct_map_end = _PAGE_END(VA_BITS_MIN);
	phys_addr_t kernel_start = __pa_symbol(_stext);
	phys_addr_t kernel_end = __pa_symbol(__init_begin);
	phys_addr_t start, end;
	phys_addr_t early_kfence_pool;
	int flags = NO_EXEC_MAPPINGS;
	u64 i;

	/*
	 * Setting hierarchical PXNTable attributes on table entries covering
	 * the linear region is only possible if it is guaranteed that no table
	 * entries at any level are being shared between the linear region and
	 * the vmalloc region. Check whether this is true for the PGD level, in
	 * which case it is guaranteed to be true for all other levels as well.
	 */
	BUILD_BUG_ON(pgd_index(direct_map_end - 1) == pgd_index(direct_map_end));

	early_kfence_pool = arm64_kfence_alloc_pool();

	if (can_set_direct_map())
		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);

	/* 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, pgprot_tagged(PAGE_KERNEL),
			       flags);
	}

	/*
	 * Map the linear alias of the [_stext, __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);
	arm64_kfence_map_pool(early_kfence_pool, pgdp);
}

읽는 법: 커널 이미지의 [_stext, __init_begin)를 일반 bank loop에서 잠시 NOMAP으로 제외하고, 실행 불가인 별도 선형 별칭으로 다시 매핑한다. 쓰기 권한 제거는 후속 함수가 맡는다.

함수별 분석

pgdp는 pgd_set_fixmap(__pa_symbol(swapper_pg_dir))로 얻은 임시 접근점이다. 두 함수는 같은 root에 서로 다른 가상 주소 영역을 채운다.

그림 2. 같은 root에 놓이는 서로 다른 매핑
swapper_pg_dirfixmap으로 쓰는 새 root
map_kernel()image text · rodata · init · data
map_mem()memblock RAM과 이미지 linear alias
새 rootfixmap 해제 뒤 TTBR1에 사용

두 직접 호출의 순서는 mmu.c:790–800에 명시돼 있다. 이미지 실행 주소와 RAM 선형 주소를 같은 매핑으로 취급하지 않는다.

1. map_kernel()

map_kernel_segment()를 각 linker 구간에 호출한다. text의 보호 속성은 kernel_exec_prot()가 결정하고, rodata 구간은 이 시점에 PAGE_KERNEL로 매핑된다. 끝에서 fixmap과 KASAN shadow를 새 root로 복사한다.

2. map_mem()

일반 RAM bank 반복문에 앞서 이미지 물리 구간을 NOMAP으로 표시해 일반 선형 매핑에서 제외한다. 반복문 뒤 그 구간을 비실행 PAGE_KERNEL 선형 별칭으로 매핑하고 NOMAP 표시를 지운다. 뒤의 mark_linear_text_alias_ro()가 쓰기 권한을 제거한다.

3. root 전환과 별도 idmap

임시 fixmap을 지운 뒤 cpu_replace_ttbr1(lm_alias(swapper_pg_dir), init_idmap_pg_dir)를 호출한다. 이어 init_mm.pgd를 지정하고 초기 init_pg_dir 영역을 memblock에 반환한다. create_idmap()은 그 뒤의 별도 호출이다.

호출입력결과주의
map_kernel(pgdp)linker 구간·새 root이미지 주소의 섹션별 매핑linear alias와 분리
map_mem(pgdp)memblock RAM·이미지 물리 구간일반 RAM과 이미지 선형 별칭최종 쓰기 보호는 뒤에
cpu_replace_ttbr1()완성 root·init_idmap_pg_dirTTBR1 root 교체create_idmap()은 후속 호출

구조체와 필드

핵심은 refcount나 RCU 수명이 아니라 page-table root에 접근하는 주소와 root를 활성화하는 시점이다.

그림 3. 하나의 물리 이미지, 두 가상 주소 경로
물리 이미지: [_stext, __init_begin)같은 RAM 구간
이미지 VAmap_kernel() · text 실행 등 섹션별 보호
선형 VAmap_mem() · 비실행 별칭 → 후속 쓰기 권한 제거

NOMAP은 일반 RAM 반복문에서 이미지 구간을 잠시 제외하는 표시다. 이후 별도 별칭을 매핑하고 표시를 지운다. mmu.c:590–624

이름코드에서 하는 일시점
swapper_pg_dir새 kernel page-table rootfixmap으로 채우고 TTBR1·init_mm.pgd에 연결
pgdproot에 쓰는 임시 fixmap 포인터두 매핑 뒤 pgd_clear_fixmap()
memblock.memory선형 매핑의 RAM 범위for_each_mem_range()가 순회
init_idmap_pg_dirTTBR1 교체의 인자create_idmap()보다 앞
init_mm.pgd커널 mm에 새 root 기록TTBR1 교체 뒤

실행 단계와 상태 변화

이 순서는 v6.6 paging_init() 본문 그대로다. 나중의 권한 변경이나 page fault를 임의의 중간 단계로 끼워 넣지 않는다.

그림 4. PGD 작성에서 후속 idmap까지
① 임시 접근fixmap · idmap_t0sz
② 이미지map_kernel
③ 선형 RAMmap_mem
④ root 교체clear fixmap · TTBR1 · init_mm.pgd
⑤ 마무리init 표 반환 · resize · create_idmap

create_idmap()은 root 교체와 memblock 정리 뒤 호출된다. mmu.c:790–811

단계소스 행다음 단계의 전제
임시 PGD 접근790–795새 root에 항목을 쓸 수 있음
이미지·RAM 매핑797–798새 root의 주요 영역 준비
fixmap 해제·TTBR1 교체800–803활성 root와 init_mm.pgd 연결
초기 표 반환·resize·idmap805–810초기 자원 정리 후 별도 identity map 생성

불변 조건과 실패 사례

두 주소 경로

같은 물리 이미지 구간이라도 image VA와 linear alias의 속성은 다르다. text의 실행 속성을 linear alias에도 그대로 적용하면 잘못 읽게 된다.

임시 NOMAP

memblock_mark_nomap()은 일반 RAM 순회를 위한 임시 제외다. 이어 별도 선형 alias를 만들고 memblock_clear_nomap()을 호출한다.

rodata 보호 시점

map_mem()은 이미지 alias를 우선 비실행으로 만든다. 쓰기 권한 제거는 후속 mark_linear_text_alias_ro()에서 수행한다.

할당 실패

paging_init() 자체에는 정리용 goto가 없다. 초기 표 할당 실패는 early_pgtable_alloc()의 panic() 경로를 확인한다.

주의

NO_EXEC_MAPPINGS인 선형 RAM과 이미지의 실행 text를 같은 매핑으로 묶지 않는다.

주의

create_idmap()은 cpu_replace_ttbr1()보다 뒤다. init_idmap_pg_dir과 구별한다.

계측과 검증

paging_init()은 초기 부팅 코드다. 실행 후 page-table dump는 최종 매핑 속성을 보여줄 수 있지만 각 호출이 언제 끝났는지는 기록하지 않는다. 부팅 정지 위치는 보드에 맞춘 earlycon과 부팅 로그를 함께 읽는다.

관찰 방법확인 가능한 것한계
earlycon·부팅 로그콘솔이 살아 있는 동안의 진행·오류각 내부 호출 완료를 자동 기록하지 않음
/sys/kernel/debug/kernel_page_tables지원 빌드에서 실행 후 매핑·권한부팅 중간 상태나 call order는 알 수 없음
v6.6 고정 소스 대조함수 호출·권한 변경·memblock 처리의 정적 순서특정 보드 실행 결과는 별도 확인
읽기 전용 예시 · debugfs와 page-table dump가 있는 빌드에서만:
test -r /sys/kernel/debug/kernel_page_tables && \
  cat /sys/kernel/debug/kernel_page_tables

이 출력은 실행 후 상태를 보는 보조 자료다. 공용 tracefs 설정을 바꾸는 추적 예시는 사용하지 않는다.

추가 확인 사항

  • kernel_exec_prot()의 선택은 현재 빌드의 rodata_enabled와 어떻게 연결되는가?
  • can_set_direct_map()이 참일 때 map_mem()이 블록·연속 매핑을 제한하는 이유는 무엇인가?
  • mark_linear_text_alias_ro()는 실제 부팅 순서에서 언제 호출되는가? paging_init() 안의 직접 호출이라고 가정하지 않고 확인할 수 있는가?
  • 실행 후 page-table dump에서 이미지 VA의 text와 동일 물리 구간의 linear alias 속성은 어떻게 다른가?