Dominando : Cadenas, Memoria y Seguridad

En C, una “cadena de texto” no existe como un tipo de dato primitivo; lo que manejamos son arrays de caracteres que terminan con un valor especial llamado carácter nulo ('\0'). Las funciones de <string.h> son herramientas diseñadas para buscar, copiar, comparar y manipular estos arrays.

Para entender cómo funcionan, debes comprender que la mayoría de estas funciones no saben cuánto mide un array; simplemente recorren la memoria byte a byte hasta que encuentran el '\0'. Por ello, si olvidas ese carácter o intentas escribir más allá del espacio reservado, el programa seguirá leyendo o escribiendo en memoria que no le pertenece, provocando errores catastróficos.

Si necesitas manipular texto, usarás funciones como strlen para medir o strcmp para comparar. Si vas a mover bloques de datos, usarás memcpy o memmove. El peligro surge cuando no controlas el tamaño del destino o cuando intentas copiar una cadena en un espacio insuficiente: el resultado es un desbordamiento de búfer (buffer overflow), un error que compromete la seguridad y la estabilidad del sistema.

#define _POSIX_C_SOURCE 200809L // Para strtok_r [POSIX]
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void) {
    // Un log de ejemplo: [INFO] id:1024:mensaje
    char log_entrada[] = "[INFO] id:1024:mensaje";
    char buffer_destino[32];
    char *token;
    char context[10]; // Contexto para strtok_r [POSIX]

    // 1. Medición y comparación
    size_t longitud = strlen(log_entrada);
    printf("Log original: %s (Longitud: %zu)\n", log_entrada, longitud);

    if (strncmp(log_entrada, "[INFO]", 6) == 0) {
        printf("El log es de tipo INFO.\n");
    }

    // 2. Búsqueda de caracteres y substrings
    char *colon_ptr = strchr(log_entrada, ':'); // Busca la primera ':'
    if (colon_ptr != NULL) {
        printf("Primer ':' encontrado en la posición: %ld\n", colon_ptr - log_entrada);
    }

    char *sub_string = strstr(log_entrada, "1024"); // Busca el substring
    if (sub_string != NULL) {
        printf("ID encontrado: %s\n", sub_string);
    }

    // 3. Tokenización (dividir la cadena)
    // Usamos strtok_r por ser reentrante [POSIX]
    printf("Tokens extraídos:\n");
    token = strtok_r(log_entrada, ":", context);
    while (token != NULL) {
        printf("  Token: %s\n", token);
        token = strtok_r(NULL, ":", context);
    }

    // 4. Copia segura y manipulación de memoria
    // Usamos memset para limpiar el buffer con ceros
    memset(buffer_destino, 0, sizeof(buffer_destino));
    
    // strncpy copia hasta n, pero no garantiza el '\0' si el origen es largo
    // Por eso, siempre es buena práctica asegurar el terminador manualmente.
    strncpy(buffer_destino, "Texto corto", sizeof(buffer_destino) - 1);
    buffer_destino[sizeof(buffer_destino) - 1] = '\0';
    printf("Buffer con strncpy: '%s'\n", buffer_destino);

    // 5. Memoria con regiones solapadas: memcpy vs memmove
    char arreglo[] = "ABCDE";
    // Queremos mover "BCD" una posición a la derecha para obtener "ABBCDE"
    // Usamos memmove porque las regiones [0-3] y [1-4] se solapan
    memmove(arreglo + 1, arreglo, 3);
    printf("Arreglo tras memmove: %s\n", arreglo);

    // 6. Comparación de memoria cruda
    char datos1[] = {0x01, 0x02, 0x03};
    char datos2[] = {0x01, 0x02, 0x03};
    if (memcmp(datos1, datos2, 3) == 0) {
        printf("Los bloques de memoria son idénticos.\n");
    }

    return 0;
}

Análisis del funcionamiento

En el código anterior, hemos visto cómo interactúa la memoria con las funciones. Al usar strlen(log_entrada), el procesador recorre log_entrada hasta detectar el valor 0 que marca el fin. Si log_entrada no tuviera ese \0, strlen seguiría leyendo hasta encontrar un cero accidental en la memoria, devolviendo un valor incorrecto o provocando un error de segmentación.

Al utilizar strtok_r, estamos realizando una tokenización destructiva. Esto significa que la función inserta caracteres \0 directamente en el string original para separar las partes. Por eso, después de usar strtok_r, el array log_entrada ha sido modificado; ya no es el mismo string original. La versión strtok_r [POSIX] requiere un puntero context para guardar el estado de la iteración, lo que permite usarla de forma segura en programas con múltiples hilos (thread-safe), a diferencia de la versión clásica strtok.

Un punto crítico es la diferencia entre memcpy y memmove. En el ejemplo, al desplazar caracteres en el arreglo, las áreas de origen y destino se solapan. memcpy no está diseñado para esto y el resultado es un comportamiento indefinido porque el compilador/CPU podría intentar copiar datos que aún no han sido actualizados por la propia copia. memmove, en cambio, garantiza la integridad al tratar la copia como si se moviera a un búfer temporal, permitiendo solapamientos sin errores.

El error frecuente

Un error clásico al usar strncpy es asumir que siempre añade un terminador nulo al final.

char destino[5];
strncpy(destino, "HOLA_MUNDO", 5); 
// ERROR: 'destino' ahora contiene {'H', 'O', 'L', 'A', '_'} 
// ¡NO TIENE '\0'!
printf("%s", destino); // Imprimirá "HOLA_" seguido de basura de la memoria.

Si el tamaño n de strncpy es igual o menor que la longitud de la cadena origen, no se escribirá el carácter nulo. Esto hará que cualquier función posterior (como printf o strlen) siga leyendo memoria descontroladamente. Para evitar esto, siempre usa strncpy limitando a sizeof(dest) - 1 y asigna manualmente dest[sizeof(dest) - 1] = '\0'. Herramientas como AddressSanitizer (-fsanitize=address) detectarán este desbordamiento de lectura de forma inmediata.

57

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio