1. Dos mundos: el que ve el programa y el que ve el hardware
Cuando escribes un programa en C, el compilador genera instrucciones que usan direcciones. Esas direcciones forman el espacio de direcciones del proceso: el conjunto de todas las direcciones que el programa puede nombrar. Pero la RAM física del equipo es otro espacio distinto, con su propio conjunto de direcciones. El programa nunca ve la RAM directamente: ve su propio espacio virtual, y el hardware se encarga de traducir cada acceso.
Dirección física es la posición real en la RAM (o en un dispositivo) que identifica un byte concreto del hardware. Dirección virtual es la posición que usa el programa y que el procesador traduce, en cada acceso, a una dirección física.
La distinción es la base de toda la gestión de memoria. Todo lo que estudiarás después (paginación, tablas de páginas, TLB, memoria virtual, mmap) son mecanismos para realizar esa traducción de forma rápida y segura.
2. Dirección física: qué es
Una dirección física es un número que identifica un byte de la memoria física del equipo. Si el equipo tiene 16 GiB de RAM, las direcciones físicas van de 0 a 234 − 1 (unas 16 mil millones). El bus de memoria del procesador lleva ese número a los módulos de RAM, que devuelven el byte correspondiente.
- La dirección física la genera el hardware, no el programa. El programa no puede nombrarla directamente.
- Varias direcciones físicas pueden corresponder a la misma RAM (aliasing) o a dispositivos (memoria mapeada de E/S), pero en un PC normal cada byte de RAM tiene una dirección física única.
- El tamaño del espacio físico lo limita la cantidad de RAM instalada y la anchura del bus de direcciones del procesador.
En un sistema sin gestión de memoria (microcontrolador, arranque temprano), el programa usa directamente direcciones físicas: la variable x está en la posición 0x2000 y siempre en la 0x2000. Ese modelo es simple, pero no permite ejecutar varios programas ni protegerlos.
3. Dirección virtual: qué es
Una dirección virtual es el número que usa el programa para referirse a una posición. El programa cree que la memoria empieza en 0 y es contigua, enorme y exclusiva. Esa creencia es la ilusión que mantiene el sistema operativo: en realidad, las posiciones que el programa usa se reparten por la RAM física de forma arbitraria, y el hardware traduce cada acceso.
- La dirección virtual la genera el compilador y el enlazador, según la convención del sistema (por ejemplo, código en direcciones bajas, pila en las altas).
- Cada proceso tiene su propio espacio de direcciones virtuales, independiente del de los demás.
- Dos procesos pueden usar la misma dirección virtual y que apunte a marcos físicos distintos (o a ninguno).
- El espacio virtual puede ser mayor que la RAM física: ahí está la base de la memoria virtual (tema 28).
Por eso, cuando un programa imprime 0x7ffd5c3a1f48, ese número es una dirección virtual. No puedes ir con un analizador lógico a la RAM y buscar ese número: en la RAM ese byte está en otra posición.
4. Por qué se inventó la dirección virtual
La dirección virtual resuelve los tres problemas que aparecieron al poner varios programas en memoria (tema 1):
| Problema | Cómo lo resuelve la dirección virtual |
|---|---|
| Reubicación | El programa se compila para un espacio virtual fijo. El sistema lo carga donde quiera en la RAM y traduce las direcciones; no hace falta recompilar. |
| Protección | Cada proceso solo puede nombrar su propio espacio virtual. El hardware rechaza cualquier acceso fuera de él. |
| Capacidad | El espacio virtual puede ser mayor que la RAM. Las partes que no se usan no ocupan memoria física. |
Además, la dirección virtual permite compartir memoria entre procesos (varias virtuales apuntando al mismo marco físico) y mapear archivos como si fueran memoria (mmap). Todo ello sin que el programa se entere.
5. Quién traduce: la MMU y el sistema operativo
La traducción no la hace el sistema operativo en cada acceso: sería demasiado lento. La hace una unidad del procesador llamada MMU (Memory Management Unit), que traduce y comprueba cada dirección al vuelo, usando unas tablas que mantiene el sistema operativo.
- El programa genera una dirección virtual al acceder a una variable o instrucción.
- La MMU la recibe antes de que salga al bus de memoria.
- La MMU consulta la tabla del proceso actual (tabla de páginas, en los sistemas modernos) para obtener el marco físico y los permisos.
- Si todo es válido, la MMU pone la dirección física en el bus y la RAM responde.
- Si no (página ausente, permiso denegado), la MMU lanza una excepción y el sistema operativo toma el control.
El sistema operativo solo interviene en los casos excepcionales: construye las tablas al crear el proceso, las actualiza cuando hay fallos de página y las destruye al terminar. En el caso normal, la traducción es puramente hardware y cuesta unos ciclos.
6. Espacios de direcciones: virtual vs físico
Los dos espacios tienen tamaños distintos y se miden de forma distinta:
| Aspecto | Espacio virtual (por proceso) | Espacio físico (del equipo) |
|---|---|---|
| Tamaño típico | 248 o 264 bytes (256 TiB o 16 EiB en 64 bits) | La RAM instalada (8–64 GiB) más dispositivos |
| Quién lo define | La arquitectura del procesador y el SO | El hardware (RAM + bus) |
| Quién lo usa | El programa (compilador, enlazador) | La MMU y el núcleo |
| Contiguo | Sí, desde 0 hasta el máximo | Sí, pero con huecos (dispositivos, memoria reservada) |
| Compartido entre procesos | No (cada uno tiene el suyo) | Sí (la misma RAM para todos) |
En un sistema de 64 bits, el espacio virtual es enorme (256 TiB en x86-64 con 48 bits de dirección virtual), pero solo una pequeña parte está asignada a cada proceso. El espacio físico es mucho más pequeño (la RAM instalada), pero es el recurso compartido que todos los procesos usan a través de la traducción.
La relación entre ambos espacios es muchos a uno: muchas direcciones virtuales (de distintos procesos o del mismo) pueden traducirse a la misma dirección física (memoria compartida, código común), y una dirección virtual puede no tener dirección física asignada (página no presente).
7. Experimento: imprimir direcciones y leer /proc/self/maps
El programa siguiente imprime las direcciones de una función, una global, una local y un bloque dinámico, y luego muestra las primeras líneas de /proc/self/maps, que es el mapa del espacio de direcciones del proceso:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
static void funcion(void) { }
int global = 7;
int main(void)
{
int local = 42;
void *heap = malloc(100);
printf("función : %p\n", (void *)&funcion);
printf("global : %p\n", (void *)&global);
printf("local : %p\n", (void *)&local);
printf("heap : %p\n", heap);
printf("\n/proc/self/maps (primeras líneas):\n");
FILE *f = fopen("/proc/self/maps", "r");
char linea[256];
int n = 0;
while (fgets(linea, sizeof linea, f) && n < 8) { fputs(linea, stdout); n++; }
fclose(f);
free(heap);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o direcciones direcciones.c
$ ./direcciones
función : 0x55a1b3c01149
global : 0x55a1b3c04010
local : 0x7ffd8a3c1f2c
local : 0x7ffd8a3c1f2c
heap : 0x55a1b3c062a0
/proc/self/maps (primeras líneas):
55a1b3c01000-55a1b3c02000 r-xp 00000000 08:01 1310734 /home/ana/direcciones
55a1b3c02000-55a1b3c03000 r--p 00001000 08:01 1310734 /home/anna/direcciones
55a1b3c03000-55a1b3c04000 rw-p 00002000 08:01 1310734 /home/ana/direcciones
55a1b3c04000-55a1b3c05000 rw-p 00000000 00:00 0
55a1b3c06000-55a1b3c07000 rw-p 00000000 00:00 0 [heap]
55a1b3c06000-55a1b3c07000 rw-p 00000000 00:00 0 [heap]
7f3a2c000000-7f3a2c021000 rw-p 00000000 00:00 0
7f3a2c021000-7f3a30000000 ---p 00000000 00:00 0
Lo que imprime el programa son direcciones virtuales. Cada una cae dentro de una región del mapa: la función en la región ejecutable del binario, la global en la región de datos, la local en la pila (zona alta) y el heap en la región marcada [heap]. El mapa muestra rangos virtuales, permisos y el archivo o región de origen, pero nunca direcciones físicas: el espacio físico es invisible para el proceso.
8. Experimento: la misma virtual, marcos distintos
Para ver que la traducción es real, se puede leer /proc/PID/pagemap, que expone el marco físico asociado a cada página virtual. Requiere privilegios en sistemas modernos, pero el concepto es el siguiente: cada entrada de 64 bits contiene el número de marco físico (PFN) y un bit que indica si la página está presente en la RAM.
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
int main(void)
{
int x = 42;
unsigned long vaddr = (unsigned long)&x;
size_t page = vaddr / 4096;
size_t off = vaddr % 4096;
char path[64];
snprintf(path, sizeof path, "/proc/%d/pagemap", getpid());
int fd = open(path, O_RDONLY);
if (fd < 0) { perror("open pagemap"); return 1; }
uint64_t entrada;
if (pread(fd, &entrada, 8, page * 8) != 8) { perror("pread"); return 1; }
int presente = (entrada >> 63) & 1;
uint64_t pfn = entrada & ((1ULL << 55) - 1);
printf("virtual = 0x%lx\n", vaddr);
printf("página = %zu (desplazamiento 0x%zx)\n", page, off);
printf("presente = %d\n", presente);
printf("PFN = 0x%lx -> física = 0x%lx\n", pfn, pfn * 4096 + off);
close(fd);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o pagemap pagemap.c
$ ./pagemap
virtual = 0x7ffd8a3c1f2c
página = 140726919954 (desplazamiento 0xf2c)
PFN = 0x1a3f2
-> física = 0x1a3f2f2c
La dirección virtual 0x7ffd8a3c1f2c se traduce al marco físico 0x1a3f2, es decir, a la posición física 0x1a3f2f2c. Si dos procesos ejecutaran este programa, la misma dirección virtual apuntaría a PFN distintos: la traducción depende del proceso, porque cada uno tiene su propia tabla.
En kernels modernos, leer /proc/PID/pagemap requiere privilegios (CAP_SYS_ADMIN) o que el proceso sea el mismo. El núcleo oculta los PFN para dificultar ataques de canal lateral (tema de seguridad). En un contenedor o sin privilegios, el programa puede fallar con EPERM.
9. Ventajas de la virtualización de direcciones
Separar las direcciones virtuales de las físicas no es solo un truco: aporta propiedades que hacen posible el sistema moderno:
- Aislamiento. Un proceso no puede nombrar la memoria de otro, porque su espacio virtual no incluye esas posiciones.
- Reubicación sin recompilar. El mismo binario se carga en cualquier posición física; la tabla hace el resto.
- Espacio mayor que la RAM. El espacio virtual puede ser enorme; solo se asigna RAM a lo que se usa (paginación por demanda, tema 29).
- Compartición controlada. Varias virtuales pueden apuntar al mismo marco, compartiendo código o datos.
- Mapeo de archivos. Un archivo del disco puede aparecer como un rango de direcciones virtuales (mmap, tema 41).
- Protección por página. Cada entrada de la tabla lleva bits de permisos (lectura, escritura, ejecución), que la MMU comprueba en cada acceso.
10. Probar con un simulador
Este simulador traduce una dirección virtual a física usando una tabla de páginas editable. El espacio virtual tiene 16 páginas de 4096 bytes (0x0000–0xFFFF) y la RAM tiene 16 marcos. Edita los marcos en la tabla y observa cómo cambia la traducción. También puedes provocar un fallo marcando una página como no presente.
Simulador · traducción virtual → física
| Campo | Valor |
|---|---|
| Número de página virtual | |
| Desplazamiento | |
| Marco físico asignado | |
| Bit presente | |
| Dirección física resultante |
Tabla de páginas (edita el marco o desmarca "presente"):
11. Mirar la traducción en Linux
Linux expone el mapa virtual de un proceso y, con privilegios, la traducción a marcos físicos:
$ cat /proc/self/maps | head -20
55a1b3c01000-55a1b3c02000 r-xp 00000000 08:01 1310734 /home/ana/prog
55a1b3c02000-55a1b3c03000 r--p 00001000 08:01 1310734 /home/ana/prog
55a1b3c03000-55a1b3c04000 rw-p 00002000 08:01 1310734 /home/ana/prog
55a1b3c04000-55a1b3c05000 rw-p 00000000 00:00 0
55a1b3c06000-55a1b3c07000 rw-p 00000000 00:00 0 [heap]
7ffd8a3a3000-7ffd8a3c4000 rw-p 00000000 00:00 0 [stack]
7ffd8a3f6000-7ffd8a3f8000 r-xp 00000000 00:00 0 [vdso]
$ getconf PAGESIZE
4096
$ grep -E 'Virtual|Physical|Swap' /proc/meminfo | head
...
$ sudo cat /proc/$$/pagemap | head -c 64 | xxd
(salida binaria de 8 bytes por página)
/proc/PID/mapsmuestra el espacio virtual del proceso: rangos, permisos y origen. Es la herramienta principal para entender qué memoria usa un programa.getconf PAGESIZEda el tamaño de página (4096), que es la unidad de traducción./proc/PID/pagemapexpone la traducción virtual→física, pero está restringido por seguridad.pmap -x PIDresume el uso de memoria de un proceso con RSS y espacio virtual.
12. Resumen
- Una dirección física identifica un byte real de la RAM; una dirección virtual es la que usa el programa y traduce el hardware.
- El programa solo ve su espacio virtual; el espacio físico es invisible para él.
- La MMU traduce y comprueba cada acceso usando tablas que mantiene el sistema operativo; el núcleo solo interviene en los casos excepcionales.
- La dirección virtual resuelve la reubicación, la protección, la capacidad (espacio mayor que la RAM), la compartición y el mapeo de archivos.
- El espacio virtual es enorme (248 o 264) y por proceso; el físico es la RAM instalada y se comparte.
- En
direcciones.c, las direcciones impresas son virtuales y caen en regiones de/proc/self/maps. - En
pagemap.c, una virtual se traduce a un marco físico (PFN) concreto; la misma virtual en otro proceso daría otro PFN. - En Linux,
/proc/PID/maps,getconf PAGESIZEypmappermiten inspeccionar el espacio virtual.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué es una dirección física y quién la genera?
Es la posición real de un byte en la RAM (o en un dispositivo). La genera el hardware al acceder a la memoria; el programa no puede nombrarla directamente.
2. ¿Qué es una dirección virtual?
Es la posición que usa el programa para referirse a una zona de memoria. El procesador la traduce en cada acceso a una dirección física usando las tablas del proceso.
3. ¿Por qué un programa no puede leer la memoria de otro?
Porque cada proceso tiene su propio espacio de direcciones virtuales y la MMU rechaza cualquier acceso fuera de él. La dirección virtual del otro proceso no existe en el espacio del primero.
4. ¿Interviene el sistema operativo en cada acceso a memoria?
No. La traducción la hace la MMU en hardware. El núcleo solo interviene en los casos excepcionales (página ausente, permiso denegado, creación y destrucción de procesos).
5. ¿Qué muestra /proc/PID/maps?
El mapa del espacio de direcciones virtuales del proceso: rangos de direcciones, permisos, desplazamiento en el archivo de origen y el archivo o región (código, datos, heap, pila, mmap). No muestra direcciones físicas.
6. ¿Qué es un PFN?
El número de marco físico (page frame number): el índice del bloque de 4096 bytes en la RAM al que se traduce una página virtual. Se obtiene de /proc/PID/pagemap.
7. ¿Por qué el espacio virtual puede ser mayor que la RAM?
Porque no toda dirección virtual tiene un marco físico asignado. Solo se asigna RAM a las páginas que se usan (paginación por demanda), y el resto puede estar en disco o no existir.
8. ¿Qué ventaja da la dirección virtual para la compartición?
Permite que varias direcciones virtuales (de uno o varios procesos) apunten al mismo marco físico, compartiendo código o datos sin duplicarlos en la RAM.
Para practicar
- Compila y ejecuta
direcciones.cvarias veces. ¿Cambia la dirección del heap entre ejecuciones? ¿Y la de la pila? Relaciona con ASLR. - Modifica
direcciones.cpara imprimir también la dirección de una variable estática y de un bloque obtenido conmmap. ¿En qué región de/proc/self/mapscae cada una? - Ejecuta
pagemap.ccon privilegios (sudo) y sin ellos. ¿Qué error aparece sin privilegios? - Ejecuta dos instancias de
pagemap.cy compara los PFN de la variablex. ¿Son iguales o distintos? ¿Por qué? - Usa
pmap -x $$en tu shell y localiza las regiones de código, datos, heap y pila. Compara con/proc/self/maps. - Modifica el simulador para que una página mapee al mismo marco que otra. ¿Qué significa eso en términos de compartición?
- Investiga con
readelf -lun binario propio: ¿qué segmentos (LOAD) aparecen y a qué direcciones virtuales se cargan?