Regla 0x3016h: Orden incorrecto o sospechoso de argumentos en llamadas a memset
Memoria, punteros y tipos (0x30XX)
0x3016h: Orden incorrecto o sospechoso de argumentos en llamadas a memset¶
Enunciado normativo¶
En toda llamada a
memset, los argumentos DEBEN respetar el orden canónicomemset(ptr, valor, cantidad): primero la dirección, después el valor del byte a escribir y por último la cantidad de bytes. NO DEBE intercambiarse el valor con la cantidad.
¿Por qué existe esta regla?¶
El problema¶
memset tiene tres argumentos de tipos distintos pero fáciles de confundir:
void *, int y size_t. El segundo es el valor del byte (se convierte a
unsigned char); el tercero es cuántos bytes se escriben. Invertirlos pasa
desapercibido porque ambos son números: memset(buffer, sizeof(buffer), 0) es
sintácticamente válido.
El efecto es doble y catastrófico: no se inicializa la memoria (se escribe el
valor sizeof(buffer) en 0 bytes, es decir nada) y, si la cantidad era grande,
podría escribirse un valor equivocado en una región enorme. El error es un clásico
pedagógico porque compila sin advertencias.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| No inicializa | La memoria conserva basura; los bugs por datos sin inicializar aparecen después. |
| Escritura masiva | Con argumentos invertidos y valores grandes, se corrompe memoria adyacente. |
| Bug silencioso | El código parece inicializar pero el efecto real es nulo. |
| Mantenibilidad | El lector debe recordar el orden; conviene que sea explícito. |
Fundamento en el estándar y en la cátedra¶
C11 §7.24.6.1 define memset(void *s, int c, size_t n). La cátedra adopta el
orden como parte del estilo idiomático y lo relaciona con 0x3018h: Inicialización idiomática de agregados con {0} en lugar de memset inmediato: para
inicializar agregados a cero, = {0} es preferible a un memset inmediato. La
regla tiene corrección automática porque el patrón invertido es detectable de
forma fiable.
Alcance y excepciones¶
Aplica a memset y a funciones con firma análoga (memcpy, memmove,
bzero), donde el tamaño es el último argumento. No aplica a calloc, cuyo
orden es calloc(cantidad, tamano), ni a memset legítimo para rellenar con
bytes distintos de cero. Tampoco exime de verificar que la cantidad sea correcta.
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — Cantidad y valor invertidos¶
struct config_t config;
memset(&config, sizeof(config), 0);Por qué falla: sizeof(config) es el valor del byte a escribir y 0 es la
cantidad de bytes. memset no escribe nada, porque el tercer argumento es 0. El
struct queda sin inicializar y el error no produce ninguna advertencia.
❌ Contraejemplo 2 — Invertidos en un arreglo¶
int datos[100];
memset(datos, sizeof(datos), 0);Por qué falla: igual que el anterior, no inicializa. Peor aún, si la intención
fuera inicializar a un valor distinto de cero, el valor se tomaría de
sizeof(datos) y la cantidad de 0, de modo que el resultado sería
silenciosamente nulo. La forma idiomática es dejar el valor en el segundo lugar.
✅ Ejemplo conforme 1 — Orden canónico explícito¶
unsigned char buffer[256];
memset(buffer, 0, sizeof(buffer));El segundo argumento es el valor (0) y el tercero la cantidad
(sizeof(buffer)). La memoria se inicializa por completo y el tamaño se mantiene
correcto si el arreglo cambia de dimensión. El orden de los argumentos coincide
con el de la firma estándar.
✅ Ejemplo conforme 2 — Inicialización de un struct con {0}¶
struct config_t config = {0};Para llevar un agregado a cero, la forma idiomática es {0} conforme a
0x3018h: Inicialización idiomática de agregados con {0} en lugar de memset inmediato. memset se reserva para rellenar con bytes distintos de cero o
para una región que no puede inicializarse en su declaración. Si se usa
memset, el orden canónico sigue siendo obligatorio.
⚠️ Casos límite¶
Valor distinto de cero:
memset(buffer, 0xFF, sizeof(buffer))es correcto; el valor es un entero, no un puntero.Byte nulo en cadenas:
memset(cadena, '\0', n)es válido, pero para inicializar un arreglo completo preferí= {0}.sizeofdel puntero:memset(ptr, 0, sizeof(ptr))inicializa sólo 8 bytes, no el bloque apuntado; usán * sizeof(*ptr)si corresponde.Solapamiento:
memsetno está pensado para regiones solapadas; para copiar usámemmove.
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | memset(ptr, sizeof(...), ...) con el tamaño en el segundo argumento. |
gcc / clang | gcc -Wall -Wextra -std=c11 -Wsizeof-pointer-memaccess ... | memset con argumento de tamaño sospechoso. |
| Revisión manual | — | Segundo argumento grande o tercero igual a 0/1. |
Checklist de autocontrol¶
¿El segundo argumento es el valor a escribir?
¿El tercero es la cantidad de bytes?
¿La cantidad es
sizeof(arreglo)on * sizeof(*ptr)?¿Usé
= {0}cuando sólo quería ceros?¿Evité
sizeof(ptr)como cantidad del bloque apuntado?
Reglas relacionadas¶
0x3018h: Inicialización idiomática de agregados con {0} en lugar de memset inmediato — inicialización idiomática de agregados con
{0}.0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) — el tamaño correcto es condición de un
memsetválido.0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado —
sizeof(ptr)no mide el bloque apuntado.0x500Bh: Inclusión obligatoria de cabeceras estándar para funciones de la biblioteca C — las funciones de la biblioteca requieren su cabecera,
<string.h>.
Antipatrón: Tamaño insuficiente en memset con sizeof(ptr)¶
Síntoma en el código del estudiante¶
Después de reservar una estructura, se inicializa con memset pero el tamaño
sale de sizeof sobre el puntero:
struct nodo_t *n = malloc(sizeof(*n));
memset(n, 0, sizeof(n));El tercer argumento repite el error de 0x3013h: n es un puntero.
Diagnóstico¶
Mecanismo del defecto¶
memset(destino, valor, cantidad) escribe exactamente cantidad bytes a partir
de destino. Si cantidad es sizeof(n) —el tamaño del puntero, 8 bytes— solo
se limpian los primeros 8 bytes del bloque, sin importar cuántos mida la
estructura. Los campos que caen más allá del byte 8 quedan con basura.
Ese resto sin inicializar es el peor de los casos: contiene valores residuales
del heap (direcciones viejas, contadores, longitudes). Al usarlos como índices o
punteros, el programa se comporta de manera impredecible. El error es
silencioso porque memset no puede saber cuánta memoria había reservada.
Consecuencia observable¶
Campos de la estructura con valores basura.
Fallos erráticos al leer esos campos, a veces con
SIGSEGVal usar un puntero residual inválido.Con
-fsanitize=memory(MSan) o Valgrind se reporta el uso de memoria no inicializada.
Fundamento en el estándar C11¶
C11 §7.24.6.1p2:
memsetcopiancaracteres; el resultado depende del valor exacto den.C11 §6.5.3.4:
sizeof(n)sobre un puntero da el tamaño del puntero.La cátedra prefiere la inicialización idiomática de agregados con
{0}en la declaración (regla 0x3018h) antes que unmemsetposterior.
Corrección idiomática¶
❌ Código con el antipatrón¶
struct nodo_t *n = malloc(sizeof(*n));
if (n == NULL) {
return NULL;
}
memset(n, 0, sizeof(n)); /* limpia solo 8 bytes */✅ Código refactorizado — sizeof(*n)¶
struct nodo_t *n = malloc(sizeof(*n));
if (n == NULL) {
return NULL;
}
memset(n, 0, sizeof(*n));✅ Alternativa idiomática — inicialización con {0}¶
struct nodo_t *n = malloc(sizeof(*n));
if (n == NULL) {
return NULL;
}
*n = (struct nodo_t){0};La regla 0x3018h prefiere {0} para agregados declarados; aplicada a memoria
dinámica, la asignación compuesta evita depender de un sizeof repetido.
⚠️ Casos límite¶
calloc(n, sizeof(*p))ya entrega el bloque en cero; en ese caso elmemsetes redundante.memsetpara poner un patrón distinto de cero (por ejemplo0xFF) sí es legítimo, pero el tamaño sigue siendosizeof(*p).Limpiar un arreglo:
memset(v, 0, n * sizeof(*v)), nuncasizeof(v).
Errores típicos al compilar o ejecutar¶
# Sin sanitizadores el error es invisible:
$ ./programa
# el campo n->sig contiene basura y el recorrido cae lejos del origen
Segmentation fault (core dumped)
$ valgrind ./programa
==1234== Conditional jump or move depends on uninitialised value(s)Checklist de verificación¶
¿El tercer argumento de
memsetessizeof(*ptr), nosizeof(ptr)?¿En arreglos usé
n * sizeof(*v)?¿Consideré
calloco{0}en lugar dememset?¿Verifié el bloque con un sanitizador de memoria?
Reglas relacionadas¶
0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) —
sizeof(*ptr)en la reserva.0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado —
sizeofsobre el puntero en lugar del tipo apuntado.0x3018h: Inicialización idiomática de agregados con {0} en lugar de memset inmediato — inicialización idiomática con
{0}.0x3013h: Asignación de memoria con sizeof sobre puntero en lugar del tipo apuntado — mismo error en
malloc.