전체 흐름
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 가상 주소는 같은 물리 페이지에 대한 서로 다른 매핑입니다.
진입과 주요 결과의 요약입니다. 나머지 직접 호출과 권한 변경의 분리는 그림 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.cmmu.c의 map_mem(), mark_rodata_ro(), paging_init()을 기준으로 삼습니다. setup.c:334는 초기 호출자이고, smp.c:444는 선형 별칭의 후속 read-only 변경 호출자입니다.
GitHub 원본: mmu.c · setup.c:332–344 · smp.c:438–445
검증한 파일 SHA-256: mmu.c 298F93AD54A59DF0A378221D10B62B66919B4D250F14A9B6C40E9B76BB6C8544; setup.c BD558E2A46F205DC7D2FEB73EFE393FB4702567A63DB108D79D1F166304282C4; smp.c 4EE0C9F17431C2F6174C7F4C7C12608EBC915063390879A202268B1E4074A427.
대표 코드
세 발췌는 고정한 arch/arm64/mm/mmu.c의 물리 행과 문자 단위로 대조했습니다. mark_rodata_ro()는 paging_init()에서 직접 호출되지 않는 별도 함수입니다.
paging_init() · 직접 호출 순서
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 제외와 선형 별칭 재매핑
/*
* 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() · 커널 이미지 주소의 권한 변경
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입니다.
2. map_mem(): 일반 RAM과 이미지 물리 구간
커널 이미지 [_text, __init_begin)를 잠시 NOMAP으로 표시해 일반 for_each_mem_range() 순회에서 제외합니다. 나머지 bank를 매핑한 뒤 이미지 구간의 non-executable 선형 별칭을 별도로 만들고 NOMAP을 해제합니다.
3. mark_rodata_ro(): 별도 이미지 주소 보호
mark_rodata_ro()는 paging_init()의 직접 호출이 아닙니다. PAGE_KERNEL_RO로 이미지 주소의 rodata 범위를 바꾸며, 선형 별칭은 mark_linear_text_alias_ro()가 별도로 바꿉니다. live mapping을 바꾸는 공통 helper는 변경 뒤 kernel TLB를 flush합니다.
| 함수 | 직접 하는 일 | 혼동하기 쉬운 경계 |
|---|---|---|
paging_init() | map_mem → allow_resize → create_idmap → declare_kernel_vmas | mark_rodata_ro() 호출은 없음 |
map_mem() | RAM bank와 이미지 구간의 선형 별칭 구성 | 일반 bank loop와 이미지 재매핑 분리 |
mark_rodata_ro() | 커널 이미지 주소의 rodata 등을 RO로 변경 | 선형 별칭의 RO 변경과 별개 |
구조체와 필드
각 값이 매핑의 입력인지 결과인지 구분하면, 임시 매핑과 완성된 선형 매핑을 혼동하지 않게 됩니다.
| 값 | 실제 역할 | 근거 |
|---|---|---|
swapper_pg_dir | paging_init()이 map_mem()에 전달하는 page-table root | mmu.c:1426–1429 |
memblock.memory | 선형 매핑할 물리 RAM bank 범위의 입력 | 1178–1191 |
[_text, __init_begin) | 일반 loop에서 잠시 제외하고 별도로 매핑할 이미지 물리 범위 | 1144–1147, 1172–1205 |
init_mm.pgd | 후속 live mapping 권한 변경에 쓰는 root | 1028–1041 |
rodata_is_rw | mark_rodata_ro()에서 false로 바꾸는 권한 상태 | 1209–1220 |
실행 단계와 상태 변화
일반 RAM bank와 커널 이미지 물리 구간의 매핑 경로가 다릅니다. 이미지를 일반 loop에서 잠시 제외하는 NOMAP과 나중에 가상 주소의 권한을 read-only로 바꾸는 동작도 서로 다릅니다.
매핑 단계와 두 주소 별칭
주소 배치는 설명용입니다. mark_rodata_ro()는 위 paging_init() 직접 호출 순서에 포함되지 않습니다.
| 구간·시점 | 코드가 하는 일 | 확인할 결과 |
|---|---|---|
| 일반 RAM bank | for_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
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