Memory · Linux 6.18.37 LTS · source + diagram note

paging_init

arm64 · Linux v6.18.37: paging_init()의 선형 매핑 생성과 별도 권한 변경 경로를 원본 코드의 실제 호출 순서로 읽습니다.

전체 흐름

arm64의 setup_arch()는 memblock 준비 뒤 paging_init()을 호출합니다. 현재 v6.18.37의 paging_init()은 map_mem(swapper_pg_dir), memblock_allow_resize(), create_idmap(), declare_kernel_vmas() 순서입니다. mark_rodata_ro()는 이 함수의 직접 호출 목록에 없습니다.

이 페이지의 질문

어느 물리 RAM 구간을 선형 매핑하고, 왜 커널 이미지 구간을 일반 bank loop에서 잠시 제외한 뒤 다시 매핑할까요?

구분할 주소

커널 이미지의 linear alias와 이미지 자체의 .rodata 가상 주소는 같은 물리 페이지에 대한 서로 다른 매핑입니다.

그림 1. arm64 초기화의 진입과 선형 매핑
setup_arch()arm64_memblock_init() 뒤 호출
→
paging_init()mmu.c:1426–1434
→
map_mem()memblock 범위를 매핑
→
linear mapRAM의 가상 별칭

진입과 주요 결과의 요약입니다. 나머지 직접 호출과 권한 변경의 분리는 그림 2에 있습니다. setup.c:332–344, mmu.c:1426–1434

소스 코드 위치

고정한 Linux v6.18.37 원본

원본 코드:
arch/arm64/mm/mmu.c
arch/arm64/kernel/setup.c
arch/arm64/kernel/smp.c

mmu.c의 map_mem(), mark_rodata_ro(), paging_init()을 기준으로 삼습니다. setup.c:334는 초기 호출자이고, smp.c:444는 선형 별칭의 후속 read-only 변경 호출자입니다.

검증한 파일 SHA-256: mmu.c 298F93AD54A59DF0A378221D10B62B66919B4D250F14A9B6C40E9B76BB6C8544; setup.c BD558E2A46F205DC7D2FEB73EFE393FB4702567A63DB108D79D1F166304282C4; smp.c 4EE0C9F17431C2F6174C7F4C7C12608EBC915063390879A202268B1E4074A427.

대표 코드

세 발췌는 고정한 arch/arm64/mm/mmu.c의 물리 행과 문자 단위로 대조했습니다. mark_rodata_ro()는 paging_init()에서 직접 호출되지 않는 별도 함수입니다.

paging_init() · 직접 호출 순서

원본 코드: mmu.c:1426–1434
void __init paging_init(void)
{
	map_mem(swapper_pg_dir);

	memblock_allow_resize();

	create_idmap();
	declare_kernel_vmas();
}

이 버전은 swapper_pg_dir를 직접 map_mem()에 넘기고, 이어서 identity map과 kernel VMA를 준비합니다.

map_mem() · NOMAP 제외와 선형 별칭 재매핑

원본 코드: mmu.c:1172–1207 (함수 정의 1142–1150)
	/*
	 * 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 [_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);
	arm64_kfence_map_pool(early_kfence_pool, pgdp);
}

함수 시작에서 flags = NO_EXEC_MAPPINGS로 두고, force_pte_mapping()이면 block/contiguous mapping도 제한합니다. 이미지 물리 구간은 일반 loop에서 잠시 빼고 PAGE_KERNEL의 선형 별칭으로 따로 매핑합니다.

mark_rodata_ro() · 커널 이미지 주소의 권한 변경

원본 코드: mmu.c:1209–1225
void mark_rodata_ro(void)
{
	unsigned long section_size;

	/*
	 * mark .rodata as read only. Use __init_begin rather than __end_rodata
	 * to cover NOTES and EXCEPTION_TABLE.
	 */
	section_size = (unsigned long)__init_begin - (unsigned long)__start_rodata;
	WRITE_ONCE(rodata_is_rw, false);
	update_mapping_prot(__pa_symbol(__start_rodata), (unsigned long)__start_rodata,
			    section_size, PAGE_KERNEL_RO);
	/* mark the range between _text and _stext as read only. */
	update_mapping_prot(__pa_symbol(_text), (unsigned long)_text,
			    (unsigned long)_stext - (unsigned long)_text,
			    PAGE_KERNEL_RO);
}

__start_rodata부터 __init_begin까지와 _text부터 _stext까지의 커널 이미지 가상 주소를 PAGE_KERNEL_RO로 바꿉니다. 이는 선형 별칭의 read-only 변경과 다른 호출입니다.

함수별 분석

setup_arch()에서 들어온 뒤 paging_init()은 선형 매핑을 먼저 만들고 memblock metadata 확장을 허용한 다음 identity map과 커널 VMA를 준비합니다.

권한을 한 번에 완성하는 함수가 아닙니다. mark_rodata_ro()와 mark_linear_text_alias_ro()는 서로 다른 주소 별칭을 후속 단계에서 변경합니다.

1. paging_init(): 초기 호출 경계

map_mem(swapper_pg_dir) 뒤 memblock_allow_resize(), create_idmap(), declare_kernel_vmas()를 호출합니다. swapper_pg_dir는 map_mem()에 전달하는 page-table root입니다.

mmu.c:1426–1434

2. map_mem(): 일반 RAM과 이미지 물리 구간

커널 이미지 [_text, __init_begin)를 잠시 NOMAP으로 표시해 일반 for_each_mem_range() 순회에서 제외합니다. 나머지 bank를 매핑한 뒤 이미지 구간의 non-executable 선형 별칭을 별도로 만들고 NOMAP을 해제합니다.

mmu.c:1172–1207

3. mark_rodata_ro(): 별도 이미지 주소 보호

mark_rodata_ro()는 paging_init()의 직접 호출이 아닙니다. PAGE_KERNEL_RO로 이미지 주소의 rodata 범위를 바꾸며, 선형 별칭은 mark_linear_text_alias_ro()가 별도로 바꿉니다. live mapping을 바꾸는 공통 helper는 변경 뒤 kernel TLB를 flush합니다.

mmu.c:1028–1059, 1209–1225, smp.c:438–445

함수직접 하는 일혼동하기 쉬운 경계
paging_init()map_mem → allow_resize → create_idmap → declare_kernel_vmasmark_rodata_ro() 호출은 없음
map_mem()RAM bank와 이미지 구간의 선형 별칭 구성일반 bank loop와 이미지 재매핑 분리
mark_rodata_ro()커널 이미지 주소의 rodata 등을 RO로 변경선형 별칭의 RO 변경과 별개

구조체와 필드

각 값이 매핑의 입력인지 결과인지 구분하면, 임시 매핑과 완성된 선형 매핑을 혼동하지 않게 됩니다.

값실제 역할근거
swapper_pg_dirpaging_init()이 map_mem()에 전달하는 page-table rootmmu.c:1426–1429
memblock.memory선형 매핑할 물리 RAM bank 범위의 입력1178–1191
[_text, __init_begin)일반 loop에서 잠시 제외하고 별도로 매핑할 이미지 물리 범위1144–1147, 1172–1205
init_mm.pgd후속 live mapping 권한 변경에 쓰는 root1028–1041
rodata_is_rwmark_rodata_ro()에서 false로 바꾸는 권한 상태1209–1220

실행 단계와 상태 변화

일반 RAM bank와 커널 이미지 물리 구간의 매핑 경로가 다릅니다. 이미지를 일반 loop에서 잠시 제외하는 NOMAP과 나중에 가상 주소의 권한을 read-only로 바꾸는 동작도 서로 다릅니다.

매핑 단계와 두 주소 별칭

그림 2. arm64 paging_init의 선형 매핑과 후속 권한 변경
setup_arch에서 paging_init으로 들어가 map_mem, memblock_allow_resize, create_idmap, declare_kernel_vmas 순서로 호출한다. map_mem은 일반 RAM을 매핑하고 커널 이미지 구간은 NOMAP으로 잠시 제외한 뒤 별도 선형 별칭으로 만든다. mark_rodata_ro와 mark_linear_text_alias_ro는 각기 다른 주소 별칭을 후속 보호한다.

그림 크게 열기

주소 배치는 설명용입니다. mark_rodata_ro()는 위 paging_init() 직접 호출 순서에 포함되지 않습니다.

같은 이미지 물리 구간을 둘러싼 매핑과 권한 단계
구간·시점코드가 하는 일확인할 결과
일반 RAM bankfor_each_mem_range()와 __map_memblock(..., pgprot_tagged(PAGE_KERNEL), flags)선형 매핑; flags 기본값은 NO_EXEC_MAPPINGS
커널 이미지 물리 구간memblock_mark_nomap()으로 일반 loop에서 제외한 뒤 PAGE_KERNEL, NO_CONT_MAPPINGS로 별도 매핑하고 memblock_clear_nomap()non-executable 선형 별칭. NOMAP은 영구 권한이 아님
paging_init()의 나머지memblock_allow_resize() → create_idmap() → declare_kernel_vmas()이 직접 호출 목록에 mark_rodata_ro()는 없음
선형 별칭의 후속 보호mark_linear_text_alias_ro()가 [_text, __init_begin)의 linear alias를 RO로 변경smp_cpus_done()에서 호출
이미지 주소의 후속 보호mark_rodata_ro()가 __start_rodata→__init_begin 등 이미지 주소에 PAGE_KERNEL_RO 적용live mapping 변경 helper가 kernel TLB flush

그림·표 근거: mmu.c:1028–1059, 1142–1225, 1426–1434; setup.c:332–334; smp.c:438–445. 고정 소스 SHA-256은 소스 코드 위치에 기록했습니다.

불변 조건과 실패 사례

이미지 구간의 임시 제외

memblock_mark_nomap()은 일반 RAM loop에서 이미지 구간을 건너뛰기 위한 조치입니다. 별도 선형 매핑 후 memblock_clear_nomap()으로 해제합니다. mmu.c:1172–1206

주소 별칭마다 다른 권한

이미지 주소의 .rodata 보호와 이미지 물리 구간의 linear alias 보호를 구분해야 합니다. 한쪽의 RO가 다른 쪽의 RO를 증명하지 않습니다. 1051–1059, 1209–1225

live mapping 변경

update_mapping_prot()는 page-table 권한 변경 뒤 해당 kernel 범위의 TLB를 flush합니다. 1028–1041

초기 할당 실패

early_pgtable_alloc()은 page-table page 할당 실패 시 panic()합니다. 이 경로를 일반적인 반환값 기반 복구 단계로 그리면 안 됩니다. 117–126

계측과 검증

paging_init()은 arm64 초기 부팅 경로의 __init 함수입니다. 부팅 뒤 tracefs에 함수를 등록해도 이미 지난 호출을 재생할 수는 없습니다. 완성된 매핑과 권한은 arm64 ptdump로 점검합니다.

방법확인 가능한 것한계
CONFIG_PTDUMP_DEBUGFS=y와 kernel_page_tables부팅 후 가상 주소 구간의 PTE/block 크기·권한·실행 가능 여부초기 함수 호출 시각이나 일시적인 NOMAP 상태는 보여주지 않음
고정 소스의 map_mem()·mark_rodata_ro()어떤 범위에 어떤 보호 속성을 적용하는지실제 보드의 최종 page-table 결과는 ptdump와 대조 필요
부팅 후 권한 확인 예시:
# CONFIG_PTDUMP_DEBUGFS=y인 arm64 커널에서
mount -t debugfs nodev /sys/kernel/debug
cat /sys/kernel/debug/kernel_page_tables

arm64 문서의 ptdump.rst:22–39와 등록 코드 ptdump.c:402–409를 참고하세요.

추가 확인 사항

  • 대상 보드에서 force_pte_mapping()이 참이면 map_mem()의 block/contiguous mapping 선택이 어떻게 달라지나요? mmu.c:1165–1170
  • 커널 이미지 물리 구간을 일반 RAM loop에서 제외하지 않으면 어떤 writable alias가 생길 수 있나요? 1172–1205
  • ptdump에서 선형 별칭과 이미지 .rodata 주소의 권한을 각각 확인했나요? ptdump.rst:22–39