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.
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:
La tabla siguiente resume cada zona, que contiene y que permisos suele tener:
| Zona | Que contiene | Permisos típicos | Cambia de tamaño |
|---|---|---|---|
| Texto | Las instrucciones del programa y los literales de cadena. | Lectura y ejecución | No |
| Datos inicializados | Variables globales y estáticas con valor inicial distinto de cero. | Lectura y escritura | No |
| BSS | Variables globales y estáticas sin inicializar, o inicializadas a cero. | Lectura y escritura | No |
| Montículo (heap) | Memoria dinámica pedida con malloc, calloc, realloc. | Lectura y escritura | Sí, hacia arriba |
| Región mapeada | Bibliotecas compartidas, archivos mapeados en memoria, memoria anónima. | Según el mapeo | Sí |
| Pila (stack) | Variables locales, parámetros, direcciones de retorno, registros guardados. | Lectura y escritura | Sí, hacia abajo |
| Espacio del núcleo | Código y datos del sistema operativo. | Solo núcleo | No |
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:
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:
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.
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.
#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.
#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:
#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 *) ¶metro);
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:
$ 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/maps | Zona | Cómo se reconoce |
|---|---|---|
555555555000-555555556000 r-xp ... imagen | Texto (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 ... imagen | Literales de solo lectura | Permisos r--p: lectura sin escritura ni ejecución. Aquí está la cadena "hola", en 0x555555556004. |
555555558000-555555559000 rw-p ... imagen | Datos y BSS | Permisos rw-p: lectura y escritura. Aquí están global_inicializada y global_no_inicializada, en 0x555555558010 y 0x555555558018. |
555555559000-55555557a000 rw-p ... [heap] | Montículo | Etiquetada [heap]. Aquí está la memoria pedida con malloc, en 0x5555555592a0. |
7ffff7d9d000-... libc.so.6 | Biblioteca compartida | Varias regiones con distintos permisos, una por cada sección de libc. Se comparte entre procesos. |
7ffffffde000-7ffffffff000 rw-p ... [stack] | Pila | Etiquetada [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.
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:
- Las direcciones de
mainyfuncioncaen en la región con permisosr-xp: el segmento de texto. - La cadena
"hola"cae en la regiónr--pde solo lectura: los literales viven junto al código. global_inicializadayglobal_no_inicializadacaen en la regiónrw-pdel ejecutable: datos y BSS juntos.- La memoria pedida con
malloccae en la región[heap]. - Las variables locales y los parámetros caen en la región
[stack], con direcciones mucho más altas.
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
- El espacio de direcciones de un proceso es el conjunto de direcciones que puede usar, y cada proceso tiene el suyo propio.
- La imagen de un proceso se organiza en zonas: texto, datos inicializados, BSS, montículo, región mapeada, pila y espacio del núcleo.
- El segmento de texto contiene el código y los literales de cadena; es de solo lectura y se comparte entre procesos que ejecutan el mismo programa.
- El segmento de datos guarda las variables globales y estáticas con valor inicial distinto de cero; el BSS, las que no se inicializan o se inicializan a cero, sin ocupar espacio en el disco.
- El montículo crece hacia arriba y sirve para la memoria dinámica que se pide con
mallocy se libera confree. - La pila crece hacia abajo y guarda variables locales, parámetros y direcciones de retorno; tiene un tamaño máximo fijo.
- La región mapeada aloja las bibliotecas compartidas y los archivos mapeados en memoria.
- El espacio del núcleo, en la parte alta, es inaccesible para el proceso.
- /proc/PID/maps muestra cada región con su rango, permisos y archivo respaldante; las etiquetas
[heap]y[stack]identifican al montículo y a la pila.
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
- Compila
imagen.c, ejecútalo en segundo plano y lee /proc/PID/maps. identifica cada zona usando las direcciones que imprimió el programa. - Repite el ejercicio con
setarch -R ./imageny compara las direcciones: verás que son estables, pero la estructura del mapa es la misma. - 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. - Escribe un programa que pida memoria con
mallocen un bucle, imprimiendo la dirección cada vez, y observa como el montículo crece en /proc/PID/maps. - Compila con
gcc -Sun programa simple y busca en el ensamblador las instrucciones que usan el segmento de datos y el BSS. - Ejecuta
size ./imageny compara los valores de text, data y bss con lo que muestra /proc/PID/maps.