wandres.dev
VMALLOC Y DIRECCIONES DEL KERNEL · el espacio virtual del kernel

El mapa del espacio de direcciones del kernel en x86-64

El plano de direcciones virtuales del kernel en x86-64: la mitad canónica alta, el direct map de toda la RAM física, la zona vmalloc/ioremap, el vmemmap y la zona de módulos. Qué es una dirección de kernel, por qué __va y __pa solo valen para el direct map, y el papel de KASLR.

⏱ 17 min

Un puntero del kernel no es solo un número: sus bits altos declaran a qué región pertenece y, con ello, cómo debe traducirse. En x86-64 el espacio virtual del kernel está partido en zonas con reglas distintas —el direct map, vmalloc, el vmemmap, los módulos— y confundir a cuál pertenece una dirección es confundir cómo la resolverá el hardware. Este es el plano que todo lo demás presupone.

🎯 Al terminar esta lección sabrás
  • Situar la mitad canónica alta y las cuatro zonas grandes del kernel en x86-64.
  • Entender el direct map como identidad lineal y por qué __va/__pa solo valen ahí.
  • Distinguir una dirección de direct map de una de vmalloc, módulo o vmemmap.
  • Conocer el papel de KASLR y del paginado de 5 niveles en este plano.

La mitad alta y el agujero no canónico

x86-64 con paginado de 4 niveles usa direcciones virtuales de 48 bits significativos, obligadas a ser canónicas: los bits 63..47 deben ser todos iguales. Eso parte el espacio en dos mitades separadas por un abismo enorme e ilegal. La mitad baja, con el bit 63 a cero, es el espacio de usuario, distinto en cada proceso. La mitad alta, con el bit 63 a uno, es el kernel, compartida por todos los procesos. Por eso una dirección de kernel siempre empieza por 0xffff...: es la marca de la mitad alta canónica.

0000000000000000 - 00007fffffffffff  128 TB  espacio de usuario (por proceso)
0000800000000000 - ffff7fffffffffff  ~16M TB  agujero no canonico (ilegal)
ffff800000000000 - ffffffffffffffff  128 TB  espacio del kernel (compartido)

Las cuatro zonas del kernel

Dentro de la mitad alta, el kernel de x86-64 dispone las regiones así (valores canónicos sin KASLR, de Documentation/arch/x86/x86_64/mm.rst):

ffff888000000000  64 TB   direct map: toda la RAM fisica (PAGE_OFFSET)
ffffc90000000000  32 TB   vmalloc / ioremap (VMALLOC_START .. VMALLOC_END)
ffffea0000000000   1 TB   vmemmap: el array de struct page
ffffffff80000000 512 MB   texto del kernel (__START_KERNEL_map, fis 0)
ffffffffa0000000  ~1.5 GB modulos (MODULES_VADDR .. MODULES_END)

Cada zona tiene un tamaño elegido con holgura deliberada. El direct map reserva 64 TB porque debe poder mapear toda la RAM física imaginable de la máquina; la zona vmalloc, 32 TB, para no agotarse jamás en 64-bit; el vmemmap, 1 TB, suficiente para un descriptor struct page por cada marco de esos 64 TB. Y entre zonas hay huecos de guarda sin mapear: un acceso que se desborde de una región cae en espacio no mapeado y falla de inmediato, en lugar de corromper a la vecina.

flowchart TB
U[usuario 0000..0000 hasta 00007fff.. 128 TB] --> H[agujero no canonico]
H --> D[direct map ffff8880.. 64 TB toda la RAM fisica]
D --> V[vmalloc / ioremap ffffc900.. 32 TB]
V --> M[vmemmap ffffea00.. 1 TB array de struct page]
M --> T[texto del kernel ffffffff80000000 512 MB]
T --> MO[modulos ffffffffa0000000 1.5 GB]

El direct map: la identidad lineal

El direct map es la pieza central. Es un mapeo uno a uno de toda la RAM física a un rango virtual continuo que empieza en PAGE_OFFSET. La página física p está siempre visible en la dirección virtual PAGE_OFFSET + p, sin excepción y sin coste de traducción a computar: por eso convertir entre ambas es una simple suma o resta.

/* arch/x86/include/asm/page.h  (esencia) */
#define __va(x)  ((void *)((unsigned long)(x) + PAGE_OFFSET))
#define __pa(x)  __phys_addr((unsigned long)(x))   /* resta PAGE_OFFSET */

kmalloc (nivel 22.1) devuelve siempre direcciones de esta zona, y de ahí que su memoria sea físicamente contigua y su virt_to_phys trivial. Además, el direct map se cubre con páginas enormes de 2 MB y 1 GB, no de 4 KB: así toda la RAM cabe en poquísimas entradas de TLB. En x86-64 toda la RAM está en el direct map de forma permanente; no existe la «memoria alta» que atormentaba a 32-bit (nivel 22.5).

Conviene no confundir dos mapeos que apuntan a la misma física: el texto del kernel está mapeado dos veces, una en __START_KERNEL_map —donde se enlazó y desde donde ejecuta— y otra en el direct map, como RAM ordinaria. Por eso __pa_symbol, y no __pa, es la macro correcta para traducir la dirección de un símbolo del kernel: aplica el desplazamiento del mapeo de texto, distinto del de la RAM directa. Es un recordatorio literal de que una misma página física puede tener más de una dirección virtual, cada una con su propia regla de traducción.

⚠️
__va y __pa solo valen en el direct map

__pa y __va son una resta y una suma de PAGE_OFFSET. Funcionan únicamente para direcciones del direct map. Aplicarlos a un puntero de vmalloc, de ioremap, de la zona de módulos o al texto del kernel produce basura silenciosa, porque esas zonas no guardan ninguna relación lineal con la física. Para traducir una dirección vmalloc usa vmalloc_to_page; para el caso general y lento, slow_virt_to_phys, que recorre de verdad las tablas de páginas. Confundir la zona es el bug de memoria más difícil de ver porque el puntero «parece» válido.

vmemmap, módulos y qué es una dirección de kernel

El vmemmap es un truco de elegancia: coloca el array global de struct page —un descriptor por cada marco físico— en un rango virtual propio, de modo que pfn_to_page y page_to_pfn sean aritmética de índices en lugar de búsquedas. La zona de módulos vive pegada al texto del kernel, en los últimos 1.5 GB, y no por capricho: las llamadas entre el núcleo y un módulo usan saltos relativos de 32 bits, que solo alcanzan si ambos están dentro de la misma ventana de 2 GB. Por eso los módulos no pueden ir a la zona vmalloc general pese a reservarse con maquinaria parecida.

Con esto, «dirección de kernel» deja de ser una noción vaga. Es una dirección de la mitad alta canónica, y su zona determina su régimen de traducción: direct map se traduce por desplazamiento; vmalloc, ioremap, módulos y vmemmap se traducen recorriendo tablas de páginas. virt_addr_valid(k) responde a la pregunta concreta «¿es k una dirección de direct map respaldada por RAM?», que es justo la precondición de __pa.

Esta clasificación es consultable en vivo. /proc/iomem muestra el reparto físico —dónde está la RAM y dónde los registros de dispositivo—, y /proc/vmallocinfo lista cada reserva de la zona vmalloc con su llamante. Si compilaste con CONFIG_PTDUMP_DEBUGFS, el volcado de tablas de páginas expone el mapeo virtual completo con el tamaño de página de cada tramo:

sudo cat /sys/kernel/debug/kernel_page_tables | head -40

Ahí verás el direct map cubierto con entradas de 2M y 1G —las páginas enormes que ahorran TLB— frente a la zona vmalloc, salpicada de entradas de 4K porque cose páginas sueltas. La geometría de la tabla de páginas delata qué asignador produjo cada dirección. Desde código, la pertenencia a una zona se consulta con predicados dedicados, no comparando direcciones a mano:

if (virt_addr_valid(p))        /* direct map, respaldado por RAM: __pa vale */
	phys = __pa(p);
else if (is_vmalloc_addr(p))   /* cae en VMALLOC_START .. VMALLOC_END */
	page = vmalloc_to_page(p);
💡
Lee el prefijo, deduce la regla

Un truco de campo: mira los primeros dígitos hex de un puntero de kernel y sabrás su zona. ffff8880… es direct map (RAM directa, __pa válido); ffffc9… es vmalloc o ioremap (recorre tablas); ffffea… es un struct page del vmemmap; ffffffff8… es texto o módulo. Con KASLR las bases cambian, pero dentro de un mismo arranque los prefijos siguen particionando el espacio, y /proc/kallsyms te da las bases reales de esa sesión.

ℹ️
KASLR y el paginado de 5 niveles

Los valores de arriba son los canónicos, pero en un kernel real no los verás fijos: KASLR aleatoriza en el arranque las bases page_offset_base, vmalloc_base y vmemmap_base, desplazando las tres zonas para dificultar los exploits. Las macros (PAGE_OFFSET, VMALLOC_START…) pasan a ser variables leídas de esas bases. Y en CPUs con LA57 el kernel puede activar paginado de 5 niveles: las direcciones pasan a 57 bits significativos, el espacio de usuario y el direct map crecen a 128 PB y las bases se recolocan, pero la estructura conceptual —mitad alta, direct map, vmalloc, vmemmap, módulos— es idéntica.

El espacio de direcciones es una ontología, no un rango

La idea que corona el nivel: en el kernel, el tipo de una dirección está codificado en sus bits, y ese tipo dicta la física de cómo se resuelve. No hay un único «puntero a memoria»; hay punteros de direct map, de vmalloc, de ioremap, de módulo, de vmemmap, y cada especie obedece leyes de traducción distintas aunque todas quepan en el mismo void *. Un número que empieza por 0xffff8880 es RAM directa y se traduce restando; uno que empieza por 0xffffc900 es un rango cosido a mano que solo las tablas de páginas saben resolver. Saber a qué zona pertenece una dirección es saber qué hará el hardware con ella, y por eso los bugs más sutiles del kernel —pasar una dirección vmalloc a __pa, hacer DMA sobre memoria de módulo, tratar ioremap como RAM— no son errores de cálculo sino errores de categoría: usar un habitante de una zona bajo las reglas de otra. El plano de direcciones no es un mapa de dónde están las cosas; es una taxonomía de qué son y cómo se comportan.

⚔️ Lee el plano en tu máquina
  1. Compara una dirección de kmalloc, una de vmalloc y &jiffies (símbolo del kernel): fíjate en sus prefijos y deduce la zona de cada una.
  2. Aplica virt_addr_valid a las tres y explica por qué solo la de kmalloc da verdadero.
  3. Lee /proc/vmallocinfo y confirma que sus direcciones caen dentro del rango VMALLOC_START .. VMALLOC_END.
  4. Explica por qué los módulos deben cargarse en los últimos 1.5 GB y no en la zona vmalloc general.
  5. Averigua si tu kernel arrancó con KASLR y con paginado de 4 o 5 niveles, y di qué bases se habrían aleatorizado.