kernel pointer에서 PAGE_OFFSET을 빼면 언제나 physical address를 얻을 수 있을까요? direct map의 유효한 주소라면 그 관계를 사용할 수 있지만, vmalloc이나 kernel image의 주소까지 같은 식으로 처리하면 잘못됩니다. kernel 내부에도 서로 다른 목적의 주소 영역이 있습니다.
x86-64의 주소 배치를 읽을 때 중요한 것은 긴 hexadecimal 표를 외우는 일이 아닙니다. 지금의 pointer가 어느 영역에 속하고, 어떤 변환 API가 그 영역을 입력으로 받는지 판단하는 것입니다. 여기서는 upstream Linux v6.18의 layout와 변환 helper를 기준으로 설명합니다.
canonical 공간의 두 끝
LAM 같은 tagging 예외를 제외한 4-level 주소에서 canonical 공간은 아래쪽 128 TiB와 위쪽 128 TiB로 나뉩니다. 가운데는 단순히 아직 allocation하지 않은 hole이 아니라 일반 canonical 주소로 쓸 수 없는 구간입니다. lower half는 process별 user address space에, upper half는 kernel 영역에 사용합니다.
5-level은 의미 있는 virtual bit가 57개이므로 각 canonical half가 크게 확장됩니다. 그렇다고 user가 lower half의 모든 byte를 자유롭게 mapping할 수 있다는 뜻은 아닙니다. architecture와 kernel은 끝의 guard, 최소 주소와 API별 제한 등을 적용합니다.
용량으로는 4-level의 양쪽 half를 합쳐 256 TiB, 5-level에서는 128 PiB이며 한쪽은 각각 128 TiB와 64 PiB입니다. 이때 48·57 bit는 부호 확장 전의 virtual 형식 폭입니다. kernel half가 커졌다는 사실에서 physical RAM 폭을 역산하거나, direct map은 수학적으로 반드시 전체의 4분의 1이어야 한다고 결론 내릴 수는 없습니다. 다른 kernel 영역을 함께 배치하기 위한 Linux의 layout 선택과 hardware address 폭은 별개의 제약입니다.
unsigned 값으로 보면 kernel 주소는 매우 큰 양수이고 signed 표시로는 주소 공간 끝에서 거꾸로 센 음수처럼 표현할 수 있습니다. 이 표기 차이는 서로 다른 memory 종류를 뜻하지 않습니다. 다만 부호를 보고 일반 pointer의 유효성을 판정하거나 임의 산술을 허용해서는 안 됩니다.
kernel 안에서도 주소 영역이 다릅니다
direct map은 physical memory를 kernel이 일정 offset 관계로 접근하도록 만든 영역입니다. vmalloc/ioremap은 별도의 virtual range를 확보하여 physical page나 device range를 연결합니다. vmemmap은 data page 자체가 아니라 struct page metadata를 접근하는 주소 영역입니다.
kernel image의 text·data, module mapping과 fixmap도 direct map과 다른 위치에 있습니다. cpu entry area, PTI의 LDT remap, KASAN shadow, EFI runtime 등의 범위는 각 기능을 위한 계약입니다. 예를 들어 KASAN shadow 주소를 일반 allocation의 data pointer로 해석할 수는 없습니다.
정적 4-level layout에서 direct map의 기준 위치는 0xffff888000000000, vmalloc은 0xffffc90000000000, vmemmap은 0xffffea0000000000입니다. 이 숫자는 특정 실행의 측정값이 아닙니다. CONFIG_RANDOMIZE_MEMORY와 KASLR이 적용되면 region 순서를 유지하면서 base가 달라질 수 있습니다.
direct map을 위한 주소 공간은 4-level에서 64 TiB, 5-level의 문서상 reservation은 32 PiB입니다. 예약 가능한 virtual 크기와 실제 physical 주소 지원 범위는 다릅니다. vmalloc 영역도 4-level의 32 TiB와 5-level의 12.5 PiB처럼 달라집니다. kernel 주소 전체를 RAM data의 일대일 복제 공간으로 보면 다른 영역의 자리를 설명할 수 없습니다.
큰 영역의 관계를 비교하면 5-level의 기본 direct-map base는 0xff11000000000000, vmalloc base는 0xffa0000000000000, vmemmap base는 0xffd4000000000000입니다. vmemmap reservation은 4-level의 1 TiB에서 5-level의 0.5 PiB로 커집니다. KASAN shadow도 기본 문서에서 각각 16 TiB와 약 8 PiB를 차지합니다. 이것은 해당 기능이 모두 켜져 실제 RAM을 그만큼 소비한다는 뜻이 아닙니다. KMSAN 등 configuration에 따라 vmalloc 안의 실제 사용 가능 범위가 더 나뉘는 조건도 있으므로, 정적 전체 영역과 allocator의 현재 범위를 혼동해서는 안 됩니다.
kernel half의 시작 부분도 바로 RAM이 아닙니다. 4-level에는 8 TiB, 5-level에는 4 PiB의 guard·hypervisor reservation이 있고 그 뒤 PTI LDT remap reservation이 각각 0.5 TiB·0.25 PiB로 놓입니다. 반대로 매우 높은 쪽의 cpu entry area와 ESPFIX stack reservation은 각각 0.5 TiB이며 큰 영역들이 5-level로 확장돼도 같은 높은 위치에 남습니다. 이 사이의 unused hole은 allocator가 임의로 써도 되는 빈 RAM이 아니라 layout에서 다른 목적과 간격을 위해 남긴 virtual 공간입니다.
위쪽 끝에서는 기능과 reservation을 더 세밀하게 구별해야 합니다. EFI의 64 GiB window는 별도 efi_pgd에 있으며 일반 kernel root에 항상 연결된 범위가 아니라 EFI runtime 호출 때 사용하는 mapping입니다. kernel image는 __START_KERNEL_map 기준 영역, module은 그 위의 별도 영역을 사용하며 KASLR과 image 크기 설정이 둘의 경계를 바꿉니다. legacy vsyscall ABI의 실제 page는 0xffffffffff600000에서 시작하는 4 KiB입니다. 그 주소에서 높은 쪽 hole 전까지의 넓은 간격 전체를 실행 가능한 vsyscall memory로 읽으면 안 됩니다. fixmap도 근처의 별도 목적 영역입니다. 이처럼 공식 layout 문서의 영역 크기, 실제 mapping과 접근 문맥을 나누어 읽어야 주소에 기능을 올바르게 연결할 수 있습니다.
kernel image와 module의 경계는 특히 build 조건을 확인해야 합니다. v6.18의 page_64_types.h는 CONFIG_RANDOMIZE_BASE가 있으면 KERNEL_IMAGE_SIZE를 1 GiB, 없으면 512 MiB로 정의합니다. 이는 binary가 그 크기만큼 RAM을 소비한다는 뜻이 아니라 image 배치를 허용하는 virtual 상한입니다. MODULES_VADDR는 __START_KERNEL_map + KERNEL_IMAGE_SIZE이므로 각각 0xffffffffc0000000 또는 0xffffffffa0000000이 됩니다. 일반적인 MODULES_END는 0xffffffffff000000이고 debug configuration에서는 더 낮아질 수 있으므로 module의 실제 window를 단순히 1 GiB·1.5 GiB의 꽉 찬 영역으로 가정해서도 안 됩니다.
direct-map 변환의 입력 계약
유효하게 direct-mapped된 physical address에 PAGE_OFFSET을 더하면 대응 kernel virtual address가 됩니다. x86의 __va()와 phys_to_virt()는 이 관계를 이용하는 계열입니다. PAGE_OFFSET도 runtime base와 연결될 수 있으므로 정적 표의 숫자를 모든 build에 박아 넣어서는 안 됩니다.
역방향 __pa()·virt_to_phys()도 generic page-table walker가 아닙니다. 입력 주소가 helper가 허용하는 mapping 영역에 있다는 전제 아래 산술로 계산합니다. vmalloc pointer를 넣었다고 연결된 PTE를 찾아 physical page를 돌려주지 않습니다.
같은 이유로 user pointer도 넣을 수 없습니다. user address는 현재 mm의 page table과 permission, fault와 reference lifetime을 따라가야 합니다. kernel에서 그 숫자를 알고 있다는 사실이 direct-map pointer로 바꾸어 사용해도 된다는 허가는 아닙니다.
이 입력 계약을 v6.18 page.h의 전체 macro 정의와 비교해 보겠습니다. 두 block은 서로 떨어진 source 위치의 정의이며 각각의 #ifndef guard도 보존했습니다. __va()에는 PFN 유효성 검사나 page-table 조회가 없습니다. __pa()도 주소를 정수로 바꾸어 architecture helper에 넘길 뿐입니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Linux v6.18, arch/x86/include/asm/page.h:40-42
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/include/asm/page.h
* Complete macro_block: __pa
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
#ifndef __pa
#define __pa(x) __phys_addr((unsigned long)(x))
#endif
// SPDX-License-Identifier: GPL-2.0
/*
* Linux v6.18, arch/x86/include/asm/page.h:57-59
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/include/asm/page.h
* Complete macro_block: __va
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
#ifndef __va
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
#endif

kernel image를 구별하는 산술
__phys_addr_nodebug()는 direct map과 높은 주소의 kernel image mapping을 구별해 처리합니다. 먼저 x - __START_KERNEL_map을 계산하고 unsigned 관계를 이용해 어느 쪽의 offset을 적용할지 선택합니다. 이를 단순한 signed 음수·양수 검사로 설명하면 실제 C 산술과 다릅니다.
kernel image 쪽이면 image 기준 offset에 phys_base를 더합니다. direct-map 쪽이면 결과적으로 PAGE_OFFSET을 뺀 관계를 얻습니다. phys_base는 kernel image의 실제 physical 배치와 관련되므로 image relocation을 무시한 고정 기준으로 대신해서는 안 됩니다.
이때 분기는 x > y라는 unsigned 비교입니다. 허용된 image 주소에서는 참이 되어 y + phys_base를 고르고, 더 낮은 direct-map 주소에서는 뺄셈의 wrap 관계를 이용해 y + (__START_KERNEL_map - PAGE_OFFSET)를 고릅니다. 후자를 정리하면 x - PAGE_OFFSET입니다. unsigned long인 y를 음수인지 검사하는 식으로 바꾸는 것은 이 carry 관계를 설명하지 못합니다.
이 최적화된 분기 자체가 입력 검증을 제공하는 것은 아닙니다. nodebug helper는 vmalloc, module, 존재하지 않는 direct-map hole 같은 임의의 kernel 주소를 받아 성공 여부까지 알려 주는 함수가 아닙니다. CONFIG_DEBUG_VIRTUAL의 다른 경로가 존재한다는 사실도 모든 production 호출에서 범위가 검증된다는 보장으로 확대할 수 없습니다.
아래는 v6.18 page_64.h의 전체 __phys_addr_nodebug()입니다. 같은 header는 CONFIG_DEBUG_VIRTUAL이면 외부 __phys_addr() 선언을 사용하고, 그렇지 않으면 __phys_addr를 이 nodebug 함수에 연결합니다. 따라서 __pa()를 보았다고 모든 build에서 바로 이 body만 실행한다고 단정하지 않습니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Linux v6.18, arch/x86/include/asm/page_64.h:22-30
* Source: https://github.com/torvalds/linux/blob/v6.18/arch/x86/include/asm/page_64.h
* Complete function: __phys_addr_nodebug
* Source body unchanged. Kernel dependencies are not bundled.
* License text: GPL-2.0.txt
*/
static __always_inline unsigned long __phys_addr_nodebug(unsigned long x)
{
unsigned long y = x - __START_KERNEL_map;
/* use the carry flag to determine if x was < __START_KERNEL_map */
x = y + ((x > y) ? phys_base : (__START_KERNEL_map - PAGE_OFFSET));
return x;
}
세 block은 공식 v6.18의 GPL-2.0 code이며 source 범위·hash 기록과 license text를 제공합니다. 책의 정확한 tag를 복원한 것이 아니며, kernel 내부 의존성이 있어 독립 실행하지 않았습니다.
변환을 읽는 안전한 방법은 먼저 주소의 출처를 추적하는 것입니다. slab에서 받은 일반 RAM allocation인지, vmalloc에서 연속 VA를 마련한 것인지, kernel symbol인지에 따라 맞는 helper와 backing 획득 방식이 달라집니다. 계산 결과가 그럴듯한 작은 정수라는 사실만으로 옳은 physical address가 되지는 않습니다.
빈 PFN과 reserved 범위
max_pfn은 physical memory를 다루는 상한 정보이지 그 아래 모든 PFN이 사용 가능한 RAM이라는 증명은 아닙니다. firmware의 memory map에는 hole과 reserved 영역이 있습니다. page metadata의 유효성, direct-map translation의 존재, allocator가 사용할 수 있는 RAM인지 여부도 서로 다릅니다.
초기 mapper는 일반 RAM 범위를 따라 hole을 제외하는 경로를 사용하지만 ISA 같은 architecture 특수 범위도 먼저 처리합니다. 큰 mapping이나 특수 배치에 따라 일부 physical hole이 translation 범위에 들어갈 수도 있습니다. 그러므로 direct-map 영역에 숫자가 들어간다는 사실을 일반 RAM 사용 허가로 바꾸어서는 안 됩니다.
device memory는 cache attribute, access width와 driver 수명 계약까지 필요합니다. CPU physical address와 device가 보는 DMA address도 같은 개념이 아닙니다. direct mapping은 kernel이 일반 memory를 편리하게 다루는 장치이지 모든 address domain을 한 숫자로 통합하는 API가 아닙니다.
이 관점에서 주소 layout는 암기용 표보다 type system에 가깝습니다. C type이 모두 pointer라고 해도 그 값의 출처가 사용할 수 있는 연산을 제한합니다. 어느 영역의 어떤 객체를 가리키는지 먼저 확인하면 값 변환과 memory 소유권의 혼동을 함께 줄일 수 있습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 3 §3.2~§3.3.
- Linux v6.18 x86-64 memory map: region과 KASLR 조건.
- Linux v6.18 page.h, page_64.h: 변환 helper와 unsigned 산술.
- Linux v6.18 pgtable_64_types.h: region별 상수와 runtime base.