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ón | Contenido | Permisos |
|---|---|---|
| .text | Instrucciones del programa. | r-x |
| .data | Variables globales y estáticas inicializadas. | rw- |
| .bss | Variables globales y estáticas no inicializadas (o a cero). | rw- |
| .rodata | Constantes y literales de cadena. | r-- |
| .init / .fini | Có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.
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".
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.
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:
- Lee la cabecera ELF para saber qué segmentos cargar (LOAD).
- Mapea .text y .rodata con permisos r-x y r-- respectivamente, desde el archivo.
- Mapea .data con permisos rw-, copiando los bytes del archivo.
- Reserva .bss a cero (sin leer del archivo), ampliando la región de datos.
- Configura la pila y el heap iniciales.
- 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:
#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;
}
$ 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
$ size secciones
text data bss dec hex filename
1520 612 12 2144 860 secciones 1520 612 12 2144 860 secciones
$ 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.
$ 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ón | En disco | En memoria |
|---|---|---|
| .text | Sí | Sí |
| .data | Sí | Sí |
| .rodata | Sí | Sí |
| .bss | No (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
| Medida | Valor |
|---|---|
| Tamaño en disco | |
| Tamaño en memoria (estas secciones) | |
| Porcentaje de .bss en memoria |
11. Mirar en Linux
$ 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
readelf -h: cabecera ELF (clase, máquina, tipo, punto de entrada).readelf -l: segmentos (LOAD) y cómo se mapean.readelf -S: secciones detalladas.nm: símbolos (funciones y variables globales).objdump -t: tabla de símbolos con direcciones.
12. Resumen
- El binario ELF se divide en secciones: .text (código), .data (datos inicializados), .bss (no inicializados), .rodata (constantes).
- .text y .rodata son de solo lectura; .data y .bss son de lectura/escritura.
- .bss no ocupa espacio en disco: el cargador lo reserva a cero en memoria.
- El cargador mapea las secciones en el espacio de direcciones con los permisos adecuados.
size,readelf,objdumpynmpermiten inspeccionar el binario.
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
- Añade a
secciones.cun array global de 1 MiB inicializado a cero. ¿Cambia el tamaño del binario? ¿Y el de .bss? - Inicializa ese array con un valor distinto de cero. ¿Cambia el tamaño del binario ahora?
- Usa
readelf -lpara ver los segmentos LOAD. ¿Cuántos hay? ¿Qué secciones agrupa cada uno? - Intenta modificar un literal de cadena:
char *s = "hola"; s[0] = 'H';. ¿Qué ocurre? - Compila con
-fdata-sections -ffunction-sectionsy observa cómo cambian las secciones (cada función/dato en su propia sección). - Usa
objdump -s -j .datapara ver los bytes de los datos inicializados. ¿Reconoces los valores? - Inspecciona un binario del sistema (
/bin/ls) consizeyreadelf -S. ¿Qué secciones tiene?