physical RAM을 kernel pointer로 쓰려면 direct map이 필요합니다. 그런데 direct map의 page table도 RAM에 만들고 초기화해야 합니다. 아직 연결하지 않은 RAM에 새 table을 놓는다면 그 table부터 접근할 수 없게 됩니다.
초기화는 이 순환을 작은 출발점으로 끊습니다. 먼저 kernel image 안에서 접근 가능한 table buffer를 마련하고, 새롭게 mapping한 RAM이 다음 table의 공급지가 되도록 범위를 넓힙니다. x86-64의 이 과정을 upstream Linux v6.18 기준으로 살펴보겠습니다.
table을 위한 table이 필요합니다
boot 시점에도 실행 중인 kernel image와 이미 준비된 root는 있습니다. kernel-global mm인 init_mm은 swapper_pg_dir을 root로 사용합니다. direct map 초기화는 아무 translation도 없는 상태에서 PGD부터 새로 만드는 작업과 다릅니다. 먼저 그 root 아래에 필요한 table을 채웁니다.
early_pgt_alloc은 초기 kernel image의 brk 공간에 예약한 작은 buffer입니다. 이 공간을 실제로 사용하려면 extend_brk()로 요청하고 physical 위치를 얻어야 합니다. kernel image를 접근할 수 있다는 사실이 초기 table을 쓸 첫 번째 통로입니다.
x86 초기화는 ISA의 첫 1 MiB를 먼저 처리하는 특수 요구도 갖습니다. 따라서 buffer는 ISA 연결뿐 아니라 이후 table allocation을 이어 갈 RAM 범위를 여는 데에도 충분해야 합니다. 전체 RAM의 모든 table을 이 작은 buffer에 넣겠다는 설계는 아닙니다.
초기 buffer의 세 경계
pgt_buf_start는 buffer 시작 PFN, pgt_buf_end는 지금까지 사용한 부분의 exclusive end, pgt_buf_top은 용량의 끝입니다. 아직 비어 있으면 start와 end가 같습니다. table 몇 page를 소비할 때마다 end를 올리되 top을 넘을 수는 없습니다.
최악의 경우에는 huge leaf를 사용하지 못하므로 하위 level의 table page를 각각 준비해야 합니다. KASLR 때문에 상위 경계를 가로지르면 추가 table도 필요할 수 있습니다. 이 여유가 단순히 data page 몇 장을 미리 갖고 있는 것과 다른 점입니다.
예약과 사용을 분리해서 보면 세 경계가 더 분명해집니다. 아래 첫 block은 v6.18의 RESERVE_BRK 호출 전체이고, 두 번째는 이어지는 early_alloc_pgt_buf() 전체입니다. macro 정의 전체를 보여 주는 block은 아닙니다. 예약한 용량을 extend_brk()로 실제 소비하고 __pa()로 physical base를 얻은 뒤, 사용 끝은 시작과 같게, 용량 끝은 page 수만큼 위에 둡니다. byte 단위의 tables와 PFN 단위의 세 경계를 섞지 않아야 합니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:187-187
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete macro_invocation: RESERVE_BRK(early_pgt_alloc)
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
RESERVE_BRK(early_pgt_alloc, INIT_PGT_BUF_SIZE);
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:188-198
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete function: early_alloc_pgt_buf
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
void __init early_alloc_pgt_buf(void)
{
unsigned long tables = INIT_PGT_BUF_SIZE;
phys_addr_t base;
base = __pa(extend_brk(tables, PAGE_SIZE));
pgt_buf_start = base >> PAGE_SHIFT;
pgt_buf_end = pgt_buf_start;
pgt_buf_top = pgt_buf_start + (tables >> PAGE_SHIFT);
}
과거의 level 수 기반 설명과 현재 상수는 구별해야 합니다. v6.18의 INIT_PGD_PAGE_TABLES는 4이고 CONFIG_RANDOMIZE_MEMORY에 따라 그 두 배 또는 네 배를 초기 page 수로 잡습니다. runtime에서 4-level로 실행한다고 이 상수를 곧바로 3으로 치환하는 계산은 확인한 코드와 다릅니다.

큰 page의 사용 가능성
RAM 범위를 크게 덮으려면 2 MiB·1 GiB leaf가 유리하지만 kernel이 언제나 사용할 수 있는 것은 아닙니다. probe_page_size_mask()는 CPU feature와 debug 조건으로 허용 mask를 만듭니다. 큰 page 사용 여부와 접근 permission mask의 준비가 같은 초기화 함수 주변에 있어도 서로 다른 판단입니다.
2 MiB에는 X86_FEATURE_PSE와 debug_pagealloc_enabled() 조건이 관여합니다. debug page allocation은 작은 page 단위 permission 변경을 다루기 위해 큰 mapping을 억제할 수 있습니다. 그 경우 direct_gbpages도 꺼집니다.
1 GiB에는 X86_FEATURE_GBPAGES뿐 아니라 direct_gbpages의 정책도 필요합니다. build 기본값과 gbpages·nogbpages 같은 boot 선택을 무시하고 현대 CPU이면 무조건 켜진다고 단정할 수 없습니다. 확인한 source는 실제로 사용할 수 있는 크기를 page_size_mask에 넣습니다.
debug 조건 때문에 mapper의 큰 page 사용을 끄더라도 CPU가 PSE를 지원하면 CR4의 PSE 자체를 활성화하는 분기는 별도로 실행됩니다. CPU control bit를 켜는 일과 이 direct-map 알고리즘이 2 MiB leaf를 선택하도록 허용하는 일은 같지 않습니다. 마찬가지로 1 GiB 조건을 만족하지 못하면 direct_gbpages를 0으로 되돌리므로, 초기 policy 값 하나만 보지 말고 probe 뒤의 상태를 보아야 합니다.
초기화는 CR4의 PSE·PGE와 지원 PTE mask도 준비합니다. PTI이면 kernel의 기본 GLOBAL 사용을 제한하는 처리가 있습니다. CPU가 feature를 지원하는지, kernel이 feature를 활성화하는지, 특정 range가 그 크기·flags를 사용할 수 있는지는 세 단계의 질문입니다.
전체 함수에서는 GLOBAL 처리 순서도 확인할 수 있습니다. 먼저 __supported_pte_mask의 GLOBAL을 지우고, PGE 지원 CPU일 때 CR4를 설정하면서 지원 bit를 다시 더합니다. 그 결과를 __default_kernel_pte_mask로 복사한 뒤 PTI이면 기본 kernel mask에서만 GLOBAL을 다시 지웁니다. 지원 가능 mask와 기본 사용 mask가 항상 같다는 뜻이 아닙니다. 아래는 v6.18 probe_page_size_mask() 전체이며 크기 probe 부분만 뗀 축약본이 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:225-261
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete function: probe_page_size_mask
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
static void __init probe_page_size_mask(void)
{
/*
* For pagealloc debugging, identity mapping will use small pages.
* This will simplify cpa(), which otherwise needs to support splitting
* large pages into small in interrupt context, etc.
*/
if (boot_cpu_has(X86_FEATURE_PSE) && !debug_pagealloc_enabled())
page_size_mask |= 1 << PG_LEVEL_2M;
else
direct_gbpages = 0;
/* Enable PSE if available */
if (boot_cpu_has(X86_FEATURE_PSE))
cr4_set_bits_and_update_boot(X86_CR4_PSE);
/* Enable PGE if available */
__supported_pte_mask &= ~_PAGE_GLOBAL;
if (boot_cpu_has(X86_FEATURE_PGE)) {
cr4_set_bits_and_update_boot(X86_CR4_PGE);
__supported_pte_mask |= _PAGE_GLOBAL;
}
/* By the default is everything supported: */
__default_kernel_pte_mask = __supported_pte_mask;
/* Except when with PTI where the kernel is mostly non-Global: */
if (cpu_feature_enabled(X86_FEATURE_PTI))
__default_kernel_pte_mask &= ~_PAGE_GLOBAL;
/* Enable 1 GB linear kernel mappings if available: */
if (direct_gbpages && boot_cpu_has(X86_FEATURE_GBPAGES)) {
printk(KERN_INFO "Using GB pages for direct mapping\n");
page_size_mask |= 1 << PG_LEVEL_1G;
} else {
direct_gbpages = 0;
}
}
이 준비를 사용하는 상위 함수가 init_mem_mapping()입니다. 현재 x86-64 경로는 boot-time PTI 판단, page-size mask 준비와 PCID 설정을 거친 뒤 max_pfn << PAGE_SHIFT를 physical 상한으로 삼습니다. ISA 범위는 일반 RAM hole 순회를 우회해 먼저 연결하고, real-mode trampoline의 page-table 기준을 준비합니다. trampoline은 보조 CPU의 시작에도 필요한 장치이므로 이 호출들을 모두 같은 취약점 완화 작업으로 묶는 것은 정확하지 않습니다. 이후에야 memblock의 방향에 따라 나머지 direct map을 넓힙니다. v6.18 init_mem_mapping()과 init_trampoline()은 이 순서를 명시합니다.
bottom-up은 조금씩 넓힙니다
memblock_bottom_up()은 table 공급지를 어느 방향에 둘 것인지와 관련됩니다. 아래에서 시작하면 kernel image 가까운 쪽의 RAM을 먼저 사용할 수 있습니다. 예를 들어 hot-pluggable memory를 movable 용도로 남기려는 구성에서는 unmovable table을 높은 RAM에 먼저 배치하는 것이 바람직하지 않을 수 있습니다.
그 때문에 상위 함수의 bottom-up 경로는 두 번 호출됩니다. 먼저 __pa_symbol(_end)로 구한 kernel image 끝부터 physical 상한까지 mapping하고, 그곳에서 table을 공급할 수 있게 된 뒤 ISA 끝에서 kernel image 끝까지를 연결합니다. 단순히 physical 주소 0부터 계속 오르는 한 번의 loop가 아닙니다. movable_node를 사용하는 hotplug 구성에서는 이러한 배치 선택이 높은 hot-pluggable memory에 unmovable page table을 먼저 두지 않으려는 목적과 연결됩니다.
memory_map_bottom_up()은 처음부터 전체 range를 한 번에 처리하지 않습니다. 초기 step_size는 PMD_SIZE, 즉 2 MiB입니다. huge page를 못 쓰더라도 이 정도의 시작 범위는 작은 brk buffer로 다음 공급지를 열 수 있도록 하는 선택입니다.
현재 start가 step 경계에 맞지 않으면 round_up(start + 1, step_size)를 이용해 다음 경계까지 진행합니다. +1은 이미 경계에 있는 start에서 같은 값을 다시 얻어 진행하지 못하는 일을 피합니다. 끝은 map_end로 제한하며 step 산술의 overflow·0 상태도 고려합니다.
중요한 측정량은 virtual 구간을 얼마나 훑었는지가 아니라 실제 mapping한 RAM의 양입니다. RAM이 충분히 연결되면 get_new_step_size()로 다음 단계를 키웁니다. 확인한 식의 shift는 PMD_SHIFT - PAGE_SHIFT - 1이며 4 KiB·2 MiB 관계에서는 8 bit, 즉 256배입니다. 2 MiB 다음 후보가 512 MiB가 되는 이유입니다.
여기서 2로 나눈다는 설명은 table fan-out 512보다 여유를 둔 성장 계수에 관한 것입니다. 현재 step을 계속 절반으로 줄이는 알고리즘이 아닙니다. 1 GiB를 모두 4 KiB로 표현하면 PTE table만 512 page, 즉 2 MiB가 필요하고 상위 경계를 넘으면 그 밖의 table 비용도 더해집니다. upper level의 개수에 따라 여유 비용이 달라지므로 하나의 오래된 주석 수치를 모든 5-level 경계의 절대값으로 삼을 수 없습니다.
top-down은 쓸 수 있는 RAM에서 시작합니다
가장 높은 physical 주소 바로 아래에 충분한 RAM이 있다는 보장은 없습니다. EFI나 VM의 배치처럼 reserved range가 많이 섞여 있다면 작은 RAM 조각을 연결하는 동안 초기 buffer를 소비하고도 다음 table을 둘 공간을 얻지 못할 수 있습니다.
memory_map_top_down()은 PMD_SIZE 크기와 alignment를 만족하는 memblock allocation을 한 번 시도한 뒤 바로 반환합니다. 목적은 그 page를 영구적으로 보유하는 것이 아니라 높은 위치의 쓸 수 있는 연속 RAM 범위를 알아내는 것입니다. allocation을 반환했다는 사실과 실제 table이 거기에 이미 설치됐다는 사실을 혼동해서는 안 됩니다.
이 탐색 결과로 real_end를 정한 뒤 작은 step부터 아래로 mapping합니다. 뒤쪽으로 조금씩 확장하더라도 이미 mapping한 RAM에서 table을 공급해야 한다는 조건은 같습니다. 처음 탐색에서 건너뛴 위쪽 범위는 이후 별도로 처리할 수 있습니다.
loop 안에서는 last_start - 1을 현재 step의 아래 경계로 내리고 그 결과를 map_start 이상으로 제한합니다. 끝에서 1을 빼는 것은 이미 정렬된 경계에서도 다음 아래 구간으로 진행하기 위한 선택입니다. mapping한 RAM 양을 누적해 step을 키우고, min_pfn_mapped도 내려갑니다. loop가 끝난 뒤 real_end < map_end이면 처음 건너뛴 [real_end, map_end)를 별도로 연결합니다. 이 마지막 호출까지 있어야 탐색에 사용한 연속 RAM 블록 위쪽이 영구적인 누락으로 남지 않습니다.
현재 구현에는 탐색 실패의 warning과 정렬된 끝을 사용하는 fallback도 있습니다. 탐색 allocation 한 번의 실패와 최종 table allocation 불능의 panic은 다른 단계입니다. max_pfn 역시 상한일 뿐 그 아래 전체가 연속 usable RAM이라는 뜻은 아닙니다.
32-bit에서 step이 0이 되는 경우
step의 자료형은 unsigned long입니다. 4 KiB base page와 2 MiB PMD 관계에서 256배씩 키우면 2 MiB, 512 MiB, 128 GiB 순서가 되지만, 32-bit unsigned long에서는 512 MiB를 8 bit shift한 값이 범위를 벗어나 0으로 wrap합니다. 이를 더 큰 signed 값으로 보정하는 일반 산술로 읽으면 제어 흐름이 달라집니다.
v6.18의 bottom-up loop는 step_size && ...를 먼저 검사합니다. step이 0이면 round_up(start + 1, 0)을 호출하지 않고 바로 next = map_end를 선택합니다. round_up()의 공개 계약은 power-of-two 단위이지만 현재 macro 식에 0을 대입하면 unsigned wrap으로 0이 나오므로, 그대로 다음 cursor로 사용하면 뒤로 돌아가거나 진행하지 못할 수 있습니다. 이 helper 내부의 overflow 방어를 다른 caller가 0 단위 rounding을 사용해도 된다는 허가로 일반화해서는 안 됩니다.
top-down은 다르게 종료합니다. 남은 last_start가 0보다 크고 step이 0이면 현재 round_down(last_start - 1, 0)의 bit 식 결과는 0입니다. 이어 map_start보다 작을 때 clamp하므로 남은 아래 범위를 한 번에 처리합니다. loop의 cursor가 하한까지 내려가며 끝납니다. 정확한 macro는 include/linux/math.h에 있습니다.
한 가지 주석상의 함정이 있습니다. 책과 현재 get_new_step_size()의 comment에는 이 0이 0x100000000ULL로 바뀐다는 설명이 남아 있지만, 아래 v6.18의 실제 memory_map_top_down()에는 그런 변환 분기가 없습니다. local start도 unsigned long입니다. 여기서는 확인한 현재 branch의 0과 clamp를 설명하며, 오래된 comment를 근거로 32-bit code가 실제 4 GiB 값을 그 변수에 저장한다고 주장하지 않습니다.
다음 세 파일은 v6.18 init.c의 전체 함수 정의입니다. standalone program이 아니라 kernel 내부 code이며 실행하지 않았습니다. 해당 source에는 개별 SPDX header가 없으므로 kernel COPYING과 license 규칙에 따른 GPL-2.0 code로 구별합니다. 전체 출처 기록과 license text를 제공합니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:599-616
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete function: get_new_step_size
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
static unsigned long __init get_new_step_size(unsigned long step_size)
{
/*
* Initial mapped size is PMD_SIZE (2M).
* We can not set step_size to be PUD_SIZE (1G) yet.
* In worse case, when we cross the 1G boundary, and
* PG_LEVEL_2M is not set, we will need 1+1+512 pages (2M + 8k)
* to map 1G range with PTE. Hence we use one less than the
* difference of page table level shifts.
*
* Don't need to worry about overflow in the top-down case, on 32bit,
* when step_size is 0, round_down() returns 0 for start, and that
* turns it into 0x100000000ULL.
* In the bottom-up case, round_up(x, 0) returns 0 though too, which
* needs to be taken into consideration by the code below.
*/
return step_size << (PMD_SHIFT - PAGE_SHIFT - 1);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:698-730
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete function: memory_map_bottom_up
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
static void __init memory_map_bottom_up(unsigned long map_start,
unsigned long map_end)
{
unsigned long next, start;
unsigned long mapped_ram_size = 0;
/* step_size need to be small so pgt_buf from BRK could cover it */
unsigned long step_size = PMD_SIZE;
start = map_start;
min_pfn_mapped = start >> PAGE_SHIFT;
/*
* We start from the bottom (@map_start) and go to the top (@map_end).
* The memblock_find_in_range() gets us a block of RAM from the
* end of RAM in [min_pfn_mapped, max_pfn_mapped) used as new pages
* for page table.
*/
while (start < map_end) {
if (step_size && map_end - start > step_size) {
next = round_up(start + 1, step_size);
if (next > map_end)
next = map_end;
} else {
next = map_end;
}
mapped_ram_size += init_range_memory_mapping(start, next);
start = next;
if (mapped_ram_size >= step_size)
step_size = get_new_step_size(step_size);
}
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:628-685
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete function: memory_map_top_down
* Body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
static void __init memory_map_top_down(unsigned long map_start,
unsigned long map_end)
{
unsigned long real_end, last_start;
unsigned long step_size;
unsigned long addr;
unsigned long mapped_ram_size = 0;
/*
* Systems that have many reserved areas near top of the memory,
* e.g. QEMU with less than 1G RAM and EFI enabled, or Xen, will
* require lots of 4K mappings which may exhaust pgt_buf.
* Start with top-most PMD_SIZE range aligned at PMD_SIZE to ensure
* there is enough mapped memory that can be allocated from
* memblock.
*/
addr = memblock_phys_alloc_range(PMD_SIZE, PMD_SIZE, map_start,
map_end);
if (!addr) {
pr_warn("Failed to release memory for alloc_low_pages()");
real_end = max(map_start, ALIGN_DOWN(map_end, PMD_SIZE));
} else {
memblock_phys_free(addr, PMD_SIZE);
real_end = addr + PMD_SIZE;
}
/* step_size need to be small so pgt_buf from BRK could cover it */
step_size = PMD_SIZE;
max_pfn_mapped = 0; /* will get exact value next */
min_pfn_mapped = real_end >> PAGE_SHIFT;
last_start = real_end;
/*
* We start from the top (end of memory) and go to the bottom.
* The memblock_find_in_range() gets us a block of RAM from the
* end of RAM in [min_pfn_mapped, max_pfn_mapped) used as new pages
* for page table.
*/
while (last_start > map_start) {
unsigned long start;
if (last_start > step_size) {
start = round_down(last_start - 1, step_size);
if (start < map_start)
start = map_start;
} else
start = map_start;
mapped_ram_size += init_range_memory_mapping(start,
last_start);
last_start = start;
min_pfn_mapped = last_start >> PAGE_SHIFT;
if (mapped_ram_size >= step_size)
step_size = get_new_step_size(step_size);
}
if (real_end < map_end)
init_range_memory_mapping(real_end, map_end);
}
memblock 범위를 clamp합니다
상위 알고리즘이 [r_start, r_end)를 요청해도 실제 RAM은 그 안에서 여러 조각일 수 있습니다. init_range_memory_mapping()은 memblock의 PFN range를 순회하고 각 start·end를 요청 범위에 clamp합니다. clamp 결과가 같거나 뒤집히면 건너뛰어야 합니다.
이 순회가 일반적인 physical hole을 제외합니다. 주소가 큰 폭으로 진행됐다고 그 폭만큼 table 공급 RAM을 얻은 것은 아니므로 반환하는 mapped RAM 크기도 별도로 누적합니다. 초기 ISA 범위를 직접 처리하는 architecture 예외와 이 일반 RAM 순회는 구별해야 합니다.
이 조건은 cache type과도 관련됩니다. 초기화 주석은 높은 physical hole까지 무조건 덮으면 fixed·variable MTRR만으로 적절한 UC 속성을 표현하기 어려울 수 있음을 지적합니다. firmware memory map의 정렬과 hole 때문에 2 MiB·1 GiB 대신 4 KiB mapping이 필요해질 수도 있습니다. memblock의 memory range와 free allocation range도 같지 않습니다. memory에 포함된 reserved RAM을 mapping하는 일과 그 RAM을 table allocation에 자유롭게 소비하는 일은 서로 다른 판단입니다.
대상 range가 아직 사용하지 않은 brk table buffer와 겹치면 can_use_brk_pgt를 제한하고 memblock 공급으로 바꿉니다. 지금 넓히는 mapping과 그 mapping을 작성할 table의 저장소가 서로 위험한 방식으로 겹치지 않도록 하는 조건입니다. 처리 뒤에는 flag를 되돌립니다.
구체적으로 비교하는 buffer 구간은 [pgt_buf_end, pgt_buf_top)을 byte 주소로 바꾼 아직 사용하지 않은 부분입니다. 두 구간의 큰 start가 작은 end 이상이면 겹치지 않으며 brk 사용을 허용합니다. 이것은 buffer가 이미 소진됐는지만 검사하는 식이 아닙니다. clamp는 양 끝 숫자를 제한하지만 실제 mapping은 여전히 end-exclusive이므로 start >= end인 결과를 건너뛰는 검사도 빠질 수 없습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:571-597
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete function: init_range_memory_mapping
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
static unsigned long __init init_range_memory_mapping(
unsigned long r_start,
unsigned long r_end)
{
unsigned long start_pfn, end_pfn;
unsigned long mapped_ram_size = 0;
int i;
for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, NULL) {
u64 start = clamp_val(PFN_PHYS(start_pfn), r_start, r_end);
u64 end = clamp_val(PFN_PHYS(end_pfn), r_start, r_end);
if (start >= end)
continue;
/*
* if it is overlapping with brk pgt, we need to
* alloc pgt buf from memblock instead.
*/
can_use_brk_pgt = max(start, (u64)pgt_buf_end<<PAGE_SHIFT) >=
min(end, (u64)pgt_buf_top<<PAGE_SHIFT);
init_memory_mapping(start, end, PAGE_KERNEL);
mapped_ram_size += end - start;
can_use_brk_pgt = true;
}
return mapped_ram_size;
}
위 block은 v6.18의 전체 init_range_memory_mapping()입니다. 각 RAM 조각을 처리한 뒤 can_use_brk_pgt = true로 복원하고 end - start를 byte 수로 누적합니다. 요청 상한이나 마지막 PFN을 반환하는 함수가 아닙니다.
전체 range 처리를 마친 init_mem_mapping()은 준비한 root를 CR3에 싣고 TLB를 flush한 뒤 hypervisor 초기화 hook과 early_memtest()를 이어 갑니다. table 메모리에 값을 썼다는 사실만으로 현재 CPU가 새 translation을 사용한다는 설명을 끝내지 않는 이유입니다. 이 초기화 순서는 runtime의 일반 mapping 변경 API를 아무 lock 없이 호출해도 된다는 허가와는 다릅니다.
마지막으로 v6.18 init_mem_mapping() 전체를 보면 32-bit 예외도 남김없이 보입니다. 64-bit는 max_pfn까지 처리하지만 32-bit의 이 단계 상한은 max_low_pfn입니다. mapping 뒤에도 64-bit는 필요하면 max_low_pfn을 갱신하고, 32-bit는 early_ioremap_page_table_range_init()을 호출합니다. 이를 highmem까지 전부 같은 direct-map 방식으로 연결한다는 뜻으로 확대하지 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Linux v6.18, arch/x86/mm/init.c:759-814
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/mm/init.c
* Complete function: init_mem_mapping
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
* Source has no SPDX header; license follows kernel COPYING.
*/
void __init init_mem_mapping(void)
{
unsigned long end;
pti_check_boottime_disable();
probe_page_size_mask();
setup_pcid();
#ifdef CONFIG_X86_64
end = max_pfn << PAGE_SHIFT;
#else
end = max_low_pfn << PAGE_SHIFT;
#endif
/* the ISA range is always mapped regardless of memory holes */
init_memory_mapping(0, ISA_END_ADDRESS, PAGE_KERNEL);
/* Init the trampoline, possibly with KASLR memory offset */
init_trampoline();
/*
* If the allocation is in bottom-up direction, we setup direct mapping
* in bottom-up, otherwise we setup direct mapping in top-down.
*/
if (memblock_bottom_up()) {
unsigned long kernel_end = __pa_symbol(_end);
/*
* we need two separate calls here. This is because we want to
* allocate page tables above the kernel. So we first map
* [kernel_end, end) to make memory above the kernel be mapped
* as soon as possible. And then use page tables allocated above
* the kernel to map [ISA_END_ADDRESS, kernel_end).
*/
memory_map_bottom_up(kernel_end, end);
memory_map_bottom_up(ISA_END_ADDRESS, kernel_end);
} else {
memory_map_top_down(ISA_END_ADDRESS, end);
}
#ifdef CONFIG_X86_64
if (max_pfn > max_low_pfn) {
/* can we preserve max_low_pfn ?*/
max_low_pfn = max_pfn;
}
#else
early_ioremap_page_table_range_init();
#endif
load_cr3(swapper_pg_dir);
__flush_tlb_all();
x86_init.hyper.init_mem_mapping();
early_memtest(0, max_pfn_mapped << PAGE_SHIFT);
}
이번 글의 init.c block들은 공식 v6.18의 전체 함수 또는 명시한 전체 macro 호출입니다. 해당 source에는 개별 SPDX가 없어 COPYING과 kernel license 규칙에 따른 GPL-2.0-only로 기록했습니다. 출처·범위·hash, license text를 제공합니다. 책의 정확한 upstream tag는 확인하지 않았으며 이 kernel 내부 code는 실행하지 않았습니다.
이 과정을 보면 direct map은 단순한 주소 덧셈만으로 생기지 않습니다. 주소 변환식이 실제로 성립하도록 table을 만들고, 그 table의 저장 공간을 먼저 접근 가능하게 해야 합니다. 초기화의 방향과 step 크기는 모두 그 공급 조건을 유지하기 위한 선택입니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 3 §3.3.1~§3.3.2.
- Linux v6.18 arch/x86/mm/init.c: early_alloc_pgt_buf(), probe_page_size_mask(), memory_map_bottom_up(), memory_map_top_down(), init_range_memory_mapping().
- Linux v6.18 mm/init-mm.c: init_mm과 root.
- Linux v6.18 arch/x86/mm/init_64.c: 하위 table 초기화.