← Gestión de Memoria Bloque 11 · Integración y cierre · Tema 51 de 52

Tema 51

Proyecto: asignador de memoria con pruebas

Un asignador no está terminado porque una secuencia de malloc y free parezca funcionar. Debe conservar alineación, tamaños, enlaces y datos después de miles de operaciones, incluidos los fallos. En este proyecto completaremos una API didáctica sobre mmap, separaremos interfaz e implementación y construiremos pruebas de reutilización, fragmentación, agotamiento y conservación de contenido. Una semilla permitirá repetir el estrés y el laboratorio interactivo mostrará cómo una comprobación detecta un objeto alterado.

1. Qué significa completar el proyecto

Retomamos el asignador con lista libre del tema 46: una región, primer ajuste, división de restos útiles y fusión inmediata. Ahora agregamos un contrato verificable, estadísticas, calloc y una batería que comprueba tanto la estructura como los bytes entregados.

Una prueba del asignador debe verificar una propiedad observable y dejar evidencia de que se conserva. Que no haya una caída del proceso no demuestra que los bloques sean distintos, estén alineados o mantengan sus datos.

La capacidad seguirá siendo fija y el uso será de un solo hilo. Eso permite estudiar la corrección sin añadir todavía varias regiones, cachés o bloqueos. «Completo» significa que todas las operaciones del contrato definido están implementadas y probadas; no que este archivo sustituya a libc.

Los archivos se guardan en un directorio de práctica y no reemplazan los de los temas anteriores. Tampoco se redefinen los símbolos malloc y free: la biblioteca sigue administrando su propia memoria para printf y otras funciones.

2. Contrato y organización de archivos

OperaciónContrato del proyecto
mi_init(bytes)Una región mmap, redondeada a página; 0 si funciona, -1 con errno si falla; rechaza otra inicialización activa
mi_malloc(n)Puntero alineado o NULL; n igual a cero devuelve NULL sin contar un fallo
mi_calloc(cantidad, tamaño)Comprueba el producto y pone a cero exactamente los bytes pedidos; producto cero devuelve NULL
mi_free(p)Devuelve 0 al liberar un bloque vivo o NULL; devuelve -1 con EINVAL si no encuentra un comienzo válido ocupado
mi_check()Devuelve 1 si el estado satisface las invariantes y 0 si detecta una inconsistencia
mi_stats(salida)Obtiene estadísticas solo de una región activa y coherente; devuelve 0 o -1
mi_destroy()Retira la región si no hay bloques vivos; con objetos pendientes devuelve -1 y EBUSY

La devolución de estado de mi_free es una decisión de esta API. El free estándar devuelve void y no permite pasarle punteros interiores, ajenos o ya liberados. No se deben trasladar las pruebas negativas de este proyecto al free de libc.

La búsqueda por igualdad de puntero evita leer una supuesta cabecera antes de un puntero ajeno. Aun así, no detecta referencias obsoletas cuando una dirección vuelve a alojar un objeto nuevo: esa dirección puede coincidir con un comienzo ocupado válido. El usuario sigue siendo responsable de la propiedad y de no conservar referencias después de liberar.

Utilizaremos asignador.h, asignador.c y pruebas.c. Los tres bloques de código siguientes contienen todo lo necesario para compilar en Linux con C17.

3. Cabeceras, lista y estadísticas

La cabecera conserva tamaño total, tamaño solicitado, enlace libre y estado. Una unión con max_align_t mantiene la alineación. Las solicitudes se redondean a A = _Alignof(max_align_t), y solo se divide si el resto alcanza para una cabecera H y una unidad A de datos.

Guardar el tamaño solicitado permite distinguir relleno de datos útiles. Las estadísticas se definen sobre la capacidad administrada, no sobre RSS ni sobre todas las asignaciones del proceso:

identidad de la región administrada
capacidad = pedidos_vivos + relleno_ocupado + libre_util + cabeceras

libre_util: suma de (tamaño total - H) de los bloques libres.
mayor_libre: mayor capacidad útil de un único bloque libre.
cabeceras: número de bloques físicos × H.
relleno: suma de (tamaño total - H - pedido) de los ocupados.

Al fusionar desaparecen cabeceras intermedias y crece el espacio útil de un hueco. Por eso la capacidad útil libre final es capacidad - H, no la suma que había antes de fusionar.

El pico de pedidos cuenta el máximo de bytes solicitados simultáneamente vivos. Las asignaciones exitosas, fallos positivos y liberaciones aceptadas se acumulan desde la última inicialización correcta. La petición cero y mi_free(NULL) no incrementan esos contadores. Las estadísticas dejan de estar disponibles tras destruir la región.

4. El verificador de invariantes

El verificador recorre los bloques físicos y contrasta la lista libre. Antes de avanzar comprueba que la cabecera que va a leer cabe en la región, que el tamaño es válido y que no se sale del intervalo.

No se desreferencia un enlace de lista sin haberlo contrastado con una cabecera física válida: se compara el puntero esperado con el bloque que ya estamos recorriendo. Un enlace erróneo, extra o cíclico debe hacer fallar esa correspondencia.

Esto es una ayuda de diagnóstico, no una barrera de seguridad frente a corrupción arbitraria o escrituras concurrentes. Las operaciones normales presuponen que el estado no fue alterado fuera del contrato, y la batería llama a mi_check después de cada cambio.

5. Archivo de interfaz

asignador.h · API propia y significado explícito de estadísticas
#ifndef ASIGNADOR_H
#define ASIGNADOR_H

#include <stddef.h>

struct MiStats {
    size_t capacidad, mapeado, cabecera, alineacion;
    size_t vivos, pedidos, pico, libre_util, mayor_libre;
    size_t cabeceras, relleno;
    unsigned long long exitos, fallos, liberaciones;
};

int mi_init(size_t bytes);
void *mi_malloc(size_t n);
void *mi_calloc(size_t cantidad, size_t tam);
int mi_free(void *p);
int mi_check(void);
int mi_stats(struct MiStats *salida);
int mi_destroy(void);

#endif

El tamaño de la cabecera y la alineación se publican como diagnóstico, para que las pruebas calculen límites sin fijar 32 y 16 como constantes de una ABI. La aplicación no obtiene permiso para leer cabeceras por conocer esos valores.

6. Implementación del asignador

Las diferencias principales con el tema 46 son el tamaño solicitado por bloque, la región configurable, los retornos comprobables, el recorrido para localizar un puntero de usuario y el verificador completo.

asignador.c · región única, lista libre, calloc y diagnóstico
#define _DEFAULT_SOURCE
#include "asignador.h"
#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

typedef union Bloque Bloque;
union Bloque {
    struct { size_t total, pedido; Bloque *sig; int libre; } v;
    max_align_t alineacion;
};

static unsigned char *base;
static Bloque *lista;
static size_t capacidad, mapeado, vivos, pedidos, pico;
static unsigned long long exitos, fallos, liberaciones;

static size_t offset(const Bloque *b)
{
    return (size_t) ((const unsigned char *) b - base);
}

static void *fallar(int codigo)
{
    fallos++;
    errno = codigo;
    return NULL;
}

int mi_init(size_t bytes)
{
    if (base != NULL) { errno = EBUSY; return -1; }
    long consulta = sysconf(_SC_PAGESIZE);
    if (bytes == 0 || consulta <= 0) { errno = EINVAL; return -1; }
    size_t pagina = (size_t) consulta, a = _Alignof(max_align_t);
    if (bytes > SIZE_MAX - (pagina - 1)) { errno = ENOMEM; return -1; }
    size_t mapa = ((bytes + pagina - 1) / pagina) * pagina;
    if (mapa > (size_t) PTRDIFF_MAX) { errno = ENOMEM; return -1; }
    size_t cap = (mapa / a) * a;
    if (cap < sizeof(Bloque) + a) { errno = EINVAL; return -1; }
    void *p = mmap(NULL, mapa, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (p == MAP_FAILED) return -1;
    if ((uintptr_t) p % a) { munmap(p, mapa); errno = EINVAL; return -1; }
    base = p; mapeado = mapa; capacidad = cap;
    vivos = pedidos = pico = 0;
    exitos = fallos = liberaciones = 0;
    lista = (Bloque *) base;
    lista->v.total = capacidad; lista->v.pedido = 0;
    lista->v.sig = NULL; lista->v.libre = 1;
    return 0;
}

void *mi_malloc(size_t n)
{
    if (n == 0) return NULL;
    if (base == NULL) return fallar(EINVAL);
    size_t a = _Alignof(max_align_t), h = sizeof(Bloque);
    if (n > SIZE_MAX - h) return fallar(ENOMEM);
    size_t necesario = n + h;
    if (necesario > SIZE_MAX - (a - 1)) return fallar(ENOMEM);
    necesario = ((necesario + a - 1) / a) * a;
    Bloque **enlace = &lista;
    while (*enlace != NULL && (*enlace)->v.total < necesario)
        enlace = &(*enlace)->v.sig;
    if (*enlace == NULL) return fallar(ENOMEM);
    Bloque *b = *enlace;
    size_t resto = b->v.total - necesario;
    if (resto >= h + a) {
        Bloque *r = (Bloque *) (base + offset(b) + necesario);
        r->v.total = resto; r->v.pedido = 0;
        r->v.sig = b->v.sig; r->v.libre = 1;
        *enlace = r;
        b->v.total = necesario;
    } else *enlace = b->v.sig;
    b->v.pedido = n; b->v.sig = NULL; b->v.libre = 0;
    vivos++; pedidos += n; exitos++;
    if (pedidos > pico) pico = pedidos;
    return (unsigned char *) b + h;
}

void *mi_calloc(size_t cantidad, size_t tam)
{
    if (cantidad == 0 || tam == 0) return NULL;
    if (tam > SIZE_MAX / cantidad) return fallar(ENOMEM);
    size_t n = cantidad * tam;
    void *p = mi_malloc(n);
    if (p != NULL) memset(p, 0, n);
    return p;
}

int mi_free(void *p)
{
    if (p == NULL) return 0;
    if (base == NULL) { errno = EINVAL; return -1; }
    Bloque *b = NULL;
    for (size_t pos = 0; pos < capacidad;) {
        Bloque *c = (Bloque *) (base + pos);
        if ((void *) (base + pos + sizeof(Bloque)) == p) { b = c; break; }
        pos += c->v.total;
    }
    if (b == NULL || b->v.libre) { errno = EINVAL; return -1; }
    Bloque *antes = NULL, *despues = lista;
    while (despues != NULL && offset(despues) < offset(b)) {
        antes = despues; despues = despues->v.sig;
    }
    pedidos -= b->v.pedido; vivos--; liberaciones++;
    b->v.pedido = 0; b->v.libre = 1; b->v.sig = despues;
    if (antes != NULL) antes->v.sig = b; else lista = b;
    if (despues != NULL && offset(b) + b->v.total == offset(despues)) {
        b->v.total += despues->v.total; b->v.sig = despues->v.sig;
    }
    if (antes != NULL && offset(antes) + antes->v.total == offset(b)) {
        antes->v.total += b->v.total; antes->v.sig = b->v.sig;
    }
    return 0;
}

int mi_check(void)
{
    if (base == NULL)
        return capacidad == 0 && mapeado == 0 && lista == NULL && vivos == 0 && pedidos == 0;
    size_t a = _Alignof(max_align_t), h = sizeof(Bloque);
    if ((uintptr_t) base % a || capacidad % a || capacidad > mapeado || capacidad < h + a)
        return 0;
    size_t pos = 0, cuenta = 0, suma = 0;
    int previo_libre = 0;
    Bloque *esperado = lista;
    while (pos < capacidad) {
        if (pos % a || capacidad - pos < h) return 0;
        Bloque *b = (Bloque *) (base + pos);
        size_t t = b->v.total;
        if (t < h + a || t % a || t > capacidad - pos ||
            (b->v.libre != 0 && b->v.libre != 1)) return 0;
        if (b->v.libre) {
            if (previo_libre || b->v.pedido != 0 || esperado != b) return 0;
            esperado = b->v.sig;
        } else {
            if (b->v.pedido == 0 || b->v.pedido > t - h || b->v.sig != NULL) return 0;
            cuenta++; suma += b->v.pedido;
        }
        previo_libre = b->v.libre;
        pos += t;
    }
    return pos == capacidad && esperado == NULL && cuenta == vivos && suma == pedidos;
}

int mi_stats(struct MiStats *s)
{
    if (s == NULL || base == NULL) { errno = EINVAL; return -1; }
    if (!mi_check()) { errno = EFAULT; return -1; }
    *s = (struct MiStats) { 0 };
    s->capacidad = capacidad; s->mapeado = mapeado;
    s->cabecera = sizeof(Bloque); s->alineacion = _Alignof(max_align_t);
    s->vivos = vivos; s->pedidos = pedidos; s->pico = pico;
    s->exitos = exitos; s->fallos = fallos; s->liberaciones = liberaciones;
    for (size_t pos = 0; pos < capacidad;) {
        Bloque *b = (Bloque *) (base + pos);
        size_t util = b->v.total - sizeof(Bloque);
        s->cabeceras += sizeof(Bloque);
        if (b->v.libre) {
            s->libre_util += util;
            if (util > s->mayor_libre) s->mayor_libre = util;
        } else s->relleno += util - b->v.pedido;
        pos += b->v.total;
    }
    return 0;
}

int mi_destroy(void)
{
    if (base == NULL) return 0;
    if (vivos != 0) { errno = EBUSY; return -1; }
    if (munmap(base, mapeado) == -1) return -1;
    base = NULL; lista = NULL;
    capacidad = mapeado = vivos = pedidos = 0;
    return 0;
}

mi_free recorre todos los bloques físicos para comprobar la igualdad con un comienzo de datos ocupado. No resta H al puntero recibido ni lo desreferencia. Eso agrega costo respecto de la versión del tema 46, y no ofrece una garantía frente a corrupción previa de las propias cabeceras.

Los desbordamientos se comprueban antes de sumar cabeceras, redondear o multiplicar. El mapeo se limita a PTRDIFF_MAX para que los desplazamientos internos no excedan el rango utilizado en diferencias de punteros. Si falla una reserva, la estructura y los objetos vivos deben permanecer intactos, aunque aumente el contador de fallos.

7. Casos deterministas y fallos esperados

Las pruebas pequeñas tienen una expectativa concreta y permiten localizar el problema antes del estrés:

CasoPropiedad que comprueba
Petición cero y producto ceroContrato explícito, sin reserva ni falso fallo
SIZE_MAX y producto desbordadoRechazo sin convertir la petición en un bloque pequeño
Hueco interior reutilizadoPrimer ajuste, división y conservación de vecinos
Resto A frente a resto H + AAbsorción de resto inútil y división mínima válida
Dos huecos separadosFallo por fragmentación sin modificar objetos vivos
Agotamiento con objetos pequeñosNULL esperado, sin bucles ni pérdidas de bloques
Liberación finalUn único hueco de capacidad útil capacidad - H
Puntero ajeno o interiorRechazo de esta API didáctica sin leer memoria del candidato

Un fallo de asignación previsto no hace fallar la batería. Lo que debe fallar es la comprobación si el asignador devuelve un resultado incompatible o altera el estado que debía conservar.

Las pruebas mantienen datos con patrones distintos y verifican los bytes pedidos. No inspeccionan relleno ni utilizan bytes de un objeto liberado. Las comparaciones de direcciones para verificar reutilización usan un valor numérico guardado con fines de diagnóstico, sin conservar acceso a la vida anterior del objeto.

8. Cómo generar estrés reproducible

Usaremos 64 slots externos que representan referencias de la aplicación. Cada operación elige uno: si está vivo, se libera; si está vacío, se intenta una reserva de 1 a 8192 bytes, a veces mediante calloc.

Un generador de 32 bits con estado y semilla conocidos produce las decisiones. No se usa la hora ni un resultado dependiente del planificador. La semilla y el número de paso se incluyen en los errores para poder repetir la secuencia.

  1. Verificar los patrones de todos los objetos vivos antes de cambiar el estado.
  2. Realizar una operación y comprobar su resultado según el contrato.
  3. Si se reservó, comprobar alineación y, para calloc, los ceros; después escribir el patrón.
  4. Verificar invariantes, identidad de estadísticas y todos los objetos vivos otra vez.
  5. Al terminar, liberar los slots y comprobar fusión completa.

Las comprobaciones por byte pueden ser mucho más costosas que la operación de asignación. Es una prueba de corrección, no un benchmark. Para medir velocidad habría que separar o desactivar esa instrumentación y diseñar otra carga.

9. Batería completa en C

El macro CHECK permanece activo con NDEBUG. Las llamadas con efectos no dependen de assert, de modo que una compilación de pruebas no elimina operaciones accidentalmente.

pruebas.c · límites, reutilización, fragmentación, agotamiento y estrés
#include "asignador.h"
#include <ctype.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static uint32_t semilla = 12345, estado;
static size_t paso;
#define CHECK(x) do { if (!(x)) { \
    fprintf(stderr, "Fallo: %s; línea %d; semilla %lu; paso %zu\n", \
            #x, __LINE__, (unsigned long) semilla, paso); \
    exit(EXIT_FAILURE); \
} } while (0)

static struct MiStats foto(void)
{
    struct MiStats s;
    CHECK(mi_check());
    CHECK(mi_stats(&s) == 0);
    CHECK(s.pedidos + s.relleno + s.libre_util + s.cabeceras == s.capacidad);
    return s;
}

static void vacio(void)
{
    struct MiStats s = foto();
    CHECK(s.vivos == 0 && s.pedidos == 0);
    CHECK(s.cabeceras == s.cabecera && s.libre_util == s.capacidad - s.cabecera);
}

static void deterministas(void)
{
    CHECK(mi_check() && mi_free(NULL) == 0);
    CHECK(mi_malloc(1) == NULL && errno == EINVAL);
    CHECK(mi_init(0) == -1 && errno == EINVAL);
    CHECK(mi_init(64UL * 1024) == 0);
    CHECK(mi_init(64UL * 1024) == -1 && errno == EBUSY);
    struct MiStats s = foto();
    size_t cap = s.capacidad, h = s.cabecera, a = s.alineacion;
    CHECK(mi_malloc(0) == NULL && mi_calloc(0, 5) == NULL);
    CHECK(foto().fallos == 0);
    CHECK(mi_malloc(SIZE_MAX) == NULL && errno == ENOMEM);
    CHECK(mi_calloc(SIZE_MAX, 2) == NULL && errno == ENOMEM);
    vacio();

    unsigned char *p = mi_malloc(4 * a), *q = mi_malloc(8 * a), *r = mi_malloc(4 * a);
    CHECK(p && q && r);
    CHECK((uintptr_t) p % a == 0 && (uintptr_t) q % a == 0 && (uintptr_t) r % a == 0);
    memset(p, 0xA1, 4 * a); memset(r, 0xC3, 4 * a);
    uintptr_t lugar = (uintptr_t) q;
    CHECK(mi_destroy() == -1 && errno == EBUSY);
    int ajeno = 0;
    CHECK(mi_free(&ajeno) == -1 && errno == EINVAL);
    CHECK(mi_free(p + 1) == -1 && errno == EINVAL);
    CHECK(mi_free(q) == 0); q = NULL;
    q = mi_calloc(1, 3 * a);
    CHECK(q != NULL && (uintptr_t) q == lugar);
    for (size_t i = 0; i < 3 * a; i++) CHECK(q[i] == 0);
    for (size_t i = 0; i < 4 * a; i++) CHECK(p[i] == 0xA1 && r[i] == 0xC3);
    CHECK(mi_free(q) == 0);
    CHECK(mi_free(q) == -1 && errno == EINVAL); q = NULL;
    CHECK(mi_free(p) == 0); p = NULL;
    CHECK(mi_free(r) == 0); r = NULL;
    vacio();

    p = mi_malloc(cap - h - a);
    CHECK(p != NULL && foto().libre_util == 0);
    CHECK(foto().cabeceras == h);
    CHECK(mi_free(p) == 0); p = NULL;
    p = mi_malloc(cap - 2 * h - a);
    CHECK(p != NULL && foto().libre_util == a);
    CHECK(foto().cabeceras == 2 * h);
    CHECK(mi_free(p) == 0); p = NULL;
    vacio();

    void *cuatro[4];
    CHECK(cap % (4 * a) == 0);
    for (size_t i = 0; i < 4; i++) { cuatro[i] = mi_malloc(cap / 4 - h); CHECK(cuatro[i]); }
    CHECK(mi_free(cuatro[0]) == 0); cuatro[0] = NULL;
    CHECK(mi_free(cuatro[2]) == 0); cuatro[2] = NULL;
    s = foto();
    CHECK(s.libre_util > cap / 3 && s.mayor_libre < cap / 3);
    CHECK(mi_malloc(cap / 3) == NULL && errno == ENOMEM);
    struct MiStats despues = foto();
    CHECK(despues.pedidos == s.pedidos && despues.libre_util == s.libre_util);
    CHECK(despues.fallos == s.fallos + 1);
    CHECK(mi_free(cuatro[1]) == 0); cuatro[1] = NULL;
    CHECK(mi_free(cuatro[3]) == 0); cuatro[3] = NULL;
    vacio();

    void *pequenos[4096];
    size_t cantidad = 0;
    while ((p = mi_malloc(1)) != NULL) {
        CHECK(cantidad < 4096); pequenos[cantidad++] = p;
    }
    CHECK(errno == ENOMEM && cantidad > 0);
    (void) foto();
    for (size_t i = 0; i < cantidad; i += 2) CHECK(mi_free(pequenos[i]) == 0);
    (void) foto();
    for (size_t i = 1; i < cantidad; i += 2) CHECK(mi_free(pequenos[i]) == 0);
    vacio();
    CHECK(mi_destroy() == 0 && mi_check());
    CHECK(mi_destroy() == 0);
}

static uint32_t aleatorio(void)
{
    estado = estado * UINT32_C(1664525) + UINT32_C(1013904223);
    return estado;
}

struct Slot { unsigned char *p; size_t n; unsigned char patron; };

static void comprobar_datos(const struct Slot slots[], size_t cantidad)
{
    size_t vivos = 0, suma = 0;
    struct MiStats s = foto();
    for (size_t i = 0; i < cantidad; i++) if (slots[i].p != NULL) {
        vivos++; suma += slots[i].n;
        CHECK((uintptr_t) slots[i].p % s.alineacion == 0);
        for (size_t j = 0; j < slots[i].n; j++) CHECK(slots[i].p[j] == slots[i].patron);
        for (size_t j = 0; j < i; j++) if (slots[j].p != NULL) {
            uintptr_t a = (uintptr_t) slots[i].p, b = (uintptr_t) slots[j].p;
            CHECK(a < b ? b - a >= slots[i].n : a - b >= slots[j].n);
        }
    }
    CHECK(s.vivos == vivos && s.pedidos == suma);
}

static void estres(size_t pasos)
{
    CHECK(mi_init(64UL * 1024) == 0);
    struct Slot slots[64] = { 0 };
    estado = semilla;
    for (paso = 1; paso <= pasos; paso++) {
        comprobar_datos(slots, 64);
        size_t i = (aleatorio() >> 16) % 64;
        if (slots[i].p != NULL) {
            CHECK(mi_free(slots[i].p) == 0); slots[i].p = NULL;
        } else {
            size_t n = 1 + (aleatorio() >> 8) % 8192;
            int ceros = (aleatorio() & 3) == 0;
            unsigned char *p = ceros ? mi_calloc(1, n) : mi_malloc(n);
            if (p != NULL) {
                if (ceros) for (size_t j = 0; j < n; j++) CHECK(p[j] == 0);
                unsigned char patron = (unsigned char) (aleatorio() >> 24);
                memset(p, patron, n);
                slots[i] = (struct Slot) { p, n, patron };
            } else CHECK(errno == ENOMEM);
        }
        comprobar_datos(slots, 64);
    }
    struct MiStats s = foto();
    printf("Estrés: semilla=%lu; pasos=%zu; exitos=%llu; fallos=%llu; pico=%zu B\n",
           (unsigned long) semilla, pasos, s.exitos, s.fallos, s.pico);
    for (size_t i = 0; i < 64; i++) { CHECK(mi_free(slots[i].p) == 0); slots[i].p = NULL; }
    vacio();
    CHECK(mi_destroy() == 0 && mi_check());
}

static int numero(const char *texto, unsigned long maximo, unsigned long *n)
{
    if (!*texto) return 0;
    for (const char *p = texto; *p; p++) if (!isdigit((unsigned char) *p)) return 0;
    char *fin; errno = 0;
    *n = strtoul(texto, &fin, 10);
    return errno == 0 && *fin == '\0' && *n <= maximo;
}

int main(int argc, char *argv[])
{
    unsigned long sem = 12345, pasos = 5000;
    if (argc > 3 || (argc > 1 && !numero(argv[1], UINT32_MAX, &sem)) ||
        (argc > 2 && (!numero(argv[2], 100000, &pasos) || pasos == 0))) {
        fprintf(stderr, "Uso: %s [semilla_32_bits [pasos_1_a_100000]]\n", argv[0]); return 1;
    }
    semilla = (uint32_t) sem;
    deterministas();
    puts("Casos deterministas correctos.");
    estres((size_t) pasos);
    puts("Datos, invariantes, estadísticas y fusión final correctos.");
    return 0;
}

Las comprobaciones de no superposición convierten direcciones a uintptr_t para diagnóstico dentro del mapeo de Linux. No se usan para desreferenciar objetos liberados ni pretenden definir orden portable entre objetos de cualquier plataforma.

El banco de slots está fuera de la región probada y lleva su propio tamaño y patrón. Así, no confía en el campo pedido de la cabecera para decidir cuántos bytes verificar. El verificador estructural y los datos externos se complementan.

10. Compilar, ejecutar e interpretar resultados

terminal · los tres archivos en el mismo directorio
$ gcc -std=c17 -Wall -Wextra -Wpedantic -O2 asignador.c pruebas.c -o pruebas
$ ./pruebas
Casos deterministas correctos.
Estrés: semilla=12345; pasos=5000; exitos=...; fallos=...; pico=... B
Datos, invariantes, estadísticas y fusión final correctos.
$ ./pruebas 0 10000
$ ./pruebas 4294967295 10000

Los contadores dependen de la semilla, longitud y tamaños de la plataforma. Un número de fallos positivo es normal en una región pequeña con demanda variable. Lo que debe ser estable al repetir en el mismo entorno es la secuencia y el resultado.

Si una comprobación falla, conserva la semilla y el paso. Repite con menos pasos hasta mantener el prefijo que reproduce el problema. Después reduce la secuencia a un caso determinista que explique la causa; añadir más semillas sin entender el fallo no lo corrige.

En plataformas donde la página base o la alineación cambien, puede ser necesario adaptar el supuesto de cuatro cuartos alineados de la prueba de fragmentación. La implementación publica las magnitudes para poder comprobar ese supuesto en vez de ocultarlo.

11. Laboratorio de pruebas interactivo

Este banco usa una región de 4096 bytes, cabecera de 32, alineación de 16 y 16 slots externos. Las peticiones van de 1 a 512 bytes. Comprueba partición, fusión, no superposición, estadísticas y patrones tras cada operación. Es un modelo del método, con parámetros distintos de la batería C.

Laboratorio · estrés reproducible y conservación de datos

Objetos vivosPedidos vivosLibre útilMayor huecoCabecerasRellenoÉxitosFallos previstosPico útil
Cabecera, offsetTotalPedidoEstado

Últimos 30 eventos:

    Cambiar los campos no modifica la ejecución activa hasta pulsar Iniciar pruebas. La semilla permite repetir ese banco con sus propios parámetros; no se promete que sus contadores coincidan con C, que usa otra capacidad y otras peticiones.

    Para probar el detector, avanza hasta que exista algún objeto y pulsa la alteración intencional. Solo se cambia un byte del arreglo del modelo, no memoria de otras aplicaciones. En la siguiente comprobación debe detectarse el patrón incorrecto y detenerse el banco con semilla, paso y slot.

    La limpieza finaliza la sesión, incluso si no se ejecutaron todos los pasos, libera los objetos y comprueba una sola cabecera con 4064 bytes útiles. Después de una prueba negativa no se permite borrar la evidencia mediante esa limpieza: hay que iniciar otra sesión.

    12. Sanitizadores y límites de la instrumentación

    terminal · instrumentación de comportamiento indefinido
    $ gcc -std=c17 -Wall -Wextra -Wpedantic -O1 -g -fsanitize=undefined -fno-sanitize-recover=all asignador.c pruebas.c -o pruebas_ubsan
    $ ./pruebas_ubsan 12345 5000
    $ gcc -std=c17 -Wall -Wextra -Wpedantic -O2 -DNDEBUG asignador.c pruebas.c -o pruebas_ndebug
    $ ./pruebas_ndebug 12345 5000

    UndefinedBehaviorSanitizer puede ayudar a detectar accesos desalineados y otras operaciones instrumentables. No demuestra ausencia de todos los errores. AddressSanitizer tampoco conoce automáticamente cada subasignación dentro de nuestro mmap: sin integración adicional, un acceso que cruza un bloque puede permanecer dentro del mismo mapeo.

    Las invariantes descubren daños de estructura y los patrones detectan alteraciones de bytes vivos. No detectan necesariamente una lectura indebida que no cambia contenido, un acceso a relleno que nadie inspecciona ni una referencia obsoleta que coincida con una nueva reserva válida.

    Para completar un informe registra compilador, opciones, tamaño de cabecera, alineación, página base, semillas y cantidad de pasos. Mantén separadas las pruebas de corrección, el perfil de memoria del tema 49 y los benchmarks de velocidad.

    Los contratos subyacentes pueden contrastarse en los manuales de mmap y la familia malloc, y las opciones de instrumentación en la documentación de GCC. Nuestra API tiene las diferencias explícitas de este proyecto.

    13. Criterios de aceptación

    No es suficiente ejecutar una sola semilla grande. Casos pequeños con expectativas exactas y secuencias más largas encuentran clases de problemas diferentes. Tampoco basta aumentar un contador de pruebas: importa qué propiedad podría detectar cada una.

    14. Extensiones y límites del diseño

    Esta implementación no tiene realloc, alineaciones extendidas, varias regiones, devolución parcial ni concurrencia. Buscar un puntero en mi_free recorre los bloques físicos; encontrar un hueco e insertar en la lista son recorridos lineales. El diagnóstico añade todavía más trabajo.

    Como siguiente paso puedes incorporar una sola ampliación y escribir primero su contrato y sus pruebas:

    1. Realloc. Conservar el prefijo, no perder el bloque original en un fallo y definir tamaño cero.
    2. Varias regiones. Registrar bases y tamaños, no fusionar reservas diferentes y retirar solo regiones enteramente libres.
    3. Canarios o zonas de guarda. Reservar espacio adicional y definir cuándo se verifica, sin atribuirles detección universal.
    4. Concurrencia. Proteger todas las transiciones relacionadas y probar transferencias de propiedad entre hilos.

    Un identificador con generación o una interfaz de handles puede ayudar con referencias obsoletas, pero cambia la forma de uso: un puntero sin información adicional no permite distinguir siempre dos vidas sucesivas en la misma dirección.

    El curso termina con dos proyectos sobre capas diferentes: el tema 50 mantiene páginas y traducciones; este administra objetos dentro de una región. La interacción entre ambas capas explica gran parte de lo observado al medir memoria real.

    15. Resumen

    16. Preguntas de repaso y desafíos

    Intenta responder antes de abrir cada explicación.

    1. ¿Por qué se necesitan patrones además de mi_check?

    La estructura puede seguir pareciendo válida mientras una operación escribe sobre los datos de otro objeto. El patrón externo verifica contenido y no depende del pedido guardado en la cabecera.

    2. ¿Por qué guardar el tamaño pedido si ya existe el tamaño total?

    Para distinguir datos vivos de capacidad adicional y conservar una contabilidad útil. El tamaño total también incluye cabecera y relleno.

    3. ¿Un fallo de asignación hace fallar automáticamente la batería?

    No. El agotamiento o la fragmentación pueden producir NULL de forma correcta. La prueba verifica el error esperado y que lo demás permanezca intacto.

    4. ¿Qué evita la búsqueda por igualdad en mi_free?

    Evita inferir y leer una cabecera antes de cualquier puntero recibido. Solo acepta un comienzo de datos ocupado que encuentre en el recorrido físico válido.

    5. ¿Eso detecta siempre un puntero obsoleto?

    No. Si la dirección ya fue reutilizada para otro objeto vivo, un puntero sin generación puede coincidir con el comienzo válido de esa nueva vida.

    6. ¿Por qué CHECK no se escribe como assert?

    Porque las comprobaciones de esta batería deben ejecutarse incluso con NDEBUG. Además, no se deben eliminar llamadas con efectos al cambiar opciones de compilación.

    7. ¿Qué recupera la fusión además de unir capacidades?

    Los bytes de las cabeceras intermedias dejan de ser metadatos independientes y pasan al espacio útil del bloque combinado.

    8. ¿La misma semilla produce contadores idénticos en cualquier plataforma?

    No necesariamente. El generador es reproducible, pero alineación, cabeceras y capacidad redondeada pueden cambiar las decisiones de reserva. Hay que registrar el entorno.

    9. ¿Por qué no se mide velocidad con este estrés?

    Porque recorre repetidamente objetos y estadísticas para comprobar corrección. Ese trabajo puede dominar sobre malloc y free.

    10. ¿Se pueden pasar estos bloques a free de libc?

    No. Pertenecen a mi_malloc y se liberan con su API. Las bibliotecas no comparten automáticamente cabeceras ni propiedad.

    Para practicar

    1. Compila los tres archivos y ejecuta varias semillas, incluyendo cero y UINT32_MAX.
    2. Agrega los seis órdenes de liberación de tres bloques y comprueba fusión completa después de cada caso.
    3. Prueba init con SIZE_MAX y peticiones cercanas a los límites de redondeo sin realizar accesos a resultados NULL.
    4. Amplía las pruebas de fallos para contrastar todas las magnitudes estructurales, excluyendo los contadores que sí deben cambiar.
    5. En el laboratorio, repite semilla y pasos y compara estadísticas. Luego activa una alteración intencional y observa el diagnóstico.
    6. Introduce en una copia un error de fusión y determina qué caso pequeño lo encuentra antes que el estrés.
    7. Documenta qué errores de lectura o referencia obsoleta no detectan los patrones y las invariantes actuales.
    8. Diseña realloc y sus pruebas de crecimiento, reducción, conservación y fallo antes de implementarlo.