Prerrequisitos y Entorno de Ejecución Requerido¶
Para compilar y verificar las soluciones de este módulo bajo el estándar C11 estricto de cátedra, se requiere:
Compilador C11: GCC 9+ o Clang 11+ configurado con flags
-Wall -Wextra -Werror -pedantic -std=c11.Entorno POSIX: Linux o WSL con utilidades estándar y herramientas de análisis dinámico.
Herramientas de Verificación: Valgrind (memcheck) y AddressSanitizer (
-fsanitize=address,undefined) para garantizar cero fugas de memoria y detectar punteros dangling o doble liberación (double free).Conocimientos Previos: Arquitectura de memoria de procesos (Stack, Heap, BSS, Data, Text), enteros de puntero
uintptr_t, funcionesmalloc/calloc/realloc/freey patrones de diseño seguro.
Objetivos Pedagógicos y Competencias (Taxonomía de Bloom)¶
Nivel 2 (Comprensión): Comprender la topología de memoria virtual, la dirección de crecimiento de Stack y Heap, y el ciclo de vida de variables.
Nivel 3 (Aplicación): Implementar reservas dinámicas, redimensionamiento defensivo con
reallocy liberación simétrica en C11.Nivel 4 (Análisis): Diagnosticar fragmentación, fugas de memoria, punteros dangling y mitigar errores mediante sanitizers y aserciones.
Andamiaje Progresivo: Ejercicios andamiados con contratos formales (precondiciones/postcondiciones), tablas de vectores de prueba y suites ejecutables con
assert().
Capítulos de Apunte Correspondientes¶
Modelo de Memoria¶
Ejercicio 2.05b.01 - Comparación Relativa de Segmentos de Memoria ⭐⭐☆☆☆¶
```c #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <stdint.h> #include <assert.h> bool verificar_layout_stack_heap(void) { int var_stack = 42; int *ptr_heap = (int *)malloc(sizeof(int)); if (ptr_heap == NULL) { return false; } *ptr_heap = 99; uintptr_t dir_stack = (uintptr_t)&var_stack; uintptr_t dir_heap = (uintptr_t)ptr_heap; /* En Linux x86_64, el stack está en el extremo superior del espacio de direcciones */ bool stack_mayor_que_heap = (dir_stack > dir_heap); free(ptr_heap); return stack_mayor_que_heap; } int main(void) { assert(verificar_layout_stack_heap()); return 0; } ```
(ej_b2_c05b_02)=
### Ejercicio 2.05b.02 - Dirección de Crecimiento del Stack ⭐⭐☆☆☆
:::{exercise}
:label: ej_b2_c05b_02_crecimiento_stack
:enumerator: mem-layout-2
Implementá una función que detecte empíricamente si la pila (*stack*) crece hacia direcciones descendentes o ascendentes mediante dos marcos de llamada anidados.
**Nivel de Bloom:** Nivel 3 (Aplicación) y Nivel 4 (Análisis).
**Conceptos requeridos:** Pila de llamadas en memoria (*call stack*), paso por referencia, comparación de punteros y marcos de activación.
**Techo conceptual:** Prohibido el uso de optimizaciones de compilador agresivas que eliminen el marco de pila (*tail call optimization*).
#### Contrato de la Función
- **Firma:** `bool stack_crece_hacia_abajo(void);`
- **Precondiciones:** Ninguna.
- **Postcondiciones:** Retorna `true` si la variable local del marco anidado reside en una dirección menor que la del marco llamador.
#### Tabla de Vectores de Prueba Obligatorios
| Arquitectura Típica | Comportamiento | Retorno Esperado | Justificación Técnica |
| :--- | :--- | :--- | :--- |
| **x86 / x86_64 / ARM64** | Marco anidado en dirección menor | `true` | Modelo de stack descendente estándar |
(ej_b2_c05b_03)=
### Ejercicio 2.05b.03 - b.3 - Tamaño de Página del Sistema ⭐⭐☆☆☆
Escribir un programa que use `sysconf(_SC_PAGESIZE)` (en sistemas POSIX) para
obtener el tamaño de página del sistema. Luego, alojar varios bloques de memoria
de diferentes tamaños y analizar cuánta memoria física se está usando realmente.
:::{note} Memoria Virtual
Recordá que el sistema operativo asigna memoria en múltiplos del tamaño de
página, típicamente 4 KB. Una asignación de 100 bytes puede consumir 4 KB de
memoria física.
Patrones de Gestión de Memoria¶
Ejercicio 2.05b.04 - Ciclo de Vida Consistente de Producto ⭐⭐☆☆☆¶
(ej_b2_c05b_05)=
### Ejercicio 2.05b.05 - b.5 - Manejo de Errores en Cadena ⭐⭐⭐☆☆
Escribir una función que aloje múltiples estructuras anidadas:
```{code-block} c
:linenos:
struct persona_t
{
char *nombre;
char *apellido;
struct direccion_t *direccion;
};
struct direccion_t
{
char *calle;
char *ciudad;
};
```
<!-- {code-block} c -->
La función debe manejar correctamente fallos de `malloc` en cualquier paso,
liberando todo lo previamente asignado antes de retornar `NULL`.
(ej_b2_c05b_06)=
### Ejercicio 2.05b.06 - b.6 - Pool de Memoria Simple ⭐☆☆☆☆
Implementar un pool de memoria básico que preasigne un bloque grande y
distribuya bloques pequeños de tamaño fijo desde él.
```{code-block} c
:linenos:
typedef struct
{
void *bloque_base;
size_t tamano_total;
size_t tamano_bloque;
size_t bloques_usados;
bool *mapa_uso;
} pool_t;
pool_t *pool_crear(size_t n_bloques, size_t tamano_bloque);
void *pool_alojar(pool_t *pool);
void pool_liberar(pool_t *pool, void *ptr);
void pool_destruir(pool_t *pool);
```
<!-- {code-block} c -->
**Ventaja:** Reduce la fragmentación y overhead de múltiples `malloc`/`free`.
## Errores Comunes y Detección
(ej_b2_c05b_07)=
### Ejercicio 2.05b.07 - b.7 - Double Free ⭐⭐☆☆☆
Escribir un programa que deliberadamente cometa el error de double free:
```{code-block} c
:linenos:
int main()
{
int *ptr = malloc(sizeof(int) * 10);
*ptr = 42;
free(ptr);
free(ptr); // ERROR: double free
return 0;
}
```
<!-- {code-block} c -->
Compilar con AddressSanitizer (`gcc -fsanitize=address`) y observar el error
detectado.
(ej_b2_c05b_08)=
### Ejercicio 2.05b.08 - b.8 - Use After Free ⭐⭐☆☆☆
Crear un programa que cometa use-after-free:
```{code-block} c
:linenos:
int main()
{
int *arr = malloc(sizeof(int) * 5);
arr[0] = 100;
free(arr);
printf("%d\n", arr[0]); // ERROR: use after free
return 0;
}
```
<!-- {code-block} c -->
Ejecutar con Valgrind: `valgrind --leak-check=full ./programa`
(ej_b2_c05b_09)=
### Ejercicio 2.05b.09 - b.9 - Memory Leak Complejo ⭐⭐⭐⭐☆
Crear un programa con un leak no trivial:
```{code-block} c
:linenos:
struct nodo_t
{
int dato;
struct nodo_t *siguiente;
};
void agregar_nodo(struct nodo_t **cabeza, int valor)
{
struct nodo_t *nuevo = malloc(sizeof(struct nodo_t));
nuevo->dato = valor;
nuevo->siguiente = *cabeza;
*cabeza = nuevo;
}
int main()
{
struct nodo_t *lista = NULL;
for (int i = 0; i < 100; i++)
{
agregar_nodo(&lista, i);
}
// FALTA: liberar todos los nodos
return 0;
}
```
<!-- {code-block} c -->
Detectar con Valgrind el leak de 100 nodos. Luego implementar una función
`liberar_lista` que corrija el problema.
(ej_b2_c05b_10)=
### Ejercicio 2.05b.10 - b.10 - Buffer Overflow en Heap ⭐⭐☆☆☆
Demostrar un buffer overflow en memoria dinámica:
```{code-block} c
:linenos:
int main()
{
char *buffer = malloc(10);
strcpy(buffer, "Esta cadena es mucho más larga que 10 bytes"); // OVERFLOW
printf("%s\n", buffer);
free(buffer);
return 0;
}
```
<!-- {code-block} c -->
Compilar con `-fsanitize=address` y observar la detección del
heap-buffer-overflow.
## Estrategias de Realocación
(ej_b2_c05b_11)=
### Ejercicio 2.05b.11 - b.11 - Vector con Estrategia de Crecimiento ⭐⭐☆☆☆
Implementar un vector dinámico con diferentes estrategias de crecimiento:
- **Lineal**: incrementar en un valor fijo (ej. +10)
- **Geométrico**: duplicar la capacidad (factor 2)
- **Fibonacci**: crecer según la secuencia de Fibonacci
Comparar el número de `realloc` realizados al insertar 10,000 elementos.
(ej_b2_c05b_12)=
### Ejercicio 2.05b.12 - b.12 - Shrinking Inteligente ⭐⭐☆☆☆
Implementar un vector que no solo crece, sino que también reduce su capacidad
cuando el tamaño cae por debajo de 1/4 de la capacidad. Esto evita el overhead
de memoria manteniendo estabilidad.
```{code-block} c
:linenos:
void vector_pop(vector_t *v)
{
if (v->size == 0)
return;
v->size--;
// Si size < capacity/4 y capacity > MIN_CAPACITY, reducir a la mitad
if (v->size > 0 && v->size < v->capacity / 4 && v->capacity > 16)
{
v->capacity /= 2;
v->datos = realloc(v->datos, v->capacity * sizeof(int));
}
}
```
<!-- {code-block} c -->
(ej_b2_c05b_13)=
### Ejercicio 2.05b.13 - b.13 - Realloc Fallido ⭐⭐☆☆☆
Escribir una función que maneje correctamente el fallo de `realloc`:
```{code-block} c
:linenos:
bool vector_push(vector_t *v, int dato)
{
if (v->size == v->capacity)
{
size_t nueva_capacidad = v->capacity * 2;
int *nuevo_bloque = realloc(v->datos, nueva_capacidad * sizeof(int));
if (nuevo_bloque == NULL)
{
return false; // Fallo, pero v->datos sigue válido
}
v->datos = nuevo_bloque;
v->capacity = nueva_capacidad;
}
v->datos[v->size++] = dato;
return true;
}
```
<!-- {code-block} c -->
:::{important} Manejo de `realloc`
Nunca asignés directamente `ptr = realloc(ptr, size)`. Si falla, perdés el
puntero original y tenés un leak. Usá un puntero temporal como en el ejemplo.
Análisis de Rendimiento y Fragmentación¶
Ejercicio 2.05b.14 - b.14 - Fragmentación Externa ⭐⭐☆☆☆¶
Escribir un programa que demuestre fragmentación externa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33#define N 1000 int main() { void *ptrs[N]; // Alojar muchos bloques pequeños for (int i = 0; i < N; i++) { ptrs[i] = malloc(64); } // Liberar bloques alternados for (int i = 0; i < N; i += 2) { free(ptrs[i]); ptrs[i] = NULL; } // Intentar alojar un bloque grande void *grande = malloc(64 * (N / 2)); if (grande == NULL) { printf("No se pudo alojar bloque grande debido a fragmentación\n"); } else { printf("Bloque grande asignado exitosamente\n"); free(grande); } // Liberar bloques restantes for (int i = 1; i < N; i += 2) { free(ptrs[i]); } return 0; }
Ejercicio 2.05b.15 - b.15 - Medición de Overhead de malloc ⭐⭐☆☆☆¶
Escribir un programa que mida el overhead de malloc comparando el tamaño
solicitado vs. el espacio realmente consumido (usando herramientas como
malloc_usable_size en glibc):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15#include <malloc.h> int main() { size_t tamaños[] = {1, 8, 16, 32, 64, 100, 128, 256, 512, 1024}; for (size_t i = 0; i < sizeof(tamaños) / sizeof(tamaños[0]); i++) { void *ptr = malloc(tamaños[i]); size_t real = malloc_usable_size(ptr); printf( "Solicitado: %5zu bytes -> Real: %5zu bytes (%.1f%% overhead)\n", tamaños[i], real, 100.0 * (real - tamaños[i]) / tamaños[i]); free(ptr); } return 0; }
Ejercicio 2.05b.16 - b.16 - Cache Locality ⭐⭐☆☆☆¶
Comparar el rendimiento de acceso secuencial vs. aleatorio en un arreglo grande:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24#define SIZE (100 * 1024 * 1024) // 100M enteros int main() { int *arr = malloc(SIZE * sizeof(int)); // Secuencial clock_t inicio = clock(); for (size_t i = 0; i < SIZE; i++) { arr[i] = i; } clock_t fin = clock(); printf("Secuencial: %.3f ms\n", 1000.0 * (fin - inicio) / CLOCKS_PER_SEC); // Aleatorio inicio = clock(); for (size_t i = 0; i < SIZE; i++) { size_t idx = rand() % SIZE; arr[idx] = i; } fin = clock(); printf("Aleatorio: %.3f ms\n", 1000.0 * (fin - inicio) / CLOCKS_PER_SEC); free(arr); return 0; }
Observación esperada: El acceso secuencial será significativamente más rápido debido a mejor locality y uso del caché.
Alineación de Memoria¶
Ejercicio 2.05b.17 - b.17 - Visualizar Alineación ⭐⭐☆☆☆¶
Escribir un programa que demuestre la alineación de memoria en structs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26struct sin_padding { char a; // 1 byte int b; // 4 bytes char c; // 1 byte double d; // 8 bytes }; struct con_padding { char a; // 1 byte + 3 padding int b; // 4 bytes char c; // 1 byte + 7 padding double d; // 8 bytes }; int main() { printf("Tamaño sin optimizar: %zu bytes\n", sizeof(struct sin_padding)); printf("Tamaño con padding: %zu bytes\n", sizeof(struct con_padding)); // Imprimir offsets de cada campo struct con_padding s; printf("Offset de 'a': %zu\n", offsetof(struct con_padding, a)); printf("Offset de 'b': %zu\n", offsetof(struct con_padding, b)); printf("Offset de 'c': %zu\n", offsetof(struct con_padding, c)); printf("Offset de 'd': %zu\n", offsetof(struct con_padding, d)); return 0; }
Ejercicio 2.05b.18 - b.18 - Alineación Manual ⭐⭐☆☆☆¶
Implementar una función que aloje memoria alineada a un límite específico (ej. 64 bytes para optimización de caché):
1 2 3 4 5 6 7 8 9 10void *malloc_alineado(size_t size, size_t alineacion) { void *ptr = NULL; // posix_memalign requiere que alineacion sea potencia de 2 if (posix_memalign(&ptr, alineacion, size) != 0) { return NULL; } return ptr; }
Integración con Herramientas¶
Ejercicio 2.05b.19 - b.19 - Script de Análisis con Valgrind ⭐⭐☆☆☆¶
Crear un Makefile que incluya targets para análisis automático:
1 2 3 4 5 6 7 8 9 10 11 12.PHONY: valgrind valgrind: programa valgrind --leak-check=full \ --show-leak-kinds=all \ --track-origins=yes \ --verbose \ ./programa .PHONY: asan asan: $(CC) -fsanitize=address -g -o programa programa.c ./programa
Ejercicio 2.05b.20 - b.20 - Supresor de Falsos Positivos ⭐⭐☆☆☆¶
Crear un archivo de supresión para Valgrind que ignore leaks conocidos de bibliotecas del sistema:
{
leak_ignorado_libc
Memcheck:Leak
...
obj:/lib/x86_64-linux-gnu/libc-*.so
}Usar con: valgrind --suppressions=mi_supresion.supp ./programa
Ejercicio 2.05b.21 - b.21 - Wrapper de malloc para Debugging ⭐⭐☆☆☆¶
Implementar wrappers de las funciones de memoria que agreguen logging:
1 2 3 4 5 6 7 8 9 10 11 12 13void *debug_malloc(size_t size, const char *archivo, int linea) { void *ptr = malloc(size); printf("[MALLOC] %zu bytes en %p (%s:%d)\n", size, ptr, archivo, linea); return ptr; } void debug_free(void *ptr, const char *archivo, int linea) { printf("[FREE] %p (%s:%d)\n", ptr, archivo, linea); free(ptr); } #define malloc(size) debug_malloc(size, __FILE__, __LINE__) #define free(ptr) debug_free(ptr, __FILE__, __LINE__)
Casos Especiales¶
Ejercicio 2.05b.22 - b.22 - Memoria Compartida entre Funciones ⭐⭐☆☆☆¶
Implementar un patrón donde una función asigna memoria y otra la libera, documentando claramente la responsabilidad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22// El llamador es responsable de liberar la memoria retornada char *construir_mensaje(const char *usuario) { size_t len = strlen("Hola, ") + strlen(usuario) + 2; char *msg = malloc(len); if (msg == NULL) { return NULL; } snprintf(msg, len, "Hola, %s", usuario); return msg; } int main() { char *mensaje = construir_mensaje("Alumno"); if (mensaje != NULL) { printf("%s\n", mensaje); free(mensaje); // ¡El main debe liberar! } return 0; }
Ejercicio 2.05b.23 - b.23 - Retorno de Estructuras con Memoria Dinámica ⭐⭐⭐☆☆¶
Implementar una función que retorne un struct que contiene punteros a memoria dinámica:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20struct resultado_t { int *datos; size_t tamano; }; struct resultado_t procesar_numeros(int *entrada, size_t n) { struct resultado_t res = {NULL, 0}; res.datos = malloc(n * sizeof(int)); if (res.datos == NULL) { return res; } for (size_t i = 0; i < n; i++) { res.datos[i] = entrada[i] * 2; } res.tamano = n; return res; }
Ejercicio 2.05b.24 - b.24 - Gestión de Recursos con goto ⭐⭐☆☆☆¶
Demostrar el uso apropiado de goto para cleanup en caso de errores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28int procesar_archivo(const char *ruta) { FILE *archivo = NULL; char *buffer = NULL; int resultado = -1; archivo = fopen(ruta, "r"); if (archivo == NULL) { goto cleanup; } buffer = malloc(1024); if (buffer == NULL) { goto cleanup; } // ... procesamiento ... resultado = 0; // éxito cleanup: if (buffer != NULL) { free(buffer); } if (archivo != NULL) { fclose(archivo); } return resultado; }
Ejercicios Integradores de Memoria Dinámica¶
Ejercicio 2.05b.25 - b.25 - Diccionario Dinámico Clave-Valor ⭐⭐⭐☆☆¶
Implementar una estructura de diccionario asociativo en memoria dinámica basada en un arreglo de pares (clave, valor):
Crecer automáticamente mediante
reallocduplicando su capacidad cuando el arreglo se lleneDuplicar dinámicamente las cadenas de texto correspondientes a las claves
Liberar cada cadena individual y el arreglo principal al destruir la estructura
Ejercicio 2.05b.26 - b.26 - Matriz Tridimensional Dinámica ⭐⭐⭐⭐☆¶
Implementar la asignación, manipulación y liberación de un volumen tridimensional de enteros (int ***) de dimensiones :
Asignar el arreglo de punteros a matrices 2D, luego los punteros a filas, y finalmente cada fila
Implementar la función de liberación que destruya simétricamente cada dimensión en orden inverso
Asegurar que ante cualquier fallo parcial de asignación durante la creación, se liberen todos los bloques previamente reservados sin dejar fugas
Ejercicio 2.05b.27 - b.27 - Parser de JSON Simplificado ⭐⭐☆☆☆¶
Crear un parser básico de JSON que construya una estructura de datos en memoria dinámica representando el objeto JSON. Manejar cadenas, números, arreglos y objetos anidados.
Ejercicio 2.05b.28 - b.28 - Sistema de Gestión de Tareas ⭐⭐☆☆☆¶
Implementar un sistema que mantenga una lista de tareas donde cada tarea tiene:
ID único
Descripción (cadena dinámica)
Subtareas (arreglo dinámico de punteros a otras tareas)
El sistema debe permitir agregar, eliminar y buscar tareas, manejando toda la memoria correctamente.
Preguntas Conceptuales¶
Ejercicio 2.05b.29 - b.29 - Stack vs Heap ⭐⭐☆☆☆¶
Pregunta: ¿Por qué no podemos simplemente usar el stack para todas las asignaciones de memoria? ¿Qué limitaciones tiene?
Respuesta esperada: El stack tiene tamaño limitado (típicamente 1-8 MB), crece automáticamente en cada llamada a función, y su memoria tiene scope limitado al bloque actual. El heap permite asignaciones grandes, persistencia más allá del scope, y tamaño determinado en runtime.
Ejercicio 2.05b.30 - b.30 - Fragmentación Interna vs Externa ⭐⭐☆☆☆¶
Pregunta: Explicar la diferencia entre fragmentación interna y externa. Dar ejemplos de cada una.
Fragmentación interna: Espacio desperdiciado dentro de un bloque asignado
(ej. malloc(17) puede asignar 32 bytes, desperdiciando 15).
Fragmentación externa: Espacio libre total suficiente pero dividido en bloques no contiguos (ej. después de liberar bloques alternados).
Ejercicio 2.05b.31 - b.31 - Costos Ocultos de malloc ⭐⭐☆☆☆¶
Pregunta: ¿Por qué malloc tiene overhead? ¿Qué información necesita
guardar el allocator?
Respuesta esperada: El allocator guarda metadatos como tamaño del bloque, estado (libre/ocupado), punteros a bloques vecinos para coalescing, información de alineación. Típicamente 8-16 bytes por bloque.
Ejercicio 2.05b.32 - b.32 - free(NULL) ⭐⭐☆☆☆¶
Pregunta: ¿Qué hace free(NULL)? ¿Es seguro? ¿Por qué esta semántica es
útil?
Respuesta esperada: free(NULL) no hace nada y es completamente seguro
según el estándar C. Esto permite código más simple sin necesidad de verificar
if (ptr != NULL) free(ptr).
Ejercicio 2.05b.33 - b.33 - Tiempo de Vida de Memoria ⭐⭐☆☆☆¶
Pregunta: Clasificar las siguientes asignaciones según su tiempo de vida:
Variable local
Variable
staticlocalVariable global
Memoria de
malloc
Respuestas:
Variable local: hasta el final del bloque
Variable
staticlocal: toda la ejecución del programa (inicializada una vez)Variable global: toda la ejecución del programa
Memoria de
malloc: hasta elfreecorrespondiente