Cuando compilas un programa en C, el código fuente desaparece para transformarse en una estructura compleja de secciones y tablas de símbolos dentro de un formato binario, generalmente ELF (Executable and Linkable Format) en sistemas Linux. Para un programador de sistemas, el binario no es una “caja negra”; es un mapa estructurado que contiene instrucciones de máquina, datos iniciales y punteros a recursos externos.
Estas herramientas permiten desglosar ese mapa. nm es el inspector de símbolos; te dice qué nombres (funciones o variables) existen y en qué sección residen. objdump es el desensamblador y analizador profundo; permite ver el código de máquina convertido a ensamblador y, si tienes información de depuración, incluso el código fuente original. readelf es la herramienta de inspección estructural; lee las cabeceras de la especificación ELF para mostrar la arquitectura, la tabla de secciones y las dependencias de bibliotecas.
Usarás estas herramientas cuando el linker te devuelva un error de undefined reference, cuando necesites verificar si una variable está en la sección .bss (memoria no inicializada) o .data (inicializada), o cuando necesites auditar el tamaño real de un binario embebido. Si no comprendes el estado de tus símbolos o la estructura de tus secciones, el comportamiento del cargador del sistema operativo será una adivinanza, provocando errores de segmentación o fallos de enlazado imposibles de rastrear solo con el código fuente.
#include <stdio.h>
/* Declaración de una variable externa (definida en otro objeto/biblioteca) */
extern int variable_externa;
/* Función con ámbito interno (static) */
static void funcion_privada(int valor) {
printf("Valor privado: %d\n", valor);
}
/* Variable global inicializada (se irá a .data) */
int variable_global = 42;
/* Variable global no inicializada (se irá a .bss) */
int variable_bss;
/* Prototipo para la función externa */
void funcion_externa_real(int);
int main(void) {
int local_stack = 10; // Variable en el stack, no aparece en nm
printf("Global: %d\n", variable_global);
printf("BSS: %d\n", variable_bss);
funcion_privada(local_stack);
/* Llamada a una función cuya definición es externa */
printf("Llamando a función externa...\n");
extern void funcion_externa_real(int);
funcion_externa_real(100);
return 0;
}
Para analizar este ejemplo, primero compila con información de depuración:
gcc -std=c11 -g -Wall -Wextra -Wpedantic -o binario binario.c
Al ejecutar nm binario, verás algo como esto:
– T main: La función main tiene un símbolo de texto (T), lo que significa que es un símbolo global definido en este objeto.
– U printf: printf aparece como U (Undefined) porque el binario confía en que la biblioteca estándar (libc) la proporcione en tiempo de ejecución.
– T variable_global: Símbolo de datos definido en la sección de datos.
– B variable_bss: Símbolo en la sección BSS (uninitialized data). El cargador la inicializará en cero.
– t funcion_privada: La t minúscula es crucial; indica que es un símbolo local. Solo es visible dentro de este archivo objeto y el linker no la exportará.
– U variable_externa: U indica que el símbolo es referenciado pero su definición no está aquí.
Si quieres ver el código ensamblador que el compilador generó para la función main, usa objdump -d binario. Si quieres la magia de ver tu código C mezclado con el ensamblador, usa objdump -S binario (esto requiere que hayas compilado con -g). Verás cómo local_stack se mueve a un registro o se asigna en una posición de la pila (sub $0x10, %rsp).
Para entender la estructura interna del archivo, readelf -S binario te mostrará las secciones reales:
– .text: Contiene el código ejecutable.
– .rodata: Contiene las cadenas de texto (como "Global: %d\n").
– .data: Contiene variable_global.
– .bss: Contiene variable_bss.
Finalmente, ldd binario te dirá qué bibliotecas compartidas (.so) se cargarán en memoria para que printf y otras funciones funcionen, permitiéndote verificar si el sistema tiene las dependencias correctas instaladas.
El error frecuente
Un error clásico ocurre al intentar usar una función marcada como static desde otro archivo objeto. Supongamos que intentas llamar a funcion_privada desde un archivo2.c.
/* En archivo2.c */
extern void funcion_privada(int); // Esto fallará en el enlazado
int main() {
funcion_privada(5);
return 0;
}
Al compilar ambos, el linker lanzará un error de undefined reference to 'funcion_privada'. Si ejecutas nm sobre el objeto de archivo2.o, verás que funcion_privada aparece como U (esperando una definición externa), pero al revisar el objeto original, nm mostrará la función como t (minúscula/local). El enlazador no puede “ver” los símbolos con ámbito local de otros archivos. AddressSanitizer no detectará esto porque no es un error de memoria, sino un error de visibilidad de símbolos en la etapa de enlazado.
N° 121