La imagen de un proceso en memoria - Procesos e Hilos
← Procesos e Hilos Bloque 1 · El concepto de proceso · Tema 2 de 43

Tema 2

La imagen de un proceso en memoria

Un proceso no es solo un programa corriendo: es una región entera de memoria organizada en zonas con propósito distinto. El código que se ejecuta, las variables que duran toda la vida del programa, la memoria que se pide en tiempo de ejecución, la pila de llamadas, las bibliotecas compartidas y el espacio del núcleo: todo convive en un mismo espacio de direcciones. En este tema recorrerás cada zona, verás como se diferencian y aprenderás a leerlas en /proc/PID/maps.

1. El espacio de direcciones de un proceso

El espacio de direcciones de un proceso es el conjunto de direcciones de memoria que ese proceso puede usar. Cada dirección es un número, y el proceso se comporta como si tuviera toda esa memoria para él solo, empezando en la dirección 0 y llegando hasta un límite que depende de la arquitectura: 232 bytes en una máquina de 32 bits, 248 o más en una de 64 bits. No toda esa memoria está ocupada ni es accesible; las zonas que el proceso realmente usa son las que se cargan desde el archivo del programa y las que va pidiendo en tiempo de ejecución.

En el tema anterior viste que dos procesos pueden guardar datos en la misma dirección y que, aun así, cada uno ve su propio valor. Eso es porque cada proceso tiene su propio espacio de direcciones: la misma dirección, en dos procesos, apunta a posiciones físicas distintas de la RAM. El núcleo y el hardware se encargan de traducir, por separado para cada proceso, esas direcciones virtuales a direcciones físicas. A esa traducción se la llama memoria virtual, y es el tema central del curso de Gestión de memoria. Aquí nos basta con saber que cada proceso tiene su propio espacio, y con entender como está organizado por dentro.

La organización no es aleatoria. Cuando el núcleo carga un programa en memoria, lo coloca según un formato llamado ELF (Executable and linkable format), el estándar de los ejecutables en linux. El archivo ELF describe que partes del programa van en que zonas de la memoria, y el núcleo las reserva y carga al crear el proceso. Lo que resulta es una imagen con varias regiones bien diferenciadas, cada una con su propósito y sus permisos.

La imagen del proceso

Se llama imagen del proceso al conjunto de todas las zonas de memoria que ocupan su código, sus datos y sus estructuras internas en un instante dado. Es lo que el núcleo carga al crear el proceso y lo que va creciendo a medida que el programa pide más memoria o llama a más funciones. Cuando un proceso termina, esa imagen desaparece.

2. Las zonas de la memoria de un proceso

Estas son las regiones que componen la imagen de un proceso típico en linux, de las direcciones más bajas a las más altas. No todas aparecen en todos los programas, y el orden exacto depende de la arquitectura y de las opciones de compilación, pero la idea general es siempre la misma:

Diagrama vertical del espacio de direcciones de un proceso de 64 bits, de las direcciones bajas abajo a las altas arriba. De abajo a arriba: la página nula no accesible; el segmento de texto con el código; el segmento de datos inicializados; el BSS; el montículo que crece hacia arriba; una zona libre; la región mapeada con bibliotecas compartidas y archivos mapeados; la pila que crece hacia abajo; y arriba del todo el espacio del núcleo reservado. direcciones altas direcciones bajas Espacio del núcleo reservado para el sistema operativo · inaccesible para el proceso Pila (stack) variables locales, parámetros, direcciones de retorno · crece hacia abajo crece ↓ Región mapeada (mmap) bibliotecas compartidas, archivos mapeados, memoria anónima zona libre (sin usar) Montículo (heap) memoria dinámica: malloc, calloc, realloc · crece hacia arriba crece ↑ BSS variables globales y estáticas no inicializadas (o inicializadas a cero) Datos inicializados variables globales y estáticas con valor inicial distinto de cero Texto (código) las instrucciones del programa · solo lectura página nula · no accesible (atrapa punteros NULL)
La imagen de un proceso en memoria. De abajo a arriba: el código, los datos, el BSS, el montículo que crece hacia arriba, una zona libre, la región mapeada, la pila que crece hacia abajo y el espacio del núcleo. Las direcciones exactas varían por ASLR.

La tabla siguiente resume cada zona, que contiene y que permisos suele tener:

ZonaQue contienePermisos típicosCambia de tamaño
TextoLas instrucciones del programa y los literales de cadena.Lectura y ejecuciónNo
Datos inicializadosVariables globales y estáticas con valor inicial distinto de cero.Lectura y escrituraNo
BSSVariables globales y estáticas sin inicializar, o inicializadas a cero.Lectura y escrituraNo
Montículo (heap)Memoria dinámica pedida con malloc, calloc, realloc.Lectura y escrituraSí, hacia arriba
Región mapeadaBibliotecas compartidas, archivos mapeados en memoria, memoria anónima.Según el mapeoSí
Pila (stack)Variables locales, parámetros, direcciones de retorno, registros guardados.Lectura y escrituraSí, hacia abajo
Espacio del núcleoCódigo y datos del sistema operativo.Solo núcleoNo

Veamos cada zona con más detalle.

3. El segmento de texto: el código

El segmento de texto contiene las instrucciones máquina del programa: el resultado de compilar el código fuente. Es la parte que el procesador ejecuta, instrucción por instrucción, siguiendo el contador de programa. Suele ser de solo lectura, porque el programa no debe modificar sus propias instrucciones mientras se ejecuta; si lo intenta, el núcleo lo termina con la señal SIGSEGV. En algunos sistemas antiguos el código era modificable, pero hoy no.

El segmento de texto también guarda los literales de cadena, como "hola", porque son datos de solo lectura que el compilador coloca junto al código. Por eso, si escribes char *s = "hola";, el puntero s vive donde lo declares (en la pila si es local, en los datos si es global), pero la cadena a la que apunta vive en el segmento de texto. Intentar modificarla, por ejemplo con s[0] = 'H';, es un error clásico que termina en una violación de segmento.

Otra propiedad importante: el segmento de texto se comparte entre todos los procesos que ejecutan el mismo programa. Si abres dos terminales y ejecutas bash en cada una, las dos tienen su propio proceso, con su propia memoria para datos y pila, pero comparten el mismo código en la RAM. El núcleo carga el archivo ELF una sola vez y lo mapea en el espacio de direcciones de cada proceso. Es una de las razones por las que ejecutar varios procesos del mismo programa no multiplica el consumo de memoria tanto como podría parecer.

4. El segmento de datos inicializados

El segmento de datos guarda las variables globales y estáticas que tienen un valor inicial distinto de cero. Por ejemplo:

datos.c
int global = 42;              /* segmento de datos */
static int estatica = 7;      /* segmento de datos */

int main(void)
{
    static int local_estatica = 3;  /* también datos */
    return 0;
}

Esas variables existen durante toda la vida del proceso, desde que se crea hasta que termina, y mantienen su valor entre una llamada a función y otra. Como el compilador conoce sus valores iniciales, los guarda en el archivo ejecutable, y el núcleo los copia a la memoria al cargar el programa. Por eso el tamaño del segmento de datos está fijado en tiempo de compilación: no crece ni se achica mientras el programa se ejecuta.

Un detalle que conviene recordar: una variable local normal, declarada dentro de una función sin static, no va aquí. Va a la pila, que veremos más adelante.

5. El segmento BSS

El BSS (Block started by symbol, un nombre heredado de un ensamblador antiguo) es la zona para las variables globales y estáticas que no se inicializan, o que se inicializan a cero:

bss.c
int sin_inicializar;          /* BSS */
int a_cero = 0;               /* BSS */
static int estatica = 0;      /* BSS */

int main(void)
{
    static int otra = 0;        /* BSS */
    return 0;
}

El estándar de C garantiza que las variables globales y estáticas que no se inicializan explícitamente valen cero al empezar el programa. El núcleo cumple esa garantía de la forma más económica posible: no guarda los ceros en el archivo ejecutable. En su lugar, el archivo ELF solo dice «esta zona del BSS tiene tantos bytes», y el núcleo la reserva en memoria y la pone toda a cero al cargar el programa. Por eso un ejecutable puede ser mucho más chico que la memoria que ocupa: si declaras un arreglo global de un millón de enteros sin inicializar, el archivo crece casi nada, pero el proceso ocupa cuatro megabytes de BSS.

El BSS no ocupa espacio en el disco

Los datos inicializados sí ocupan espacio en el archivo ejecutable, porque hay que guardar sus valores. El BSS no: solo se anota su tamaño. Esta diferencia explica por que un programa con grandes arreglos globales sin inicializar compila a un ejecutable pequeño, pero consume mucha memoria al correr.

En la práctica, datos y BSS suelen aparecer juntos en /proc/PID/maps como una sola región de lectura y escritura, porque ambos tienen los mismos permisos. La distinción es lógica, no física.

6. El montículo (heap)

El montículo, o heap, es la zona de la memoria que un programa usa para pedir espacio en tiempo de ejecución, cuando no sabe de antemano cuánto va a necesitar. En C se pide con malloc, calloc y realloc, y se devuelve con free. A diferencia de las variables globales, que viven toda la vida del proceso, la memoria del montículo vive desde que se pide hasta que se libera, y puede ser de cualquier tamaño.

monticulo.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *p = malloc(1000 * sizeof *p);  /* 1000 enteros en el montículo */
    if (p == NULL) {
        perror("malloc");
        return 1;
    }
    p[0] = 42;
    printf("memoria pedida en %p\n", (void *) p);

    free(p);   /* la memoria vuelve al montículo */
    return 0;
}

El montículo crece hacia las direcciones más altas, desde el final del BSS hacia arriba. Cuando un programa pide memoria, la biblioteca de C (glibc) administra el montículo: si ya tiene espacio libre, lo reutiliza; si no, le pide al núcleo que amplíe la zona con la llamada al sistema brk (o sbrk). El núcleo mueve el límite superior del montículo y le da al proceso las páginas nuevas. Cuando se libera memoria, glibc puede devolverla al núcleo o guardarla para reutilizarla después.

Por eso el montículo no crece de a un byte: crece de a páginas, bloques de memoria del tamaño que use el sistema (típicamente 4096 bytes en linux). Un malloc de 16 bytes puede no pedirle nada al núcleo si glibc ya tiene espacio disponible; un malloc de varios megabytes puede usar directamente mmap, que crea una región separada en lugar de agrandar el montículo.

7. La pila (stack)

La pila es la zona donde viven las variables locales de las funciones, los parámetros que se les pasan y las direcciones de retorno que permiten volver al llamador. Cada vez que se llama a una función, el programa reserva en la pila un bloque llamado marco de pila (stack frame), con todo lo que esa función necesita; cuando la función termina, ese bloque se descarta y la pila vuelve al estado anterior.

pila.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>

void g(int y)
{
    int z = y + 1;     /* z vive en la pila, en el marco de g */
    printf("en g: z = %d en %p\n", z, (void *) &z);
}

void f(int x)
{
    int a = x * 2;     /* a vive en la pila, en el marco de f */
    printf("en f: a = %d en %p\n", a, (void *) &a);
    g(a);
}

int main(void)
{
    int n = 5;         /* n vive en la pila, en el marco de main */
    f(n);
    return 0;
}

La pila crece hacia las direcciones más bajas, es decir, hacia abajo, en dirección opuesta al montículo. Cada llamada a función empuja un marco nuevo; cada retorno lo saca. Por eso la pila tiene un tamaño máximo fijado al crear el proceso (en linux, por omisión, 8 MiB en la arquitectura x86-64), y si un programa la desborda —por ejemplo con recursión infinita o con un arreglo local enorme— el núcleo lo termina con SIGSEGV. Ese límite se puede consultar y cambiar con ulimit -s.

Un detalle útil: las variables locales de una función desaparecen cuando la función retorna. Si una función devuelve la dirección de una variable local, esa dirección deja de ser válida, y usarla después es un error grave (un puntero colgante). Es por eso que las funciones que necesitan devolver datos que sobrevivan a la llamada los piden en el montículo, o reciben un puntero a un búfer que les pasa el llamador.

8. La región mapeada y las bibliotecas compartidas

Entre el montículo y la pila queda un espacio grande que el núcleo usa para mapear archivos y memoria. Ahí es donde se cargan las bibliotecas compartidas (como libc.so), los archivos que un programa pide mapear con mmap, y las regiones de memoria anónima que la biblioteca de C usa para pedidos grandes de malloc.

Cuando un programa en C llama a printf, el código de printf no está en el ejecutable: está en la biblioteca compartida libc, que el núcleo mapea en la región mapeada del proceso. La ventaja es que, igual que con el segmento de texto, la biblioteca se carga una sola vez en la RAM y se comparte entre todos los procesos que la usan. Por eso un programa que usa libc no duplica su código en cada proceso.

La región mapeada también sirve para mapear archivos directamente en memoria: en lugar de leer un archivo con read, un programa puede pedirle al núcleo que lo coloque en su espacio de direcciones y acceder a él como si fuera un arreglo. Es una técnica avanzada que se usa, por ejemplo, en bases de datos y en servidores de archivos.

9. El espacio del núcleo

En la parte más alta del espacio de direcciones está el espacio del núcleo, reservado para el sistema operativo. El código y los datos del núcleo viven ahí, y son inaccesibles para el proceso: si intenta leerlos o escribirlos, el hardware lo detiene y el núcleo lo termina. En linux, en una máquina de 64 bits, la mitad superior del espacio de direcciones (a partir de 0x800000000000) está reservada al núcleo.

Este espacio se mapea en todos los procesos, no solo en uno, porque el núcleo necesita estar disponible para atender llamadas al sistema desde cualquier proceso. Pero el proceso no puede acceder a él directamente: solo entra al espacio del núcleo cuando hace una llamada al sistema, y entonces el procesador cambia de modo usuario a modo núcleo, ejecuta el código del sistema y vuelve. Es el tema del bloque 2.

10. Ver todo en /proc/PID/maps

En el tema anterior viste /proc/PID/status, que resume el proceso. Ahora toca /proc/PID/maps, que muestra el mapa completo de su memoria: cada región, su rango de direcciones, sus permisos y, cuando se puede, el archivo que la respalda.

Para observarlo con calma, compilamos un programa que se queda esperando después de pedir memoria en varias zonas, y lo dejamos corriendo:

imagen.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int global_inicializada = 42;          /* datos */
int global_no_inicializada;             /* BSS */
static int estatica_inicializada = 7;  /* datos */
static int estatica_no_inicializada;   /* BSS */

void funcion(int parametro)
{
    int local = 10;                     /* pila */
    int *dinamico = malloc(sizeof *dinamico);  /* montículo */
    *dinamico = 99;

    printf("PID: %d\n", (int) getpid());
    printf("código de funcion:    %p\n", (void *) funcion);
    printf("parámetro:            %p\n", (void *) &parametro);
    printf("variable local:       %p\n", (void *) &local);
    printf("puntero dinámico:     %p\n", (void *) &dinamico);
    printf("memoria del montículo:%p\n", (void *) dinamico);
    printf("global inicializada:  %p\n", (void *) &global_inicializada);
    printf("global no inicializ.: %p\n", (void *) &global_no_inicializada);

    printf("Presiona enter para terminar...\n");
    getchar();

    free(dinamico);
}

int main(void)
{
    int local_main = 1;
    char *cadena = "hola";            /* literal en el texto */

    printf("main (código):        %p\n", (void *) main);
    printf("cadena literal:       %p\n", (void *) cadena);
    printf("cadena (puntero):     %p\n", (void *) &cadena);
    printf("local de main:        %p\n", (void *) &local_main);

    funcion(5);
    return 0;
}

Lo compilamos y lo ejecutamos en segundo plano, y desde otra terminal leemos /proc/PID/maps:

terminal · leer el mapa de memoria de un proceso
$ gcc -std=c17 -Wall -Wextra -o imagen imagen.c
$ ./imagen &
[1] 512
PID: 512
main (código):        0x555555555149
cadena literal:       0x555555556004
cadena (puntero):     0x7fffffffe3e8
local de main:        0x7fffffffe3ec
código de funcion:    0x5555555551a9
parámetro:            0x7fffffffe3cc
variable local:       0x7fffffffe3c8
puntero dinámico:     0x7fffffffe3d0
memoria del montículo:0x5555555592a0
global inicializada:  0x555555558010
global no inicializ.: 0x555555558018
$ cat /proc/512/maps
555555554000-555555555000 r--p 00000000 08:01 1310734 /home/ana/procesos/imagen
555555555000-555555556000 r-xp 00001000 08:01 1310734 /home/ana/procesos/imagen
555555556000-555555557000 r--p 00002000 08:01 1310734 /home/ana/procesos/imagen
555555557000-555555558000 r--p 00002000 08:01 1310734 /home/ana/procesos/imagen
555555558000-555555559000 rw-p 00003000 08:01 1310734 /home/ana/procesos/imagen
555555559000-55555557a000 rw-p 00000000 00:00 0                          [heap]
55555557a000-7ffff7d9d000 ---p 00000000 00:00 0
7ffff7d9d000-7ffff7da0000 r--p 00000000 08:01 1310734 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7da0000-7ffff7f35000 r-xp 00003000 08:01 1310734 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f35000-7ffff7f8d000 r--p 00198000 08:01 1310734 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f8d000-7ffff7f91000 r--p 001f0000 08:01 1310734 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f91000-7ffff7f93000 rw-p 001f4000 08:01 1310734 /usr/lib/x86_64-linux-gnu/libc.so.6
7ffff7f93000-7ffff7f9f000 rw-p 00000000 00:00 0
7ffff7fc4000-7ffff7fc8000 r--p 00000000 00:00 0                          [vvar]
7ffff7fc8000-7ffff7fca000 r-xp 00000000 00:00 0                          [vdso]
7ffff7fca000-7ffff7fcc000 r--p 00000000 08:01 1310734 /lib64/ld-linux-x86-64.so.2
7ffff7fcc000-7ffff7ff1000 r-xp 00002000 08:01 1310734 /lib64/ld-linux-x86-64.so.2
7ffff7ff1000-7ffff7ffa000 r--p 00017000 08:01 1310734 /lib64/ld-linux-x86-64.so.2
7ffff7ffa000-7ffff7ffc000 r--p 00020000 08:00 0
7ffff7ffc000-7ffff7ffd000 rw-p 00022000 08:01 1310734 /lib64/ld-linux-x86-64.so.2
7ffff7ffd000-7ffff7ffe000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
7ffffffff000-7fffffffffff ---p 00000000 00:00 0
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

Cada línea tiene el mismo formato: inicio-fin permisos desplazamiento dispositivo inodo ruta. El rango de direcciones está en hexadecimal, y los permisos son cuatro letras: r lectura, w escritura, x ejecución y p privada (o s compartida). La ruta dice que archivo respalda esa región: el ejecutable, una biblioteca, o nada, si es memoria anónima.

Identifiquemos las zonas del programa en esa salida:

Región en /proc/PID/mapsZonaCómo se reconoce
555555555000-555555556000 r-xp ... imagenTexto (código)Permisos r-xp: lectura y ejecución, sin escritura. Aquí viven main y funcion, cuyas direcciones empiezan por 0x555555555....
555555556000-555555557000 r--p ... imagenLiterales de solo lecturaPermisos r--p: lectura sin escritura ni ejecución. Aquí está la cadena "hola", en 0x555555556004.
555555558000-555555559000 rw-p ... imagenDatos y BSSPermisos rw-p: lectura y escritura. Aquí están global_inicializada y global_no_inicializada, en 0x555555558010 y 0x555555558018.
555555559000-55555557a000 rw-p ... [heap]MontículoEtiquetada [heap]. Aquí está la memoria pedida con malloc, en 0x5555555592a0.
7ffff7d9d000-... libc.so.6Biblioteca compartidaVarias regiones con distintos permisos, una por cada sección de libc. Se comparte entre procesos.
7ffffffde000-7ffffffff000 rw-p ... [stack]PilaEtiquetada [stack]. Aquí están las variables locales y los parámetros, en direcciones que empiezan por 0x7ffffff....

Fíjate en dos cosas. Primero, el código (0x555555555...) y los datos (0x555555558...) quedan en la misma región general del ejecutable, pero en páginas distintas con permisos distintos: el código no se puede escribir, los datos sí. Segundo, la pila (0x7ffffff...) y el montículo (0x555555559...) están muy lejos entre sí, con una zona libre enorme en el medio, que es donde crecen y donde se mapean las bibliotecas.

Las direcciones cambian por ASLR

La protección ASLR (que viste en el tema 1) coloca el código, el montículo, la pila y las bibliotecas en direcciones distintas en cada ejecución. Los rangos de /proc/PID/maps serán otros, pero la estructura —que zona está dónde y con que permisos— es la misma. Si quieres direcciones estables para experimentar, ejecuta el programa con setarch -R.

11. Cómo crecen el montículo y la pila

Las zonas de código, datos y BSS tienen un tamaño fijo desde que el programa se carga. Las que crecen y se achican mientras el programa corre son el montículo y la pila, y lo hacen en direcciones opuestas: el montículo hacia arriba, la pila hacia abajo. Esa disposición no es casual: al crecer en direcciones opuestas, las dos zonas aprovechan el espacio libre del medio y tardan mucho más en chocar entre sí.

El montículo crece cuando el programa pide memoria y glibc le pide al núcleo que amplíe la región con brk. La pila crece automáticamente cuando una función se llama y necesita más espacio: el procesador decrementa el puntero de pila y el núcleo reserva las páginas nuevas de forma transparente. Si la pila intenta crecer más allá de su límite, o si el montículo choca con la pila, el programa recibe SIGSEGV y termina.

Hay una excepción útil: para pedidos grandes, glibc no usa el montículo sino mmap, que crea una región anónima separada en la zona mapeada. Eso permite liberar esa memoria de vuelta al sistema cuando se hace free, algo que con el montículo no siempre ocurre, porque el montículo solo puede achicarse desde su extremo superior.

12. Un programa que muestra todas las zonas

El programa imagen.c del apartado anterior es, en realidad, un pequeño tour por la memoria: imprime la dirección de una variable de cada zona, y esas direcciones coinciden con las regiones de /proc/PID/maps. La idea es que, al ejecutarlo, puedas mirar la salida y el mapa al mismo tiempo, y entender que zona corresponde a cada dirección.

Algunas observaciones que vale la pena hacer al comparar la salida con el mapa:

Si ejecutas el programa dos veces, las direcciones cambian por ASLR, pero las relaciones entre ellas se mantienen: el código sigue estando por debajo del montículo, y el montículo por debajo de la pila.

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Que es el espacio de direcciones de un proceso?

Es el conjunto de direcciones de memoria que ese proceso puede usar. Cada proceso tiene el suyo propio, y la misma dirección en dos procesos apunta a posiciones físicas distintas de la RAM.

2. ¿En que se diferencian el segmento de datos y el BSS?

El segmento de datos guarda las variables globales y estáticas con valor inicial distinto de cero, y ocupa espacio en el archivo ejecutable. El BSS guarda las que no se inicializan o se inicializan a cero; el núcleo las pone a cero al cargar el programa, y no ocupan espacio en el disco.

3. ¿Por que un ejecutable con un arreglo global de un millón de enteros sin inicializar puede ser pequeño, pero consumir mucha memoria al correr?

Porque ese arreglo vive en el BSS. El archivo ELF solo anota el tamaño del BSS, sin guardar los ceros; el núcleo reserva y pone a cero esa memoria al cargar el programa, así que el proceso ocupa cuatro megabytes aunque el archivo casi no crezca.

4. ¿Que es el montículo, y en que dirección crece?

Es la zona para la memoria dinámica que se pide con malloc, calloc y realloc y se libera con free. Crece hacia las direcciones más altas, desde el final del BSS hacia arriba.

5. ¿Que guarda la pila, y en que dirección crece?

Guarda las variables locales de las funciones, los parámetros y las direcciones de retorno, en marcos de pila que se apilan y desapilan con cada llamada y retorno. Crece hacia las direcciones más bajas, es decir, hacia abajo.

6. ¿Por que el código y las bibliotecas compartidas no se duplican en cada proceso?

Porque el núcleo carga el archivo ELF y las bibliotecas compartidas una sola vez en la RAM y las mapea en el espacio de direcciones de cada proceso que las usa. Cada proceso tiene su propia memoria para datos y pila, pero comparte el código.

7. ¿Que significa la línea 555555559000-55555557a000 rw-p ... [heap] en /proc/PID/maps?

Que la región entre esas dos direcciones es el montículo del proceso: memoria de lectura y escritura, privada, respaldada por memoria anónima (no por un archivo). Es la zona que glibc administra y amplía con brk.

8. ¿Por que los literales de cadena como "hola" no se pueden modificar?

Porque el compilador los coloca en el segmento de texto, que es de solo lectura. Intentar modificarlos produce una violación de segmento y el núcleo termina el proceso con SIGSEGV.

9. ¿Que es ASLR y como afecta a /proc/PID/maps?

ASLR es la protección que coloca el código, el montículo, la pila y las bibliotecas en direcciones distintas en cada ejecución. Por eso los rangos de /proc/PID/maps cambian de una ejecución a otra, aunque la estructura de zonas se mantiene.

10. ¿Que ocurre si una función devuelve la dirección de una variable local?

Esa dirección deja de ser válida cuando la función retorna, porque su marco de pila se descarta. Usarla después es un puntero colgante, un error grave. Por eso las funciones que devuelven datos duraderos los piden en el montículo o reciben un búfer del llamador.

Para practicar

  1. Compila imagen.c, ejecútalo en segundo plano y lee /proc/PID/maps. identifica cada zona usando las direcciones que imprimió el programa.
  2. Repite el ejercicio con setarch -R ./imagen y compara las direcciones: verás que son estables, pero la estructura del mapa es la misma.
  3. Agrega una variable global más, sin inicializar, y un arreglo global grande, por ejemplo int grande[1000000];. Observa como cambia el tamaño de la región de datos y BSS en /proc/PID/maps.
  4. Escribe un programa que pida memoria con malloc en un bucle, imprimiendo la dirección cada vez, y observa como el montículo crece en /proc/PID/maps.
  5. Compila con gcc -S un programa simple y busca en el ensamblador las instrucciones que usan el segmento de datos y el BSS.
  6. Ejecuta size ./imagen y compara los valores de text, data y bss con lo que muestra /proc/PID/maps.