← Gestión de Memoria Bloque 2 · La pila y el heap · Tema 8 de 52

Tema 8

Datos, BSS y segmento de código

No toda la memoria de un programa se pide en tiempo de ejecución. El compilador y el enlazador colocan las instrucciones, las variables globales inicializadas, las no inicializadas y las constantes en secciones del binario, que el cargador mapea al espacio de direcciones. En este tema verás dónde vive cada cosa, cómo se cargan y cómo inspeccionarlas con size, readelf y objdump.

1. El binario y sus secciones

Un ejecutable en Linux (formato ELF) no es un bloque de bytes indiferenciado: está dividido en secciones, cada una con un propósito. El compilador genera estas secciones y el enlazador las combina. Las más importantes para la memoria son:

SecciónContenidoPermisos
.textInstrucciones del programa.r-x
.dataVariables globales y estáticas inicializadas.rw-
.bssVariables globales y estáticas no inicializadas (o a cero).rw-
.rodataConstantes y literales de cadena.r--
.init / .finiCódigo de inicialización y finalización.r-x

El cargador del sistema mapea estas secciones en el espacio de direcciones del proceso con los permisos adecuados. Por eso el código es de solo lectura y ejecución, y los datos son de lectura y escritura.

2. El segmento de código (.text)

La sección .text contiene las instrucciones máquina del programa. Es la parte que la CPU ejecuta. Se mapea con permisos r-x (lectura y ejecución, sin escritura) para evitar que un programa modifique su propio código (política W^X).

Todas las funciones del programa y las funciones estáticas viven aquí. Las funciones de bibliotecas compartidas (libc) viven en sus propios archivos .so, mapeados por separado.

3. Los datos inicializados (.data)

La sección .data contiene las variables globales y estáticas que tienen un valor inicial distinto de cero. Como el valor inicial está en el binario, esta sección ocupa espacio en el archivo y se carga en memoria tal cual.

ejemplos de .data
int contador = 0;          /* BSS: inicializado a cero */
int max = 100;             /* .data: valor inicial distinto de cero */
static int estado = 1;     /* .data */
char nombre[] = "ana";      /* .data: array inicializado */

4. El BSS (.bss)

La sección .bss (Block Started by Symbol) contiene las variables globales y estáticas no inicializadas o inicializadas a cero. Como su valor inicial es cero, no ocupa espacio en el archivo: el cargador reserva la memoria y la pone a cero al cargar el programa.

Esto ahorra espacio en disco: un array global de 1 MiB inicializado a cero no añade 1 MiB al binario, solo una entrada en la tabla de secciones que dice "reserva 1 MiB a cero".

Por qué .bss no ocupa espacio en disco

El archivo ELF solo guarda el tamaño de la sección, no sus bytes. El cargador reserva esa cantidad de memoria y la pone a cero. Por eso un programa con un array global grande puede tener un binario pequeño.

5. Las constantes (.rodata)

La sección .rodata (read-only data) contiene las constantes y los literales de cadena. Se mapea con permisos de solo lectura (r--), por lo que intentar modificarla provoca SIGSEGV.

ejemplos de .rodata
const int MAX = 100;                  /* .rodata */
const char *msg = "hola";             /* el literal "hola" está en .rodata */
printf("valor: %d\n", x);              /* el formato está en .rodata */

Un error clásico es intentar modificar un literal de cadena: char *s = "hola"; s[0] = 'H'; provoca SIGSEGV porque "hola" está en .rodata.

6. Cómo se cargan en memoria

El cargador del sistema (el programa que ejecuta execve) lee el ELF y mapea las secciones en el espacio de direcciones del proceso:

  1. Lee la cabecera ELF para saber qué segmentos cargar (LOAD).
  2. Mapea .text y .rodata con permisos r-x y r-- respectivamente, desde el archivo.
  3. Mapea .data con permisos rw-, copiando los bytes del archivo.
  4. Reserva .bss a cero (sin leer del archivo), ampliando la región de datos.
  5. Configura la pila y el heap iniciales.
  6. Salta al punto de entrada (_start), que llama a main.

Los segmentos LOAD del ELF agrupan secciones adyacentes con los mismos permisos. Por eso .text y .rodata pueden compartir un segmento de solo lectura, y .data y .bss otro de lectura/escritura.

7. Experimento en C

El programa siguiente declara variables en cada sección e imprime sus direcciones, para que puedas verificar en qué región cae cada una:

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

int global_init = 42;          /* .data */
int global_bss;                /* .bss */
static int estatica_init = 7; /* .data */
static int estatica_bss;      /* .bss */
const int CONSTANTE = 100;    /* .rodata */

static void funcion(void) { }  /* .text */

int main(void)
{
    static int estatica_local = 9;   /* .data */
    int local = 1;                    /* pila */
    const char *msg = "hola";         /* "hola" en .rodata */

    printf("función         : %p (.text)\n", (void *)&funcion);
    printf("global_init     : %p (.data)\n", (void *)&global_init);
    printf("global_bss      : %p (.bss)\n", (void *)&global_bss);
    printf("estatica_init   : %p (.data)\n", (void *)&estatica_init);
    printf("estatica_bss    : %p (.bss)\n", (void *)&estatica_bss);
    printf("CONSTANTE       : %p (.rodata)\n", (const void *)&CONSTANTE);
    printf("estatica_local  : %p (.data)\n", (void *)&estatica_local);
    printf("local           : %p (pila)\n", (void *)&local);
    printf("literal \"hola\"  : %p (.rodata)\n", (const void *)msg);
    return 0;
}
terminal · direcciones por sección
$ gcc -std=c17 -Wall -Wextra -o secciones secciones.c
$ ./secciones
función         : 0x401149 (.text)
global_init     : 0x404010 (.data)
global_bss      : 0x404014 (.bss)
estatica_init   : 0x404018 (.data)
estatica_bss    : 0x40401c (.bss)
CONSTANTE       : 0x402004 (.rodata)
estatica_local  : 0x404020 (.data)
local           : 0x7ffe2c3a1f2c (pila)
literal "hola"  : 0x402008 (.rodata)

Las direcciones de .text, .rodata y .data/.bss están cerca (en el rango bajo, típico de un binario sin PIE). La variable local está en la pila (zona alta). Las constantes y los literales comparten .rodata.

8. Inspección con size, readelf y objdump

terminal · inspeccionar secciones
$ size secciones
   text    data     bss     dec     hex filename
   1520     612      12    2144     860 secciones   1520     612      12    2144     860 secciones
terminal · readelf y objdump
$ readelf -S secciones | grep -E 'text|data|bss|rodata'
  [14] .text   PROGBITS  0000000000401060 ... AX
  [15] .rodata PROGBITS  0000000000402000 ...  A
  [24] .data   PROGBITS  0000000000404000 ...  WA
  [25] .bss    NOBITS    0000000000404024 ...  WA

readelf -S muestra las secciones con su dirección virtual, tipo y flags (A = allocatable, W = writable, X = executable). .bss es NOBITS: no ocupa espacio en el archivo.

terminal · objdump
$ objdump -s -j .rodata secciones | head
Contents of section .rodata:
 402000 01000200 686f6c61 00 ...  ....hola.
$ objdump -d secciones | grep '
'

9. Tamaño en disco vs en memoria

El binario en disco contiene .text, .data, .rodata y los metadatos. El .bss no ocupa espacio en disco (solo una entrada con el tamaño). En memoria, el cargador reserva espacio para todas las secciones, incluido .bss (poniéndolo a cero). Por eso un programa puede tener un binario pequeño pero usar mucha RAM si tiene arrays globales grandes.

SecciónEn discoEn memoria
.textSíSí
.dataSíSí
.rodataSíSí
.bssNo (solo tamaño)Sí (a cero)

10. Probar con un simulador

Este simulador muestra cómo se distribuyen las secciones en el espacio de direcciones y en el archivo, y cómo cambia el tamaño en disco y en memoria al variar el tamaño de .bss y .data.

Simulador · secciones de un binario

MedidaValor
Tamaño en disco
Tamaño en memoria (estas secciones)
Porcentaje de .bss en memoria

11. Mirar en Linux

terminal · secciones y símbolos
$ readelf -h secciones | grep -E 'Class|Machine|Type|Entry'
$ readelf -l secciones | grep -A2 LOAD
$ nm secciones | grep -i ' U '
$ objdump -t secciones | grep global_init

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué contiene la sección .bss?

Variables globales y estáticas no inicializadas o inicializadas a cero. No ocupa espacio en el archivo; se pone a cero al cargar.

2. ¿Qué diferencia hay entre .data y .bss?

.data contiene variables inicializadas a valores distintos de cero (ocupa espacio en disco). .bss contiene las no inicializadas (no ocupa espacio en disco).

3. ¿Dónde viven las constantes (const)?

En .rodata, que se mapea de solo lectura. Intentar modificarla provoca SIGSEGV.

4. ¿Por qué .bss no ocupa espacio en el archivo?

Porque su contenido inicial es cero: el cargador reserva la memoria y la pone a cero, sin necesidad de guardar los bytes en disco.

5. ¿Qué permisos tiene .text?

r-x (lectura y ejecución, sin escritura), por la política W^X.

6. ¿Qué muestra size?

El tamaño de las secciones text, data y bss, más el total en decimal y hexadecimal.

7. ¿Qué es NOBITS en readelf?

Indica que la sección no ocupa espacio en el archivo (como .bss); solo se reserva en memoria.

8. ¿Qué herramienta muestra los segmentos LOAD del ELF?

readelf -l, que lista los segmentos y cómo se mapean en memoria.

Para practicar

  1. Añade a secciones.c un array global de 1 MiB inicializado a cero. ¿Cambia el tamaño del binario? ¿Y el de .bss?
  2. Inicializa ese array con un valor distinto de cero. ¿Cambia el tamaño del binario ahora?
  3. Usa readelf -l para ver los segmentos LOAD. ¿Cuántos hay? ¿Qué secciones agrupa cada uno?
  4. Intenta modificar un literal de cadena: char *s = "hola"; s[0] = 'H';. ¿Qué ocurre?
  5. Compila con -fdata-sections -ffunction-sections y observa cómo cambian las secciones (cada función/dato en su propia sección).
  6. Usa objdump -s -j .data para ver los bytes de los datos inicializados. ¿Reconoces los valores?
  7. Inspecciona un binario del sistema (/bin/ls) con size y readelf -S. ¿Qué secciones tiene?