전체 흐름
arm64 Linux v6.6의 setup_arch()는 arm64_memblock_init() 다음에 paging_init()을 호출한다. 여기서 다루는 것은 실행 중 프로세스의 page fault가 아니라 부팅 초기에 swapper_pg_dir에 커널 이미지 매핑과 RAM 선형 매핑을 만드는 작업이다.
map_kernel()은 커널 이미지의 가상 주소 구간을 섹션별로 매핑한다. map_mem()은 memblock RAM에 선형 별칭을 만든다.
두 매핑 뒤 fixmap을 지우고 TTBR1을 새 root로 교체한다. init_mm.pgd를 설정하고 초기 표 메모리를 반환한 후 별도의 idmap을 만든다.
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–347 | memblock 준비 다음 paging_init() 호출 |
arch/arm64/mm/mmu.c:722–758 | map_kernel()의 이미지 섹션 매핑 |
arch/arm64/mm/mmu.c:566–625 | map_mem()의 RAM bank·이미지 별칭 매핑 |
arch/arm64/mm/mmu.c:790–811 | paging_init()의 호출·전환·마무리 |
대표 코드
세 상자는 각각 지정한 함수의 연속된 전체 본문이다. 서로 다른 함수의 앞뒤 줄을 이어 붙이지 않았다.
paging_init()
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()
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()
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에 서로 다른 가상 주소 영역을 채운다.
두 직접 호출의 순서는 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_dir | TTBR1 root 교체 | create_idmap()은 후속 호출 |
구조체와 필드
핵심은 refcount나 RCU 수명이 아니라 page-table root에 접근하는 주소와 root를 활성화하는 시점이다.
NOMAP은 일반 RAM 반복문에서 이미지 구간을 잠시 제외하는 표시다. 이후 별도 별칭을 매핑하고 표시를 지운다. mmu.c:590–624
| 이름 | 코드에서 하는 일 | 시점 |
|---|---|---|
swapper_pg_dir | 새 kernel page-table root | fixmap으로 채우고 TTBR1·init_mm.pgd에 연결 |
pgdp | root에 쓰는 임시 fixmap 포인터 | 두 매핑 뒤 pgd_clear_fixmap() |
memblock.memory | 선형 매핑의 RAM 범위 | for_each_mem_range()가 순회 |
init_idmap_pg_dir | TTBR1 교체의 인자 | create_idmap()보다 앞 |
init_mm.pgd | 커널 mm에 새 root 기록 | TTBR1 교체 뒤 |
실행 단계와 상태 변화
이 순서는 v6.6 paging_init() 본문 그대로다. 나중의 권한 변경이나 page fault를 임의의 중간 단계로 끼워 넣지 않는다.
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·idmap | 805–810 | 초기 자원 정리 후 별도 identity map 생성 |
불변 조건과 실패 사례
같은 물리 이미지 구간이라도 image VA와 linear alias의 속성은 다르다. text의 실행 속성을 linear alias에도 그대로 적용하면 잘못 읽게 된다.
memblock_mark_nomap()은 일반 RAM 순회를 위한 임시 제외다. 이어 별도 선형 alias를 만들고 memblock_clear_nomap()을 호출한다.
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 처리의 정적 순서 | 특정 보드 실행 결과는 별도 확인 |
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 속성은 어떻게 다른가?