1. La tabla de segmentos
Cada proceso tiene su propia tabla de segmentos, mantenida por el sistema operativo y consultada por la MMU. La tabla es un array de entradas; el número de segmento (s) es el índice. Al cambiar de proceso, el SO carga el registro que apunta a la tabla de ese proceso.
La tabla puede estar en memoria (y la MMU la lee en cada acceso, con caché) o, en arquitecturas modernas, en registros dedicados.
2. Campos de cada entrada
| Campo | Significado |
|---|---|
| Base | Dirección física donde empieza el segmento en la RAM. |
| Límite | Tamaño del segmento (en bytes o palabras). |
| Permisos | Bits de lectura (R), escritura (W) y ejecución (X). |
| Presente | Si el segmento está en la RAM. |
| Privilegio (DPL) | Nivel de privilegio requerido para acceder (en x86). |
3. Protección por segmento
La MMU comprueba, en cada acceso, que:
- El segmento s sea válido (exista en la tabla).
- El desplazamiento d sea menor que el límite del segmento.
- El acceso (lectura/escritura/ejecución) esté permitido por los permisos del segmento.
- El nivel de privilegio del proceso sea suficiente.
Si alguna condición falla, se lanza una excepción de protección. El sistema operativo la atiende y, normalmente, envía SIGSEGV al proceso.
Esto permite, por ejemplo, que el segmento de código sea de solo lectura y ejecución (r-x), impidiendo que un programa modifique su propio código (W^X).
4. Compartición de código
La compartición es natural con segmentación: varios procesos pueden tener entradas en su tabla que apunten al mismo segmento físico (misma base y límite). Así, el código de libc se carga una sola vez en la RAM y se comparte entre todos los procesos que la usan.
- Los segmentos compartidos son de solo lectura (r-x), para evitar que un proceso modifique el código de otro.
- Los datos no se comparten por defecto (cada proceso tiene su propia copia).
- La compartición ahorra memoria: un sistema con 100 procesos que usan libc ahorra cientos de MiB.
5. El origen de la falla de segmentación
La "falla de segmentación" (segmentation fault, SIGSEGV) es la señal que el sistema envía a un proceso cuando intenta acceder a una dirección que no le pertenece o con permisos inadecuados. A pesar del nombre, no tiene nada que ver con la segmentación en el sentido clásico: es el mecanismo de protección de memoria funcionando.
El nombre histórico viene de los sistemas con segmentación, donde un acceso fuera de los límites de un segmento generaba una "falla de segmento". Hoy, con paginación, el mismo mecanismo (la MMU detecta el acceso inválido y lanza una excepción) se llama SIGSEGV por compatibilidad.
Causas típicas de SIGSEGV:
- Desreferenciar un puntero NULL.
- Acceder a memoria liberada (use-after-free).
- Desbordar un búfer (buffer overflow).
- Escribir en memoria de solo lectura (como .rodata).
- Acceder a una dirección no mapeada.
Sin protección, el programa leería o escribiría en silencio la memoria de otro. El sistema prefiere detener el proceso culpable en el acto, señalando dónde falló.
6. Ejemplo numérico
Tabla de segmentos de un proceso:
| Seg | Base | Límite | Permisos |
|---|---|---|---|
| 0 | 0x10000 | 0x3000 | r-x |
| 1 | 0x20000 | 0x2000 | rw- |
| 2 | 0x30000 | 0x1000 | rw- |
Accesos:
| Acceso | Resultado | Motivo |
|---|---|---|
| Leer seg 0, off 0x1000 | OK | Dentro del límite, permiso r |
| Escribir seg 0, off 0x1000 | SIGSEGV | Sin permiso w |
| Leer seg 1, off 0x2000 | SIGSEGV | off ≥ límite |
| Ejecutar seg 1, off 0x0000 | SIGSEGV | Sin permiso x |
| Leer seg 5, off 0x0000 | SIGSEGV | Segmento inexistente |
7. Experimento en C
El programa siguiente simula la protección por segmento y muestra qué accesos son válidos:
#include <stdio.h>
typedef struct { unsigned long base, limite; int r, w, x; } Segmento;
static int acceder(Segmento *t, int s, unsigned long off, char modo)
{
if (s < 0 || s >= 3) return 0;
if (off >= t[s].limite) return 0;
if (modo == 'r' && !t[s].r) return 0;
if (modo == 'w' && !t[s].w) return 0;
if (modo == 'x' && !t[s].x) return 0;
return 1;
}
int main(void)
{
Segmento t[3] = {
{0x10000, 0x3000, 1, 0, 1}, /* código: r-x */
{0x20000, 0x2000, 1, 1, 0}, /* datos: rw- */
{0x30000, 0x1000, 1, 1, 0} /* pila: rw- */
};
struct { int seg; unsigned long off; char modo; } casos[] = {
{0, 0x1000, 'r'}, {0, 0x1000, 'w'}, {1, 0x2000, 'r'},
{1, 0x0000, 'x'}, {5, 0x0000, 'r'}
};
for (int i = 0; i < 5; i++) {
int ok = acceder(t, casos[i].seg, casos[i].off, casos[i].modo);
printf("seg %d off 0x%04lx modo %c -> %s\\n",
casos[i].seg, casos[i].off, casos[i].modo, ok ? "OK" : "SIGSEGV");
}
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o proteccion_seg proteccion_seg.c
$ ./proteccion_seg
seg 0 off 0x1000 modo r -> OK
seg 0 off 0x1000 modo w -> SIGSEGV
seg 1 off 0x2000 modo r -> SIGSEGV
seg 1 off 0x0000 modo x -> SIGSEGV
seg 5 off 0x0000 modo r -> SIGSEGVseg 5 off 0x0000 modo r -> SIGSEGV
8. Probar con un simulador
Este simulador evalúa un acceso (segmento, desplazamiento, modo) contra una tabla de segmentos editable con base, límite y permisos.
Simulador · protección por segmento
| Campo | Valor |
|---|---|
| Base | |
| Límite | |
| Permisos | |
| ¿Dentro del límite? | |
| ¿Permiso concedido? | |
| Resultado |
Tabla de segmentos (editable: base, límite, r, w, x):
9. En Linux y x86 hoy
En Linux moderno, la protección no se hace por segmentos (que son planos en x86-64) sino por páginas (bits de permisos en la tabla de páginas). Pero el concepto de segmentación sigue en:
- ELF: los segmentos (LOAD) definen regiones con permisos.
- mprotect: cambia los permisos de una región de memoria (tema 42).
- W^X: la política de no permitir escritura y ejecución simultáneas.
10. Resumen
- La tabla de segmentos tiene una entrada por segmento, con base, límite y permisos.
- La protección se verifica en cada acceso: segmento válido, desplazamiento dentro del límite, permiso concedido.
- La compartición de código es natural: varios procesos apuntan al mismo segmento físico.
- SIGSEGV es el mecanismo de protección funcionando, no un error de segmentación clásico.
- En Linux moderno, la protección real se hace por páginas, pero los conceptos persisten.
11. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué campos tiene una entrada de la tabla de segmentos?
Base, límite, permisos (r, w, x), presente y nivel de privilegio.
2. ¿Qué comprueba la MMU en cada acceso?
Que el segmento sea válido, que el desplazamiento sea menor que el límite y que el modo de acceso esté permitido.
3. ¿Cómo se comparte código entre procesos con segmentación?
Varios procesos tienen entradas en su tabla que apuntan al mismo segmento físico (misma base y límite).
4. ¿Por qué la compartición requiere que el segmento sea de solo lectura?
Para evitar que un proceso modifique el código o datos compartidos de otro.
5. ¿Qué es SIGSEGV y por qué se llama "falla de segmentación"?
Es la señal que se envía cuando un acceso es inválido. El nombre es histórico, de los sistemas con segmentación.
6. ¿Qué causas típicas provocan SIGSEGV?
Puntero NULL, use-after-free, buffer overflow, escritura en memoria de solo lectura, dirección no mapeada.
7. ¿Cómo se cambian los permisos de una región en Linux?
Con mprotect(), que modifica los permisos de las páginas de una región (tema 42).
8. ¿Qué es la política W^X?
Escribir o ejecutar, pero no ambas: una región no puede ser writable y executable a la vez.
Para practicar
- Modifica
proteccion_seg.cpara añadir un segmento de heap (seg 3) con permisos rw- y límite 0x2000. - Evalúa los accesos (3, 0x1FFF, 'w'), (3, 0x2000, 'r') y (0, 0x0000, 'w') con el simulador.
- Explica por qué la compartición de código ahorra memoria en un sistema con muchos procesos.
- Investiga cómo implementa Linux la compartición de bibliotecas (.so) con mmap.
- Usa mprotect para cambiar una región a solo lectura y verifica que escribir en ella provoca SIGSEGV.
- Explica la diferencia entre una SIGSEGV por desplazamiento fuera de rango y una por permiso denegado.
- Dibuja la tabla de segmentos de un proceso con código, datos y pila, y marca los permisos de cada uno.