← Programación en C para Sistemas Operativos Bloque 3 · Funciones y organización · Tema 12 de 42

Tema 12

La pila de llamadas

Varias veces en el curso dejamos una explicación para después: por qué una variable local sin inicializar contenía el valor de otra función, por qué una recursión profunda terminaba con Segmentation fault, qué protegía el mensaje stack smashing detected. Todas tienen la misma respuesta: la pila, la zona de memoria donde cada llamada a una función guarda sus parámetros, sus variables y el lugar adonde tiene que volver. En este tema la miramos de cerca, con direcciones, ensamblador y gdb, y sacamos de ella la regla más importante del bloque: nunca se devuelve la dirección de una variable local.

1. Qué es la pila

Una pila es una estructura en la que el último elemento que entra es el primero que sale, como una pila de platos. Las llamadas a funciones siguen exactamente ese orden: si main llama a a y a llama a b, b termina antes que a, y a antes que main. Por eso los programas guardan la información de cada llamada en curso en una pila.

La pila de llamadas (call stack) es una región de la memoria de cada proceso, o de cada hilo, donde cada llamada a una función en curso ocupa un bloque, su marco de pila (stack frame), con sus variables locales, ciertos parámetros y la dirección a la que debe volver. Al llamar a una función se agrega un marco; al volver, se quita.

No es un concepto abstracto: es una zona concreta de la memoria del proceso, que el núcleo crea al arrancar el programa y que se ve en su mapa de memoria, como vimos en el tema 6:

terminal · la pila de un proceso, en su mapa de memoria
$ grep stack /proc/self/maps
7ffff8571000-7ffff8593000 rw-p 00000000 00:00 0                          [stack]

El procesador tiene un registro dedicado a la pila, %rsp en x86-64, el puntero de pila, que siempre apunta a su cima. Agregar algo a la pila es restar a %rsp y escribir; quitarlo es sumar. La pila empieza siendo pequeña, 136 KiB en esa línea (0x22000 bytes), y el núcleo la agranda automáticamente a medida que el programa la usa, hasta un límite que veremos en la sección 11.

2. La pila crece hacia abajo

Este programa encadena cuatro llamadas y muestra en cada una la dirección de su variable local:

crece.c
#include <stdio.h>

static void nivel3(void)
{
    int c = 3;
    printf("nivel3: &c = %p\n", (void *) &c);
}

static void nivel2(void)
{
    int b = 2;
    printf("nivel2: &b = %p\n", (void *) &b);
    nivel3();
}

static void nivel1(void)
{
    int a = 1;
    printf("nivel1: &a = %p\n", (void *) &a);
    nivel2();
}

int main(void)
{
    int m = 0;
    printf("main:   &m = %p\n", (void *) &m);
    nivel1();
    printf("main otra vez, llamando a nivel3 directamente:\n");
    nivel3();
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o crece crece.c && ./crece
main:   &m = 0x7fffd973b5f4
nivel1: &a = 0x7fffd973b5d4
nivel2: &b = 0x7fffd973b5b4
nivel3: &c = 0x7fffd973b594
main otra vez, llamando a nivel3 directamente:
nivel3: &c = 0x7fffd973b5d4

Cada nivel está 0x20 bytes, 32, por debajo del anterior: en x86-64, como en casi todos los procesadores, la pila crece hacia las direcciones menores. 32 bytes es lo que ocupa aquí cada marco, y la sección siguiente explica en qué. La última línea es la más reveladora: cuando main llama a nivel3 directamente, su variable c queda en …5d4, la misma dirección que antes tenía a, de nivel1. Ese lugar se liberó cuando nivel1 volvió, y la siguiente llamada lo ocupó. Es el mecanismo que hizo aparecer el 4242 de otra función en basura.c, en el tema 6.

3. El marco de pila

Cada marco contiene lo que la llamada necesita para ejecutarse y para volver. Con la convención que usa gcc sin optimizar, un marco tiene esta forma:

La pila durante la ejecución de nivel3: de arriba hacia abajo, el marco de main, con la dirección de retorno a la biblioteca, el rbp guardado y la variable m en 0x7fffd973b5f4; el marco de nivel1, con la dirección de retorno a main, el rbp de main y la variable a en 5d4; el marco de nivel2, con el retorno a nivel1, el rbp de nivel1 y b en 5b4; y el marco de nivel3, con el retorno a nivel2, el rbp de nivel2 y c en 594. El registro rsp apunta a la cima, al final del marco de nivel3. La pila crece hacia las direcciones menores retorno a la biblioteca de C rbp guardado m = 0 retorno a main rbp de main a = 1 retorno a nivel1 rbp de nivel1 b = 2 retorno a nivel2 rbp de nivel2 c = 3 …b5f4 …b5d4 …b5b4 …b594 marco de main marco de nivel1 marco de nivel2 marco de nivel3 crece hacia abajo %rsp
La pila mientras se ejecuta nivel3, simplificada. Arriba, las direcciones mayores; cada llamada agrega un marco debajo del anterior.
ContenidoQuién lo ponePara qué
Dirección de retornoLa instrucción call del llamador.Saber adónde volver: la instrucción siguiente a la llamada.
%rbp guardadoEl prólogo de la función, pushq %rbp.Restaurar el marco del llamador al volver. %rbp marca la base del marco actual.
Variables localesLa función, al restar a %rsp.Las variables automáticas y los parámetros que llegaron en registros, guardados para usarlos.
Registros guardadosLa función, si usa registros que debe preservar.Devolverle al llamador sus registros como estaban.
Argumentos de másEl llamador, antes del call.Los argumentos que no caben en registros (sección 4).
RellenoEl compilador.Mantener %rsp alineado a 16 bytes en cada llamada, como exige la convención.

Esto completa lo que vimos en el tema 4 con el ensamblador de cuadrado: pushq %rbp y movq %rsp, %rbp son el prólogo, que arma el marco; popq %rbp y ret son el epílogo, que lo desarma y salta a la dirección de retorno. En crece.c, cada marco ocupa 32 bytes: 8 de la dirección de retorno, 8 del %rbp guardado, y 16 para la variable local de 4 bytes, redondeados por la alineación.

4. Cómo viajan los argumentos

En el tema 4 vimos que el argumento de cuadrado llegaba en el registro %edi. La convención de llamada de Linux en x86-64, llamada System V AMD64, reparte los argumentos enteros y punteros en seis registros, en este orden: %rdi, %rsi, %rdx, %rcx, %r8 y %r9. Los de punto flotante usan otros registros, %xmm0 a %xmm7. ¿Y si hay más de seis? Este programa lo muestra:

marco.c
#include <stdio.h>

long sumar8(long a, long b, long c, long d, long e, long f, long g, long h)
{
    long total = a + b + c + d + e + f + g + h;
    return total;
}

int main(void)
{
    printf("%ld\n", sumar8(1, 2, 3, 4, 5, 6, 7, 8));
    return 0;
}
terminal · la llamada y la función, sin optimizar (abreviado)
$ gcc -std=c17 -O0 -S -fno-asynchronous-unwind-tables -o - marco.c
main:
	…
	pushq	$8                  # h y g: a la pila
	pushq	$7
	movl	$6, %r9d            # a … f: a registros
	movl	$5, %r8d
	movl	$4, %ecx
	movl	$3, %edx
	movl	$2, %esi
	movl	$1, %edi
	call	sumar8

sumar8:
	endbr64
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rdi, -24(%rbp)     # los seis registros, guardados en el marco
	movq	%rsi, -32(%rbp)
	…
	movq	%r9, -64(%rbp)
	…
	movq	16(%rbp), %rax      # g, en el marco del llamador
	…
	movq	24(%rbp), %rax      # h
	…
	popq	%rbp
	ret

Los seis primeros argumentos viajaron en registros, y los dos últimos, el 7 y el 8, fueron empujados a la pila por main antes de la llamada. Dentro de sumar8, sin optimizar, los seis registros se copian al marco, en desplazamientos negativos respecto de %rbp; los dos de la pila se leen en desplazamientos positivos, 16(%rbp) y 24(%rbp), por encima de la dirección de retorno, en 8(%rbp), y del %rbp guardado, en 0(%rbp). El resultado vuelve en %rax. Esta convención es la interfaz binaria del tema 1: por respetarla, código de cualquier compilador o lenguaje puede llamarse entre sí. Y explica una recomendación práctica: una función con más de seis parámetros es más lenta de llamar, además de más difícil de usar.

5. La pila vista con gdb

El depurador gdb, que el tema 39 estudia en detalle, puede detener un programa y mostrar su pila. Compilado con -g y detenido al entrar en nivel3:

terminal · gdb sobre crece, detenido en nivel3
$ gcc -std=c17 -g -O0 -o crece crece.c
$ gdb -q -batch -ex 'break nivel3' -ex run -ex bt -ex 'info frame' -ex 'frame 2' -ex 'info locals' ./crece
Breakpoint 1 at 0x1195: file crece.c, line 4.

Breakpoint 1, nivel3 () at crece.c:4
4	{
#0  nivel3 () at crece.c:4
#1  0x000055555555521f in nivel2 () at crece.c:13
#2  0x0000555555555278 in nivel1 () at crece.c:20
#3  0x00005555555552d1 in main () at crece.c:27
Stack level 0, frame at 0x7fffffffd780:
 rip = 0x555555555195 in nivel3 (crece.c:4); saved rip = 0x55555555521f
 called by frame at 0x7fffffffd7a0
 …
 Saved registers:
  rbp at 0x7fffffffd770, rip at 0x7fffffffd778
#2  0x0000555555555278 in nivel1 () at crece.c:20
20	    nivel2();
a = 1

bt (backtrace) lista los marcos de la pila, del más reciente, el 0, al más antiguo: es la cadena de llamadas que llevó hasta aquí, con el archivo y la línea de cada una. info frame muestra el marco actual por dentro: el saved rip, 0x55555555521f, es la dirección de retorno, y coincide con la que aparece en la línea #1 del backtrace; está guardada en 0x7fffffffd778, justo encima del %rbp guardado, en …d770, como en el diagrama. Y con frame 2 se puede subir a otro marco y ver sus variables: a sigue valiendo 1 mientras nivel1 espera que vuelva nivel2. Las direcciones son distintas de las de la ejecución anterior porque gdb desactiva ASLR para que las direcciones sean repetibles.

Así es como gdb produce un backtrace: recorre la pila de marco en marco, leyendo las direcciones de retorno. Y es la primera herramienta para entender un Segmentation fault: ver en qué función, llamada desde dónde, ocurrió.

6. La dirección de retorno

La dirección de retorno es solo un número guardado en la pila. gcc permite leerla, junto con la del marco, con dos funciones propias:

retorno.c
#include <stdio.h>

__attribute__((noinline))
static void quien_me_llamo(void)
{
    printf("volveré a       %p\n", __builtin_return_address(0));
    printf("mi marco está en %p\n", __builtin_frame_address(0));
}

int main(void)
{
    printf("main empieza en %p\n", (void *) main);
    quien_me_llamo();
    return 0;
}
terminal · y el código de main en el ejecutable
$ ./retorno
main empieza en 0x5874553c618c
volveré a       0x5874553c61b7
mi marco está en 0x7fff51175910
$ objdump -d --no-show-raw-insn retorno | grep -A1 'call.*quien'
    11b2:	call   1149 <quien_me_llamo>
    11b7:	mov    $0x0,%eax

La función va a volver a …61b7, que está 0x2b bytes después del comienzo de main. En el ejecutable, main empieza en 0x118c, y 0x118c + 0x2b es 0x11b7: exactamente la instrucción que sigue al call, la que hace return 0. Eso es lo que guarda call en la pila, y lo que ret usa para saltar. La consecuencia es grave: si un programa escribe por error sobre esa posición de la pila, cambia el lugar adonde volverá la función. La sección 10 lo muestra.

7. Las variables automáticas

Ahora se entiende el nombre: las variables locales tienen duración automática, como vimos en el tema 6, porque su memoria se crea y se libera sola, como parte del marco de la llamada. Tres consecuencias:

Lo mismo vale para los hilos, en el curso Procesos e Hilos: cada hilo tiene su propia pila, de modo que las variables automáticas nunca se comparten entre hilos, a diferencia de las globales y las static.

8. No devolver la dirección de una local

De la segunda consecuencia sale el error más importante de este tema. Una función que devuelve la dirección de una de sus variables locales entrega una dirección de un marco que, en el momento en que el llamador la recibe, ya no existe. A un puntero así se lo llama colgante (dangling):

colgante.c
#include <stdio.h>

/* devuelve la dirección de una variable local: un error */
static int *crear_contador(void)
{
    int contador = 42;
    int *p = &contador;
    return p;                         /* la variable muere al volver */
}

static void otra_funcion(void)
{
    int pisar[4] = {1111, 2222, 3333, 4444};
    printf("  otra_funcion usa la pila: %d\n", pisar[0]);
}

int main(void)
{
    int *p = crear_contador();

    printf("primera lectura:  *p = %d\n", *p);
    otra_funcion();
    printf("segunda lectura:  *p = %d\n", *p);
    return 0;
}
terminal · sin optimizar, con -O2 y con AddressSanitizer
$ gcc -std=c17 -Wall -Wextra -o colgante colgante.c && ./colgante
primera lectura:  *p = 42
  otra_funcion usa la pila: 1111
segunda lectura:  *p = 4444
$ gcc -std=c17 -Wall -Wextra -O2 -o colgante colgante.c 2>/dev/null && ./colgante
primera lectura:  *p = 0
  otra_funcion usa la pila: 1111
segunda lectura:  *p = 0
$ gcc -std=c17 -g -fsanitize=address -o colgante colgante.c
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./colgante
=================================================================
==502==ERROR: AddressSanitizer: stack-use-after-return on address 0x73621f9d0020 …
READ of size 4 at 0x73621f9d0020 thread T0
    #0 0x598f3ea5d4b3 in main /home/ana/c/tema12/colgante.c:21
…
Address 0x73621f9d0020 is located in stack of thread T0 at offset 32 in frame
    #0 0x598f3ea5d258 in crear_contador /home/ana/c/tema12/colgante.c:5

Sin optimizar, la primera lectura parece funcionar: el 42 todavía estaba en la memoria del marco muerto. Pero en cuanto otra función ocupó ese lugar de la pila, el valor cambió a 4444, un elemento del arreglo de otra_funcion. Es lo peor que puede hacer un error: funcionar en las pruebas sencillas y fallar después, lejos de su causa. Con -O2, el compilador entendió que la lectura usaba una variable ya muerta y la trató como sin inicializar, con un 0. AddressSanitizer, con la opción detect_stack_use_after_return, lo detecta en la primera lectura e informa exactamente qué marco era el dueño de esa memoria.

Si la dirección se devuelve directamente, sin la variable intermedia, el compilador lo ve, y su reacción es llamativa:

terminal · directo.c: return &contador;
$ gcc -std=c17 -Wall -Wextra -o directo directo.c
directo.c: In function ‘crear_contador’:
directo.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
    6 |     return &contador;                 /* el compilador lo detecta */
      |            ^~~~~~~~~
$ ./directo
p = (nil)
Segmentation fault

Además de advertir, gcc reemplazó la dirección devuelta por NULL, y la lectura terminó el programa en el acto. Es deliberado: un fallo inmediato es mucho mejor que un valor que cambia más tarde. Pero solo ocurre cuando el compilador puede ver el problema; con la variable intermedia de colgante.c, no hubo advertencia. La regla tiene que estar en la cabeza del programador: una función nunca devuelve la dirección de una variable automática, ni la guarda en un lugar que sobreviva a la llamada, como una variable global.

9. Qué hacer en cambio

A veces una función tiene que entregar un dato que no cabe en el valor de retorno, como un texto. Hay tres soluciones correctas, con ventajas distintas, y las tres aparecen en la biblioteca de C:

nombres.c
#include <stdio.h>

/* versión 1: buffer estático, compartido por todas las llamadas */
static const char *nombre_senal_estatico(int n)
{
    static char buffer[32];

    snprintf(buffer, sizeof buffer, "señal %d", n);
    return buffer;
}

/* versión 2: el llamador provee el buffer */
static char *nombre_senal(int n, char *destino, size_t tam)
{
    snprintf(destino, tam, "señal %d", n);
    return destino;
}

int main(void)
{
    printf("estático: %s y %s\n", nombre_senal_estatico(2), nombre_senal_estatico(15));

    char a[32], b[32];
    printf("del llamador: %s y %s\n", nombre_senal(2, a, sizeof a), nombre_senal(15, b, sizeof b));
    return 0;
}
terminal
$ ./nombres
estático: señal 2 y señal 2
del llamador: señal 2 y señal 15

Devolver la dirección de una variable static es correcto, porque esa variable no está en la pila y vive todo el programa. Pero todas las llamadas comparten el mismo buffer: en el primer printf, las dos llamadas se ejecutaron antes de imprimir, y la segunda pisó el texto de la primera, de modo que se ve «señal 2» dos veces (cuál de las dos gana depende del orden de evaluación del tema 7). Con el buffer provisto por el llamador, cada llamada escribe en su propio lugar.

SoluciónVentajaDesventajaEn la biblioteca
Buffer staticCómoda de usar.Una llamada pisa la anterior; no es segura con hilos.strerror, asctime, getpwnam
Buffer del llamadorSin memoria compartida ni pedida; el llamador controla el tamaño.El llamador tiene que saber cuánto espacio dar.fgets, snprintf, strerror_r, getcwd
Memoria dinámicaTamaño ilimitado y sin compartir.El llamador tiene que liberarla con free.getline, strdup

La biblioteca muestra la historia de este problema: muchas funciones antiguas devuelven un buffer estático, y POSIX agregó después versiones terminadas en _r, de «reentrante», que reciben el buffer del llamador, como strerror_r o el strtok_r que mencionamos en el tema 6. En código nuevo, la opción preferida es el buffer del llamador; la memoria dinámica es el bloque 7.

10. Desbordar un buffer en la pila

Como las variables locales y la dirección de retorno están juntas en la pila, escribir más allá del final de un arreglo local puede llegar hasta la dirección de retorno. Este programa copia un texto en un arreglo de 16 bytes sin comprobar su largo:

pisar.c
#include <stdio.h>
#include <string.h>

static void saludar(const char *nombre)
{
    char buffer[16];

    strcpy(buffer, nombre);          /* no comprueba el tamaño */
    printf("hola, %s\n", buffer);
}

int main(int argc, char *argv[])
{
    if (argc > 1)
        saludar(argv[1]);
    printf("saludar volvió bien\n");
    return 0;
}
terminal · con el protector de pila de Ubuntu, sin él, y en gdb
$ ./pisar ana
hola, ana
saludar volvió bien
$ ./pisar 'un nombre bastante más largo que dieciséis'
*** stack smashing detected ***: terminated
Aborted
$ gcc -std=c17 -g -O0 -fno-stack-protector -o pisar_np pisar.c
$ ./pisar_np 'un nombre bastante más largo que dieciséis'
hola, un nombre bastante más largo que dieciséis
Segmentation fault
$ gdb -q -batch -ex run -ex bt --args ./pisar_np AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
0x00005555555551c9 in saludar (nombre=0x7fffffffdb59 'A' <repeats 48 times>) at pisar.c:10
10	}
#0  0x00005555555551c9 in saludar (nombre=0x7fffffffdb59 'A' <repeats 48 times>) at pisar.c:10
#1  0x4141414141414141 in ?? ()
#2  0x4141414141414141 in ?? ()

strcpy copió los 47 bytes del texto, más su \0, en un arreglo de 16. Los bytes de más pisaron lo que había encima en la pila: el %rbp guardado y la dirección de retorno. Sin protección, el programa llegó al ret del final de saludar, en la línea 10, y la dirección de retorno ya no era válida. El backtrace de gdb lo muestra sin ambigüedad: la dirección de retorno es 0x4141414141414141, que son ocho letras A (0x41 en ASCII), copiadas del argumento. Quien controla el texto controla adónde salta el programa. Ese es el desbordamiento de buffer en la pila, la técnica de ataque más clásica contra programas en C.

El mensaje stack smashing detected de la primera ejecución es la defensa que vimos en el tema 4, -fstack-protector-strong, que Ubuntu enciende por omisión. El compilador coloca un valor al azar, el canario, entre los arreglos locales y la dirección de retorno, y antes de volver comprueba que siga intacto. Un desbordamiento que llega a la dirección de retorno pisa primero el canario, y el programa aborta antes de saltar a ningún lado. Con ASLR y las páginas de la pila sin permiso de ejecución, que muestra la rw-p de /proc/self/maps, forma la defensa en capas de los sistemas actuales. Ninguna reemplaza la solución real, que es no desbordar: snprintf en lugar de strcpy, como veremos en los temas 21 y 22.

11. El tamaño de la pila

La pila tiene un tamaño máximo, que en Linux se controla con ulimit -s y es de 8 MiB por omisión, como vimos en el tema 11 con la recursión profunda. No hace falta una recursión para agotarla: alcanza con un arreglo local grande. Este programa declara un arreglo de tamaño variable, cuyo tamaño se decide al ejecutar:

grande.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void procesar(size_t mib)
{
    char datos[mib * 1024 * 1024];      /* arreglo de tamaño variable, en la pila */

    memset(datos, 0, sizeof datos);
    printf("procesados %zu MiB en la pila\n", mib);
}

int main(int argc, char *argv[])
{
    size_t mib = argc > 1 ? (size_t) atoi(argv[1]) : 1;

    procesar(mib);
    return 0;
}
terminal
$ ulimit -s
8192
$ ./grande 1
procesados 1 MiB en la pila
$ ./grande 7
procesados 7 MiB en la pila
$ ./grande 9
Segmentation fault

Con 9 MiB, el marco de procesar no cabe en la pila, y el primer acceso más allá del límite termina el programa. No hay un mensaje de error, ni una forma de detectarlo desde el programa: datos no puede «fallar» como lo haría malloc, que devuelve NULL. Por eso los arreglos de tamaño variable cuyo tamaño viene de los datos son peligrosos, y muchos proyectos, incluido el núcleo Linux, los prohíben. La regla práctica: en la pila, solo datos pequeños y de tamaño conocido, de unos pocos KiB; lo grande o de tamaño variable va en memoria dinámica, en el bloque 7.

Hay contextos con pilas mucho más chicas. Cada hilo tiene la suya, de 8 MiB por omisión en Linux pero configurable, y en programas con miles de hilos se reduce. Y la pila del núcleo Linux, la que usa el sistema operativo al atender una llamada al sistema, es de solo 16 KiB: en el código del núcleo, un arreglo local de unos pocos KiB ya es un problema.

12. La optimización y la pila

Todo lo que vimos sobre marcos corresponde a código sin optimizar. Con -O2, el compilador trata la pila con mucha más libertad:

terminal · sumar8, de marco.c, con -O2
$ gcc -std=c17 -O2 -S -fno-asynchronous-unwind-tables -o - marco.c | sed -n '/^sumar8:/,/ret/p'
sumar8:
	endbr64
	addq	%rsi, %rdi
	addq	%rdx, %rdi
	addq	%rcx, %rdi
	addq	%r8, %rdi
	leaq	(%rdi,%r9), %rax
	addq	8(%rsp), %rax
	addq	16(%rsp), %rax
	ret

Ya no hay pushq %rbp ni marco: los argumentos se suman directamente en los registros, y los dos que llegaron por la pila se leen relativos a %rsp. Omitir el puntero de marco libera un registro y ahorra instrucciones, y es lo habitual con optimización. Por eso los depuradores necesitan la información de -g para reconstruir un backtrace de código optimizado, y por eso se depura mejor con -O0 o -Og, como dijimos en el tema 4. La expansión en línea del tema 11 va todavía más lejos: una función expandida no tiene marco propio, y no aparece en el backtrace.

La optimización más interesante para la recursión es la llamada de cola. Cuando una función termina devolviendo directamente el resultado de otra llamada, return f(…), su marco ya no hace falta, y el compilador puede reemplazar la llamada por un salto:

cola.c
#include <stdio.h>
#include <stdlib.h>

/* suma de 1 a n, recursiva; la llamada es lo último que hace la función */
static long sumar_hasta(long n, long acumulado)
{
    if (n == 0)
        return acumulado;
    return sumar_hasta(n - 1, acumulado + n);
}

int main(int argc, char *argv[])
{
    long n = argc > 1 ? atol(argv[1]) : 10;

    printf("suma de 1 a %ld = %ld\n", n, sumar_hasta(n, 0));
    return 0;
}
terminal · diez millones de niveles, sin optimizar y con -O2
$ gcc -std=c17 -Wall -Wextra -O0 -o cola cola.c && ./cola 10000000
Segmentation fault
$ gcc -std=c17 -Wall -Wextra -O2 -o cola cola.c && ./cola 10000000
suma de 1 a 10000000 = 50000005000000

Sin optimizar, diez millones de marcos agotan la pila. Con -O2, el compilador convirtió la recursión en un bucle, sin ninguna instrucción call, y la pila no crece. Es útil saberlo, pero no conviene depender de ello: el estándar de C no garantiza esta optimización, y el mismo programa compilado sin optimizar se cae. Si la profundidad puede ser grande, lo correcto es escribir el bucle.

13. Por qué la pila es tan rápida

Con todos estos peligros, ¿por qué se usa tanto la pila? Porque es, por lejos, la forma más rápida de obtener memoria. Reservar el espacio de todas las variables locales de una función es una sola instrucción, subq $N, %rsp, y liberarlo es otra, que además viene incluida en el epílogo. No hay que buscar un hueco libre, ni llevar la cuenta de qué está ocupado, ni devolver nada explícitamente. Y como la cima de la pila se usa constantemente, suele estar en la memoria caché del procesador.

La memoria dinámica del bloque 7, en cambio, la administra malloc, que tiene que encontrar un bloque libre del tamaño pedido, registrarlo, y más tarde recibirlo de vuelta con free. Cuesta decenas o cientos de veces más, y exige que el programador recuerde liberarla. Las dos tienen su lugar: la pila, para datos pequeños que viven lo que dura una llamada; la memoria dinámica, para datos grandes, de tamaño variable o que deben sobrevivir a la función que los crea.

14. Errores frecuentes

ErrorQué pasaCorrección
Devolver la dirección de una variable localPuntero colgante: el valor cambia después, o el programa se cae.Buffer del llamador, static o memoria dinámica.
Guardar en una global la dirección de una localEl mismo problema, sin ninguna advertencia.Copiar el valor, no la dirección.
Usar dos veces seguidas una función con buffer staticLa segunda llamada pisa el resultado de la primera.Copiar el resultado antes, o usar la versión _r.
Copiar sin límite en un arreglo local: strcpy, scanf("%s")Desbordamiento en la pila: stack smashing detected, o algo peor.snprintf, fgets, tamaños comprobados.
Arreglos locales grandesSegmentation fault al superar el límite de la pila.Memoria dinámica para lo que pase de unos pocos KiB.
Arreglos de tamaño variable con tamaños de los datosUn tamaño grande agota la pila sin forma de detectarlo.Memoria dinámica, que informa la falla.
Recursión cuya profundidad depende de los datosDesbordamiento de pila con entradas grandes.Un bucle, o un límite de profundidad.
Depender de la optimización de llamadas de colaFunciona con -O2 y se cae sin optimizar.Escribir el bucle.
Desactivar el protector de pilaLos desbordamientos llegan a la dirección de retorno.Dejar las protecciones por omisión.

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué se usa una pila para las llamadas a funciones?

Porque las llamadas terminan en el orden inverso al que empezaron: la última función llamada es la primera en volver. Ese es exactamente el orden de una pila, en la que el último elemento agregado es el primero que se quita. Cada llamada agrega su marco y, al volver, lo quita.

2. En crece.c, ¿por qué nivel3, llamada desde main, tiene su variable en la misma dirección que antes tenía a?

Porque cuando nivel1 volvió, su marco se liberó, y la siguiente llamada desde main se ubicó en el mismo lugar de la pila, justo debajo del marco de main. El marco de nivel3 ocupó el espacio que había ocupado el de nivel1, y su variable quedó en la misma posición.

3. ¿Qué contiene un marco de pila, y quién pone cada cosa?

La dirección de retorno, que pone la instrucción call del llamador; el %rbp del llamador, que guarda el prólogo de la función; las variables locales, para las que la función reserva espacio restando a %rsp; y, según el caso, registros guardados, argumentos que no cupieron en registros (que pone el llamador) y relleno para la alineación.

4. En sumar8, ¿por dónde llegan los argumentos g y h, y por qué?

Por la pila: main los empuja con pushq antes del call, y sumar8 los lee en 16(%rbp) y 24(%rbp). La convención de llamada de Linux en x86-64 solo usa seis registros para argumentos enteros, y estos son el séptimo y el octavo.

5. ¿Qué es la dirección de retorno, y por qué __builtin_return_address(0) dio main + 0x2b?

Es la dirección de la instrucción a la que debe volver la función al terminar: la que sigue a la instrucción call que la llamó. call la guarda en la pila y ret salta a ella. En retorno.c, el call a quien_me_llamo ocupa hasta main + 0x2a, y la instrucción siguiente está en main + 0x2b.

6. En colgante.c, ¿por qué la primera lectura mostró 42 y la segunda 4444?

Porque p apunta a la variable contador de un marco que ya no existe. Inmediatamente después de volver, nadie había reutilizado ese lugar de la pila, y el 42 seguía ahí. Al llamar a otra_funcion, su marco ocupó el mismo lugar, y su arreglo escribió 4444 donde había estado contador. Leer a través de p es comportamiento indefinido en ambos casos.

7. ¿Por qué directo.c termina con Segmentation fault en la primera lectura?

Porque gcc detectó que la función devolvía la dirección de una variable local, lo advirtió con -Wreturn-local-addr, y reemplazó el valor devuelto por NULL, para que el error se manifieste de inmediato. Leer a través de NULL termina el programa.

8. ¿Por qué nombre_senal_estatico(2) y nombre_senal_estatico(15) en el mismo printf muestran el mismo texto?

Porque las dos llamadas devuelven la dirección del mismo buffer static, y las dos se ejecutan antes de que printf lea los textos. La segunda llamada que se ejecutó sobrescribió el resultado de la primera, y printf recibió dos veces la misma dirección con el mismo contenido.

9. En pisar_np, ¿por qué el backtrace muestra 0x4141414141414141?

Porque strcpy copió el argumento, una larga fila de letras A, más allá del final del arreglo de 16 bytes, y pisó el %rbp guardado y la dirección de retorno, que están encima en la pila. 0x41 es el código ASCII de A, de modo que la dirección de retorno quedó formada por ocho A. Al ejecutar ret, el programa intentó saltar ahí.

10. ¿Cómo detecta el protector de pila un desbordamiento?

El compilador coloca un valor aleatorio, el canario, entre las variables locales y la dirección de retorno al entrar a la función, y comprueba que no haya cambiado antes de volver. Un desbordamiento que avanza hacia la dirección de retorno tiene que pisar primero el canario; si cambió, el programa aborta con stack smashing detected en lugar de saltar a una dirección corrupta.

11. ¿Por qué ./grande 9 falla, y por qué no hay forma de detectarlo desde el programa?

Porque un arreglo de 9 MiB en la pila supera el límite de 8 MiB, y al acceder más allá del límite el núcleo termina el proceso con SIGSEGV. Declarar una variable local no puede «fallar» con un valor de retorno como malloc: el espacio se reserva moviendo %rsp, y el problema solo aparece al tocar memoria que no existe.

12. ¿Por qué cola.c con diez millones de niveles funciona con -O2 y falla sin optimizar?

Porque la llamada recursiva es lo último que hace la función, una llamada de cola, y con -O2 el compilador la reemplaza por un salto, convirtiendo la recursión en un bucle que no agrega marcos. Sin optimizar, cada nivel agrega un marco, y diez millones de marcos superan el límite de la pila.

Para practicar

  1. Agrega a crece.c una variable char nombre[100] en nivel2 y vuelve a ejecutarlo. ¿Cuánto cambió la distancia entre los marcos? Explica la cifra.
  2. Ejecuta crece en gdb, detente en nivel3 y usa x/8gx $sp para ver los primeros 64 bytes de la pila. Identifica la dirección de retorno y el %rbp guardado.
  3. Escribe una función con diez parámetros long y mira su ensamblador con -O0. ¿En qué desplazamientos de %rbp lee el noveno y el décimo?
  4. Modifica colgante.c para que crear_contador use una variable static, y comprueba que las dos lecturas den 42. ¿Qué pasa si llamas dos veces a crear_contador e incrementas el valor a través del primer puntero?
  5. Reescribe saludar en pisar.c con snprintf para que nunca desborde, y pruébala con el texto largo, con y sin -fno-stack-protector.
  6. Averigua con ./grande el tamaño máximo que soporta tu pila, y repite con ulimit -s 16384. Luego reescribe procesar con malloc y free y comprueba que funciona con 100 MiB.
  7. Escribe la versión recursiva de sumar_hasta que no sea de cola, return n + sumar_hasta(n - 1), y comprueba si -O2 la salva también con diez millones de niveles. Mira el ensamblador para explicar el resultado.