1. Una cadena es un arreglo con un \0
En C, una cadena es una sucesión de caracteres seguida de un byte con valor cero, el terminador nulo, que se escribe '\0'. Se guarda en un arreglo de char, y se maneja a través de un puntero a su primer carácter. Su largo no está guardado en ningún lado: se averigua recorriéndola hasta el terminador.
Ya usamos cadenas desde el primer programa, y en los temas 17 y 18 vimos de dónde salen: el arreglo, el puntero al primer elemento, el recorrido hasta el \0. Este programa muestra todos los bytes de dos arreglos, incluidos los que vienen después del terminador:
#include <stdio.h>
#include <string.h>
static void mostrar_bytes(const char *nombre, const char *s, size_t n)
{
printf("%-8s", nombre);
for (size_t i = 0; i < n; i++) {
if (s[i] == '\0')
printf(" \\0");
else
printf(" %c", s[i]);
}
printf("\n");
}
int main(void)
{
char saludo[] = "hola";
char nombre[10] = "ana";
printf("saludo: sizeof = %zu, strlen = %zu\n", sizeof saludo, strlen(saludo));
printf("nombre: sizeof = %zu, strlen = %zu\n", sizeof nombre, strlen(nombre));
mostrar_bytes("saludo", saludo, sizeof saludo);
mostrar_bytes("nombre", nombre, sizeof nombre);
saludo[2] = '\0'; /* cortar la cadena */
printf("después de saludo[2] = '\\0': \"%s\", strlen = %zu\n", saludo, strlen(saludo));
mostrar_bytes("saludo", saludo, sizeof saludo);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -o anatomia anatomia.c
$ ./anatomia
saludo: sizeof = 5, strlen = 4
nombre: sizeof = 10, strlen = 3
saludo h o l a \0
nombre a n a \0 \0 \0 \0 \0 \0 \0
después de saludo[2] = '\0': "ho", strlen = 2
saludo h o \0 a \0
\0. Lo que hay después es parte del arreglo, pero no de la cadena.La salida muestra las tres ideas que sostienen todo el tema:
- El terminador ocupa lugar.
"hola"tiene 4 caracteres y ocupa 5 bytes. Todo arreglo que vaya a guardar una cadena de n caracteres necesita n + 1 bytes. - El arreglo y la cadena son cosas distintas.
nombremide 10 bytes, lo que dicesizeof, y guarda una cadena de 3 caracteres, lo que dicestrlen. Los 6 bytes finales son parte del arreglo, disponibles para una cadena más larga, pero ninguna función de cadenas los mira. - La cadena termina en el primer
\0. Al escribir'\0'ensaludo[2], la cadena pasó a ser"ho", aunque laay el terminador original siguen en la memoria. Es la forma habitual de recortar una cadena en C: no se borra nada, se mueve el final.
Esta representación tiene una ventaja y un costo. La ventaja es que una cadena no necesita nada más que sus bytes: no hay una cabecera con el largo, y un puntero a cualquier posición de una cadena es a su vez una cadena, su sufijo. El costo es que nadie sabe cuánto mide una cadena sin recorrerla, ni cuánto lugar queda en el arreglo que la contiene. Ese costo es el tema de casi todo lo que sigue.
2. Inicializar cadenas
Una cadena literal puede inicializar un arreglo de char de varias formas, y no todas producen una cadena:
| Declaración | Tamaño | Contenido |
|---|---|---|
char s[] = "hola"; | 5 | El compilador cuenta los caracteres y agrega el \0. La forma habitual. |
char s[10] = "hola"; | 10 | hola, el \0 y cinco ceros más: lo que no se inicializa queda en cero. |
char s[5] = "hola"; | 5 | Justo: los 4 caracteres y el \0. |
char s[4] = "hola"; | 4 | Los 4 caracteres sin terminador. Válido en C, no es una cadena. |
char s[] = {'h', 'o'}; | 2 | Dos caracteres sueltos, sin terminador: tampoco es una cadena. |
char s[3] = "hola"; | 3 | Error: la literal no cabe. |
const char *p = "hola"; | 8 | Un puntero a la literal, que está en memoria de solo lectura (tema 17). |
El caso de char s[4] = "hola" es una regla curiosa de C: si el arreglo tiene exactamente el lugar para los caracteres, el terminador se descarta sin error (C++, en cambio, lo rechaza). El resultado es un arreglo de cuatro letras que cualquier función de cadenas va a seguir leyendo más allá de su final, buscando un \0 que no está:
#include <stdio.h>
#include <string.h>
int main(void)
{
char numero[] = "12345";
char letras[4] = "hola"; /* no queda lugar para el '\0' */
char otra[] = {'s', 'o'}; /* tampoco: no es una cadena */
printf("letras: \"%s\", strlen = %zu\n", letras, strlen(letras));
printf("otra: \"%s\", strlen = %zu\n", otra, strlen(otra));
printf("numero: \"%s\"\n", numero);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -o sinfin sinfin.c
sinfin.c: In function ‘main’:
sinfin.c:7:22: warning: initializer-string for array of ‘char’ truncates NUL terminator but destination lacks ‘nonstring’ attribute (5 chars into 4 available) [-Wunterminated-string-initialization]
7 | char letras[4] = "hola"; /* no queda lugar para el '\0' */
| ^~~~~~
$ ./sinfin
letras: "hola12345", strlen = 9
otra: "sohola12345", strlen = 11
numero: "12345"
Los tres arreglos quedaron uno al lado del otro en la pila, otra, después letras y después numero, y como los dos primeros no tienen terminador, printf y strlen siguieron leyendo hasta el \0 de numero. Es comportamiento indefinido, y aquí tuvo un resultado inofensivo solo porque había un cero cerca: con otra disposición, la lectura habría mostrado basura o llegado a memoria no asignada. gcc 15 advierte en el caso de la literal truncada, con -Wextra; en el caso de los caracteres sueltos no puede saber si la intención era formar una cadena, y no dice nada. El atributo nonstring que menciona el mensaje sirve para declarar que un arreglo guarda bytes de largo fijo a propósito, como algunos campos de formatos binarios, y no una cadena.
La regla práctica: para texto, se escribe char s[] = "…" y se deja que el compilador cuente, o se reserva un tamaño holgado, char s[64], sabiendo que caben 63 caracteres.
3. Medir: strlen y sizeof
strlen(s) cuenta los caracteres desde s hasta el primer \0, sin incluirlo, y devuelve un size_t. Es la función largo del tema 18. sizeof, en cambio, es un operador que el compilador resuelve sin mirar el contenido: con un arreglo da su tamaño total, y con un puntero, 8. La tabla resume cuándo usar cada uno:
sizeof | strlen | |
|---|---|---|
| Qué mide | El lugar reservado, en bytes. | La cadena guardada, en caracteres. |
| Cuándo se calcula | Al compilar. | Al ejecutar, recorriendo la cadena. |
Con char s[10] = "ana" | 10 | 3 |
Con const char *p = "ana" | 8, el tamaño del puntero | 3 |
Con un parámetro char s[] | 8: el arreglo se convirtió en puntero | El largo de la cadena |
| Se usa para | Saber cuánto cabe en un arreglo: fgets(buf, sizeof buf, …) | Saber cuánto ocupa el texto. |
Tres consecuencias de que strlen recorra la cadena:
- Cuesta tiempo proporcional al largo. En un bucle como
for (i = 0; i < strlen(s); i++), la condición recorre la cadena entera en cada vuelta; si el cuerpo modificas, el compilador no puede evitar recalcularla, y el bucle hace un trabajo proporcional al cuadrado del largo. Se calcula una vez antes,size_t n = strlen(s);, o se recorre hasta el\0sin medir. - Necesita una cadena válida. Sin terminador, sigue leyendo fuera del arreglo, como en
sinfin.c. ConNULL, termina enSegmentation fault:strlenno comprueba su argumento. - Devuelve un
size_t, sin signo. Restar dos largos nunca da negativo, como vimos en el tema 7:
$ gcc -std=c17 -Wall -Wextra -o largos largos.c
largos.c: In function ‘main’:
largos.c:10:31: warning: comparison of unsigned expression in ‘< 0’ is always false [-Wtype-limits]
10 | if (strlen(a) - strlen(b) < 0)
| ^
$ ./largos sh bash
"sh" es más larga
strlen(a) - strlen(b) = 18446744073709551614
2 − 4 en size_t da la vuelta a 2⁶⁴ − 2, un número enorme y positivo, y el programa afirma que «sh» es más larga que «bash». La comparación correcta no resta: strlen(a) < strlen(b).
4. El panorama de string.h
El archivo de cabecera string.h declara dos familias de funciones. Las que empiezan con str trabajan con cadenas: se detienen en el \0. Las que empiezan con mem trabajan con bloques de bytes: reciben el tamaño como argumento y no le dan ningún significado al cero.
| Para | Cadenas (hasta el \0) | Bloques (n bytes) | Sección |
|---|---|---|---|
| Medir | strlen | — | 3 |
| Copiar | strcpy, stpcpy (POSIX) | memcpy, memmove | 5, 10 |
| Agregar al final | strcat | — | 5 |
| Comparar | strcmp, strncmp, strcasecmp (POSIX) | memcmp | 8 |
| Buscar un carácter | strchr, strrchr | memchr | 9 |
| Buscar una subcadena | strstr | — | 9 |
| Medir tramos | strspn, strcspn, strpbrk | — | 9 |
| Rellenar | — | memset | 10 |
| Con límite de tamaño | snprintf, strncpy, strncat, strlcpy | — | Tema 22 |
| Separar en partes | strtok, strtok_r | — | Tema 22 |
| Duplicar en memoria nueva | strdup | — | Tema 28 |
Las funciones comparten algunas convenciones que conviene tener presentes. El destino va primero y el origen después, como en una asignación: strcpy(destino, origen) se lee «destino = origen». Los parámetros que la función solo lee son const char *, como vimos en el tema 19. Las que copian devuelven el destino, lo que permite encadenarlas aunque casi nunca convenga. Las que buscan devuelven un puntero a lo encontrado, o NULL. Y ninguna recibe el tamaño del destino: todas suponen que hay lugar.
5. Copiar y concatenar
Una cadena no se copia con =: el nombre de un arreglo no es asignable, y asignar punteros copia la dirección, no el texto, como vimos en el tema 17. Para copiar los caracteres está strcpy(destino, origen), que copia el origen con su \0; para agregar al final de una cadena está strcat(destino, origen), que busca el \0 del destino y copia ahí el origen. Este programa arma rutas de archivos de tres formas:
#define _POSIX_C_SOURCE 200809L /* stpcpy es de POSIX */
#include <stdio.h>
#include <string.h>
int main(void)
{
char ruta[64];
/* con strcpy y strcat */
strcpy(ruta, "/home/ana");
strcat(ruta, "/");
strcat(ruta, ".bashrc");
printf("strcat: %s (%zu)\n", ruta, strlen(ruta));
/* con stpcpy: cada llamada devuelve dónde quedó el '\0' */
char *fin = ruta;
fin = stpcpy(fin, "/etc");
fin = stpcpy(fin, "/");
fin = stpcpy(fin, "passwd");
printf("stpcpy: %s (%td)\n", ruta, fin - ruta);
/* con memcpy, cuando los largos ya se conocen */
const char *dir = "/proc", *arch = "/self/status";
size_t ld = strlen(dir), la = strlen(arch);
memcpy(ruta, dir, ld);
memcpy(ruta + ld, arch, la + 1); /* + 1: copiar también el '\0' */
printf("memcpy: %s (%zu)\n", ruta, ld + la);
return 0;
}
$ ./copiar
strcat: /home/ana/.bashrc (17)
stpcpy: /etc/passwd (11)
memcpy: /proc/self/status (17)
Las tres versiones hacen lo mismo con distinto costo. strcat tiene que encontrar el final del destino antes de copiar, y lo hace recorriéndolo desde el principio en cada llamada: armar una cadena con k llamadas a strcat recorre una y otra vez lo ya copiado, el mismo problema de la sección 3. stpcpy, de POSIX, lo resuelve devolviendo un puntero al \0 que acaba de escribir, de modo que la llamada siguiente empieza justo ahí; la resta fin - ruta, del tema 18, da el largo sin medir. Y cuando los largos ya se conocen, memcpy copia exactamente los bytes indicados, sin buscar ningún terminador: por eso hay que acordarse de copiar el \0, con la + 1.
Las tres versiones tienen además algo en común: ninguna sabe que ruta mide 64 bytes. Si los textos fueran más largos, escribirían igual. En este programa todos los textos están escritos en el código y caben; el problema aparece cuando el texto viene de afuera.
6. El desbordamiento de strcpy
strcpy copia hasta encontrar el \0 del origen, sea cual sea el tamaño del destino. Si el origen lo controla quien usa el programa, como un argumento, una línea leída o un dato que llega por la red, quien usa el programa decide cuántos bytes se escriben. Este programa guarda un nombre y un permiso en una estructura, un tipo que agrupa variables y que estudiaremos en el tema 24; aquí basta saber que sus dos campos quedan uno a continuación del otro en la memoria:
#include <stdio.h>
#include <string.h>
struct usuario {
char nombre[8];
int es_admin;
};
int main(int argc, char *argv[])
{
struct usuario u = {"", 0};
if (argc != 2) {
fprintf(stderr, "uso: %s nombre\n", argv[0]);
return 1;
}
strcpy(u.nombre, argv[1]); /* ¿cabe? strcpy no lo pregunta */
printf("nombre: %s\n", u.nombre);
printf("es_admin: %d (0x%08x)\n", u.es_admin, (unsigned) u.es_admin);
if (u.es_admin)
printf("acceso de administrador concedido\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -o desborde desborde.c
$ ./desborde ana
nombre: ana
es_admin: 0 (0x00000000)
$ ./desborde maximiliano
nombre: maximiliano
es_admin: 7302753 (0x006f6e61)
acceso de administrador concedido
nombre cayeron sobre es_admin. Leídos como un entero, en el orden little endian del tema 5, valen 0x006f6e61.«maximiliano» tiene 11 letras y ocupa 12 bytes con el terminador. strcpy escribió los 8 primeros en nombre y los 4 restantes, a, n, o y el \0, en el campo siguiente, es_admin. El programa no falló, no mostró ningún mensaje, y le dio permisos de administrador a quien escribió un nombre largo. Esta es la forma esencial del desbordamiento de buffer (buffer overflow): un dato externo más largo que el lugar reservado pisa lo que hay al lado, y lo que hay al lado puede ser una decisión de seguridad, un puntero o, como vimos en el tema 12, la dirección de retorno de la función. Desde el gusano de Morris, en 1988, que entró a miles de máquinas desbordando un buffer de fingerd con gets, es una de las vulnerabilidades más explotadas de la historia.
La regla que strcpy impone es que quien llama garantice que el destino tiene al menos strlen(origen) + 1 bytes. Con datos del propio programa se puede verificar leyendo el código; con datos externos, hay que comprobarlo antes de copiar, o usar una función que reciba el tamaño del destino, que es el tema del tema 22.
7. strcat y las defensas del sistema
strcat tiene el mismo problema que strcpy, con una cuenta más difícil: el lugar que queda es el tamaño del destino menos lo que ya tiene. Esta función arma un saludo en un arreglo de 16 bytes:
#include <stdio.h>
#include <string.h>
static void saludar(const char *nombre)
{
char mensaje[16] = "Hola, ";
strcat(mensaje, nombre); /* 6 bytes ocupados: caben 9 más */
strcat(mensaje, "!");
puts(mensaje);
}
int main(int argc, char *argv[])
{
saludar(argc > 1 ? argv[1] : "mundo");
return 0;
}
$ ./saludo Ana
Hola, Ana!
$ ./saludo Bartolomeo
Hola, Bartolomeo!
$ ./saludo Maximiliano_Rodriguez
*** stack smashing detected ***: terminated
Aborted
«Hola, Bartolomeo!» ocupa 18 bytes con el terminador, y el arreglo tiene 16: el programa escribió 2 bytes fuera de él y funcionó, aparentemente, bien. Los bytes cayeron en un hueco de la pila que nadie usaba, y no hubo consecuencia visible. Ese es el peor resultado posible para quien prueba el programa, porque la prueba pasa. Con el nombre más largo, la escritura llegó hasta el valor testigo que el protector de pila coloca antes de la dirección de retorno, y al volver de saludar el programa lo encontró alterado y abortó, como vimos en el tema 12.
Las mismas pruebas, compiladas de otras dos formas:
$ gcc -std=c17 -Wall -Wextra -g -fsanitize=address,undefined -o saludo saludo.c
$ ./saludo Bartolomeo
=================================================================
==411==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x71c363fd0030 at pc 0x75c366f23257 bp 0x7ffdc0d2d230 sp 0x7ffdc0d2c9d8
WRITE of size 11 at 0x71c363fd0030 thread T0
#0 0x75c366f23256 in strcat ../../../../src/libsanitizer/asan/asan_interceptors.cpp:527
#1 0x5c289b8ac405 in saludar /home/ana/c/tema21/saludo.c:8
#2 0x5c289b8ac581 in main /home/ana/c/tema21/saludo.c:15
…
This frame has 1 object(s):
[32, 48) 'mensaje' (line 6) <== Memory access at offset 48 overflows this variable
$ gcc -std=c17 -Wall -Wextra -g -O2 -o saludo saludo.c
$ ./saludo Bartolomeo
*** buffer overflow detected ***: terminated
Aborted
AddressSanitizer, del tema 1, señala la línea exacta: un strcat que escribió 11 bytes, «Bartolomeo» y su terminador, y cuyo acceso se salió de mensaje, que ocupa los bytes 32 a 48 del marco. Con -O2, Ubuntu activa _FORTIFY_SOURCE, del tema 4: el compilador sabe que mensaje mide 16 bytes y reemplaza strcat por __strcat_chk, que recibe ese tamaño y aborta antes de escribir de más. Ahora el error con «Bartolomeo» se detecta en todas las ejecuciones, y no solo cuando el desbordamiento es grande.
¿Y desborde.c? Ahí las defensas se comportan distinto, y vale la pena ver por qué:
$ gcc -std=c17 -Wall -Wextra -g -fsanitize=address,undefined -o desborde desborde.c
$ ./desborde maximiliano
nombre: maximiliano
es_admin: 7302753 (0x006f6e61)
acceso de administrador concedido
$ gcc -std=c17 -Wall -Wextra -g -O2 -o desborde desborde.c
$ ./desborde maximiliano
*** buffer overflow detected ***: terminated
Aborted
AddressSanitizer no vio nada: protege los límites de cada objeto de la memoria, y la estructura u entera es un solo objeto de 12 bytes; «maximiliano» y su terminador caben exactamente en ella. El desbordamiento de un campo sobre el siguiente le es invisible. _FORTIFY_SOURCE, en cambio, conoce el tamaño del campo nombre y lo detectó. La tabla resume las defensas:
| Defensa | Cómo se activa | Qué detecta | Cuándo |
|---|---|---|---|
| Protector de pila | Por defecto en Ubuntu (-fstack-protector-strong) | Escrituras que alcanzan el valor testigo de un marco de pila. | Al volver de la función, después del daño. |
_FORTIFY_SOURCE | Por defecto en Ubuntu con -O1 o más | Copias a un destino cuyo tamaño conoce el compilador, incluso un campo. | Antes de escribir. |
| AddressSanitizer | -fsanitize=address, solo al desarrollar | Cualquier acceso fuera de un objeto: pila, montículo, globales. | En el acceso mismo. |
| Ninguna | — | Desbordamientos de un campo a otro sin optimizar, o destinos de tamaño desconocido. | Nunca. |
Las defensas son una red de seguridad, no un permiso: _FORTIFY_SOURCE no puede hacer nada cuando el destino es un puntero cuyo tamaño el compilador no conoce, como un parámetro char * o memoria pedida con malloc, y el protector de pila solo actúa cuando el daño ya ocurrió. La única corrección es que el programa no escriba fuera del arreglo.
8. Comparar cadenas
Comparar dos cadenas con == compara dos direcciones, no dos textos, igual que con los arreglos del tema 17. Para comparar el contenido está strcmp(a, b), que recorre las dos cadenas hasta encontrar la primera diferencia o el final, y devuelve un número negativo, cero o positivo, la misma convención de los comparadores del tema 20. Este programa compara las dos palabras que recibe como argumentos:
#define _POSIX_C_SOURCE 200809L /* strcasecmp es de POSIX */
#include <stdio.h>
#include <string.h>
#include <strings.h>
static const char *relacion(int r)
{
return r < 0 ? "va antes que" : r > 0 ? "va después de" : "es igual a";
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "uso: %s palabra1 palabra2\n", argv[0]);
return 1;
}
const char *a = argv[1];
const char *b = argv[2];
int r;
r = strcmp(a, b);
printf("strcmp = %4d: \"%s\" %s \"%s\"\n", r, a, relacion(r), b);
r = strcasecmp(a, b);
printf("strcasecmp = %4d: sin distinguir mayúsculas, %s\n", r, relacion(r));
r = strncmp(a, b, 3);
printf("strncmp(3) = %4d: los 3 primeros %s\n", r, r == 0 ? "coinciden" : "difieren");
printf("a == b = %4d: ¿la misma dirección?\n", a == b);
return 0;
}
$ ./comparar bash zsh
strcmp = -24: "bash" va antes que "zsh"
strcasecmp = -24: sin distinguir mayúsculas, va antes que
strncmp(3) = -24: los 3 primeros difieren
a == b = 0: ¿la misma dirección?
$ ./comparar bash bash
strcmp = 0: "bash" es igual a "bash"
strcasecmp = 0: sin distinguir mayúsculas, es igual a
strncmp(3) = 0: los 3 primeros coinciden
a == b = 0: ¿la misma dirección?
$ ./comparar Zsh bash
strcmp = -8: "Zsh" va antes que "bash"
strcasecmp = 24: sin distinguir mayúsculas, va después de
strncmp(3) = -8: los 3 primeros difieren
a == b = 0: ¿la misma dirección?
$ ./comparar bash bashrc
strcmp = -114: "bash" va antes que "bashrc"
strcasecmp = -114: sin distinguir mayúsculas, va antes que
strncmp(3) = 0: los 3 primeros coinciden
a == b = 0: ¿la misma dirección?
$ ./comparar año ano
strcmp = 85: "año" va después de "ano"
strcasecmp = 85: sin distinguir mayúsculas, va después de
strncmp(3) = 85: los 3 primeros difieren
a == b = 0: ¿la misma dirección?
Los resultados enseñan varias cosas:
- Solo importa el signo. glibc devuelve la diferencia entre los primeros bytes distintos:
'b' - 'z'es −24,'Z' - 'b'es −8, y el\0que termina «bash» contra larde «bashrc» da −114. Otra biblioteca, o el propiogcccuando compara literales al compilar, puede devolver −1 y 1. El estándar solo promete el signo: se escribestrcmp(a, b) < 0o== 0, nunca== -1. - El orden es el de los bytes, no el alfabético. Las mayúsculas, del 65 al 90 en ASCII, van antes que todas las minúsculas, por eso «Zsh» va antes que «bash».
strcasecmp, de POSIX, declarada enstrings.h, compara ignorando mayúsculas y minúsculas. El orden alfabético de un idioma, con sus acentos y eñes, depende de la configuración regional y es otro problema, el destrcoll. - Los bytes se comparan sin signo. La
ñde «año» empieza con el byte0xc3, 195, y va después de lan, 110: la diferencia es 85. El estándar exige comparar comounsigned char, de modo que los bytes de UTF-8 van después de todo ASCII, aunquechartenga signo en x86-64. strncmpcompara un prefijo. Con un límite de 3, «bash» y «bashrc» son iguales. Es la forma de preguntar si una cadena empieza con otra:strncmp(linea, "VmRSS:", 6) == 0.==da siempre 0, incluso con dos «bash» idénticos: son dos argumentos distintos, en dos direcciones distintas.
Comparar con == contra una literal es un error tan frecuente que gcc lo señala:
$ gcc -std=c17 -Wall -Wextra -o igual igual.c
igual.c: In function ‘main’:
igual.c:11:15: warning: comparison with string literal results in unspecified behavior [-Waddress]
11 | if (orden == "salir")
| ^~
La comparación compila, y es siempre falsa: orden es un arreglo de la pila y "salir" está en .rodata. Lo correcto es strcmp(orden, "salir") == 0. Y la condición if (strcmp(a, b)), sin comparar con nada, es verdadera cuando las cadenas son distintas, lo contrario de lo que sugiere su lectura: conviene escribir siempre el == 0 o el != 0.
9. Buscar dentro de una cadena
Las funciones de búsqueda devuelven un puntero a lo que encontraron dentro de la cadena, o NULL. Como un puntero a cualquier posición de una cadena es otra cadena, el resultado se puede usar directamente: es el resto del texto desde ese punto. Este programa separa una ruta en sus partes, recorre los directorios de una variable como PATH y limpia una línea de configuración:
#include <stdio.h>
#include <string.h>
int main(void)
{
const char *ruta = "/usr/lib/x86_64-linux-gnu/libc.so.6";
const char *barra = strrchr(ruta, '/'); /* la última '/' */
const char *nombre = barra != NULL ? barra + 1 : ruta;
printf("nombre: %s\n", nombre);
printf("directorio: %.*s\n", (int) (barra - ruta), ruta);
const char *punto = strchr(nombre, '.'); /* el primer '.' */
if (punto != NULL)
printf("sufijo: %s\n", punto);
printf("¿contiene \"linux\"? %s\n", strstr(ruta, "linux") != NULL ? "sí" : "no");
/* recorrer los directorios de PATH, separados por ':' */
const char *path = "/usr/local/bin:/usr/bin:/bin::/usr/games";
const char *p = path;
int i = 1;
for (;;) {
const char *dos_puntos = strchr(p, ':');
size_t largo = dos_puntos != NULL ? (size_t) (dos_puntos - p) : strlen(p);
printf("PATH %d: \"%.*s\"\n", i++, (int) largo, p);
if (dos_puntos == NULL)
break;
p = dos_puntos + 1;
}
/* strspn y strcspn: largos de tramos */
const char *linea = " \tclave=valor # comentario";
size_t blancos = strspn(linea, " \t"); /* cuántos son blancos */
const char *dato = linea + blancos;
size_t hasta = strcspn(dato, " #"); /* cuántos NO son ' ' ni '#' */
printf("blancos: %zu, dato: \"%.*s\"\n", blancos, (int) hasta, dato);
return 0;
}
$ ./buscar
nombre: libc.so.6
directorio: /usr/lib/x86_64-linux-gnu
sufijo: .so.6
¿contiene "linux"? sí
PATH 1: "/usr/local/bin"
PATH 2: "/usr/bin"
PATH 3: "/bin"
PATH 4: ""
PATH 5: "/usr/games"
blancos: 4, dato: "clave=valor"
| Función | Devuelve | En buscar.c |
|---|---|---|
strchr(s, c) | Puntero a la primera aparición de c, o NULL. | El primer . del nombre; cada : de PATH. |
strrchr(s, c) | Puntero a la última aparición de c, o NULL. | La última /: separa directorio y nombre. |
strstr(s, sub) | Puntero a la primera aparición de la subcadena, o NULL. | ¿Contiene «linux»? |
strspn(s, acepta) | Cuántos caracteres iniciales están en acepta. | Los blancos del principio. |
strcspn(s, rechaza) | Cuántos caracteres iniciales no están en rechaza. | El dato, hasta un espacio o #. |
strpbrk(s, set) | Puntero al primer carácter que está en set, o NULL. | (Como strcspn, pero devuelve un puntero.) |
Algunos detalles del programa. Los resultados de las búsquedas se combinan con la aritmética del tema 18: barra - ruta es la cantidad de caracteres antes de la barra, y barra + 1, el nombre que sigue. Para mostrar una parte de una cadena sin copiarla ni modificarla, printf acepta %.*s, del tema 10: la precisión indica cuántos caracteres mostrar como máximo, y se pasa como un int antes de la cadena. El recorrido de PATH tampoco modifica el texto, y por eso puede trabajar sobre una literal, y muestra el directorio vacío entre ::, que para el intérprete de órdenes significa el directorio actual; strtok, que veremos en el tema 22, se lo saltearía. Y strcspn ya lo usamos en el tema 10, en la línea linea[strcspn(linea, "\n")] = '\0', que corta una línea en el primer \n, o no hace nada si no lo tiene.
Hay un detalle sobre const, que vimos en el tema 19: strchr recibe un const char * y devuelve un char * sin const, porque C no tiene forma de devolver «el mismo tipo que recibió». Por eso el compilador permite escribir a través del resultado de una búsqueda sobre una literal; aquí guardamos todos los resultados en const char * para no perder la protección. C23 corrige esto: en ese estándar, las funciones de búsqueda devuelven un puntero const si reciben uno.
10. Las funciones mem y el solapamiento
Las funciones mem trabajan con bytes, no con cadenas: reciben siempre la cantidad, no se detienen en el \0, y sirven para cualquier tipo de dato, como usamos memcpy en los temas 5 y 17.
| Función | Hace |
|---|---|
memcpy(dst, src, n) | Copia n bytes. Los dos bloques no pueden superponerse. |
memmove(dst, src, n) | Copia n bytes, correctamente aunque los bloques se superpongan. |
memset(dst, c, n) | Pone los n bytes en el valor c. Habitual con 0: memset(buf, 0, sizeof buf). |
memcmp(a, b, n) | Compara n bytes, como strcmp pero sin detenerse en el \0. |
memchr(s, c, n) | Busca el byte c en los primeros n, como strchr en un bloque. |
La diferencia entre memcpy y memmove es el solapamiento. Copiar de una parte de un arreglo a otra parte del mismo arreglo, por ejemplo para quitar los primeros caracteres de una cadena, hace que origen y destino compartan bytes. memmove está obligada a funcionar en ese caso, copiando en el orden adecuado; memcpy, strcpy y strcat no, y usarlas así es comportamiento indefinido. Sus prototipos lo dicen con restrict, una promesa de que los punteros no apuntan a la misma memoria:
#include <stdio.h>
#include <string.h>
int main(void)
{
char texto[] = " sangría";
char copia[sizeof texto];
memcpy(copia, texto, sizeof texto);
/* quitar los blancos iniciales, en el mismo arreglo */
size_t blancos = strspn(texto, " ");
memmove(texto, texto + blancos, strlen(texto + blancos) + 1);
printf("memmove: \"%s\"\n", texto);
/* lo mismo con strcpy: origen y destino se solapan */
strcpy(copia, copia + blancos);
printf("strcpy: \"%s\"\n", copia);
return 0;
}
$ ./solapar
memmove: "sangría"
strcpy: "sangría"
$ gcc -std=c17 -Wall -Wextra -g -O2 -o solapar solapar.c
In file included from /usr/include/string.h:580,
from solapar.c:2:
In function ‘strcpy’,
inlined from ‘main’ at solapar.c:17:5:
/usr/include/x86_64-linux-gnu/bits/string_fortified.h:93:10: warning: ‘__builtin___strcpy_chk’ accessing 1 byte at offsets 0 and [0, 11] may overlap 1 byte at offset 0 [-Wrestrict]
$ gcc -std=c17 -Wall -Wextra -g -fsanitize=address -o solapar solapar.c && ./solapar
memmove: "sangría"
=================================================================
==543==ERROR: AddressSanitizer: strcpy-param-overlap: memory ranges [0x7102692d0040,0x7102692d0049) and [0x7102692d0042, 0x7102692d004b) overlap
#0 0x75026b4bd0ed in strcpy ../../../../src/libsanitizer/asan/asan_interceptors.cpp:573
#1 0x63d31855d4bc in main /home/ana/c/tema21/solapar.c:17
Las dos versiones dieron el mismo resultado, y eso es lo engañoso: la versión de strcpy que eligió glibc en este procesador copió en un orden que, por casualidad, funciona cuando el destino está antes del origen. Otra versión, otro procesador u otra optimización pueden copiar en otro orden y mezclar los bytes. Con -O2, gcc advierte que los bloques pueden superponerse, y AddressSanitizer lo confirma al ejecutar: strcpy-param-overlap, con los dos rangos de direcciones.
No es una preocupación teórica. Hacia fines de 2010, glibc incorporó una versión más rápida de memcpy que copiaba de atrás hacia adelante en algunos procesadores, y varios programas que usaban memcpy con bloques superpuestos, entre ellos el reproductor Flash de Adobe, empezaron a producir sonido distorsionado. El error estaba en esos programas, no en glibc; pero para no romper los ejecutables existentes, glibc conserva hasta hoy dos versiones del símbolo memcpy: los programas compilados con versiones antiguas reciben una que se comporta como memmove. Las veremos en la sección 13. La regla: si hay alguna posibilidad de que los bloques se superpongan, memmove.
11. Cadenas en la frontera con el núcleo
El terminador nulo no es solo una convención de C: es parte de la interfaz entre los programas y Linux. Las llamadas al sistema que reciben nombres, como open o execve, reciben cadenas terminadas en \0, y por eso un nombre de archivo no puede contener un byte nulo; tampoco una /, que separa los directorios. En cambio, las llamadas que mueven datos, read y write, trabajan con bloques de bytes y reciben el tamaño: no miran el \0 ni lo agregan. Un ejemplo que usa los dos mundos es /proc/PID/cmdline, el archivo donde Linux expone los argumentos de cada proceso:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char ruta[64] = "/proc/self/cmdline";
char buf[4096];
if (argc == 2) { /* el PID de otro proceso */
strcpy(ruta, "/proc/");
strcat(ruta, argv[1]); /* ¡sin comprobar el largo! ver tema 22 */
strcat(ruta, "/cmdline");
}
int fd = open(ruta, O_RDONLY);
if (fd < 0) {
perror(ruta);
return 1;
}
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n < 0) {
perror("read");
return 1;
}
buf[n] = '\0'; /* read no agrega el terminador */
printf("%s: %zd bytes\n", ruta, n);
int i = 0;
for (const char *p = buf; p < buf + n; p += strlen(p) + 1)
printf(" [%d] \"%s\"\n", i++, p);
return 0;
}
$ ./cmdline uno "dos tres" ""
/proc/self/cmdline: 24 bytes
[0] "./cmdline"
[1] "uno"
[2] "dos tres"
[3] ""
$ ./cmdline 1
/proc/1/cmdline: 11 bytes
[0] "/sbin/init"
$ od -c /proc/self/cmdline
0000000 o d \0 - c \0 / p r o c / s e l f
0000020 / c m d l i n e \0
0000031
$ strace -e trace=openat,read ./cmdline uno "dos tres" 2>&1 >/dev/null | tail -3
openat(AT_FDCWD, "/proc/self/cmdline", O_RDONLY) = 3
read(3, "./cmdline\0uno\0dos tres\0", 4095) = 23
+++ exited with 0 +++
El archivo contiene los argumentos tal como el núcleo los guarda en la memoria del proceso, la zona de la pila que vimos en el tema 19: uno detrás de otro, cada uno con su \0. od y strace lo muestran byte a byte. Es un bloque con varias cadenas adentro, y el programa lo recorre aprovechando que cada una termina en \0: p += strlen(p) + 1 salta la cadena actual y su terminador, y deja p al comienzo de la siguiente. El argumento vacío "" aparece como un \0 solo, de largo cero. El mismo formato usa /proc/PID/environ para las variables de entorno.
Hay dos detalles que se repiten en todo programa que lee datos del sistema. El primero: read devuelve una cantidad de bytes y no agrega ningún terminador, porque no sabe si los datos son texto. Si el programa va a tratar el buffer como cadena, tiene que agregarlo él, y para eso lee como máximo sizeof buf - 1 bytes, dejando el último lugar libre. Sin ese buf[n] = '\0', el strlen de la última cadena seguiría por la basura del buffer. El segundo es la advertencia del comentario: el PID llega de la línea de órdenes y se concatena con strcat en un arreglo de 64 bytes, sin comprobar nada. Un argumento de 60 caracteres desborda ruta. Lo dejamos así a propósito, para corregirlo en el tema siguiente con snprintf.
12. Bytes, no caracteres
En el tema 3 vimos que la í de un texto en UTF-8 ocupa dos bytes, y quedó pendiente lo que eso significa para strlen. Para C, un char es un byte, y una cadena es una sucesión de bytes; las funciones de string.h no saben nada de codificaciones:
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int main(void)
{
char palabra[] = "año";
printf("\"%s\": sizeof = %zu, strlen = %zu\n", palabra, sizeof palabra, strlen(palabra));
for (size_t i = 0; palabra[i] != '\0'; i++)
printf(" byte %zu: 0x%02x\n", i, (unsigned char) palabra[i]);
for (char *p = palabra; *p != '\0'; p++)
*p = (char) toupper((unsigned char) *p);
printf("toupper: \"%s\"\n", palabra);
return 0;
}
$ ./utf8
"año": sizeof = 5, strlen = 4
byte 0: 0x61
byte 1: 0xc3
byte 2: 0xb1
byte 3: 0x6f
toupper: "AñO"
«año» tiene tres letras y strlen dice 4, porque la ñ se codifica en UTF-8 con dos bytes, 0xc3 0xb1. toupper pasó a mayúsculas la a y la o, y dejó intactos los dos bytes de la ñ, que por separado no son letras. Las consecuencias prácticas:
strlenmide bytes, que es justamente lo que importa para reservar memoria o parawrite. Para contar caracteres hay que decodificar UTF-8.- Recortar una cadena en una posición arbitraria, como hicimos con
saludo[2] = '\0', puede partir un carácter por la mitad y dejar un byte suelto que la terminal muestra como�. - Lo que sí funciona bien son las búsquedas de caracteres ASCII, como
/,:o\n: UTF-8 está diseñado para que ningún byte de un carácter de varios bytes coincida con un carácter ASCII, ni con\0. Por eso los nombres de archivo en UTF-8 funcionan sin cambios conopeny con todas las funciones de este tema.
Manejar texto como caracteres, y no como bytes, requiere configurar la región con setlocale y usar las funciones de caracteres anchos de wchar.h, o una biblioteca dedicada; en la programación de sistemas, donde casi todo son rutas, nombres y protocolos, trabajar con bytes es lo habitual.
13. Por dentro: gcc y glibc
Las funciones de cadenas se usan tanto que el compilador y la biblioteca les dan un trato especial. Del lado del compilador, gcc conoce su significado y, cuando puede, las calcula sin llamarlas:
$ gcc -std=c17 -O2 -S -fno-asynchronous-unwind-tables -o - constantes.c
largo_fijo:
endbr64
movl $7, %eax
ret
poner_hola:
endbr64
movl $1634496360, (%rdi)
movb $0, 4(%rdi)
ret
largo_variable:
endbr64
jmp strlen@PLT
strlen("sistema") se volvió la constante 7. strcpy(buf, "hola") se volvió dos instrucciones: guardar el número 1634496360, que en hexadecimal es 0x616c6f68, los bytes de h, o, l y a en orden little endian, y después un byte 0. Solo cuando la cadena es desconocida, como el parámetro de largo_variable, hay una llamada real a strlen. Es el mismo trato que explica por qué strcmp con dos literales puede devolver −1 en lugar de la diferencia de bytes de la sección 8: el resultado lo calculó el compilador.
Del lado de la biblioteca, glibc tiene varias versiones de cada función, escritas en ensamblador para distintas generaciones de procesadores, y elige una al cargar el programa:
$ nm -D /usr/lib/x86_64-linux-gnu/libc.so.6 | grep -E ' (strlen|strcpy|strcat|strcmp|memcpy|memmove)@'
00000000000c22f0 T memcpy@GLIBC_2.2.5
00000000000b8c80 i memcpy@@GLIBC_2.14
00000000000b9600 i memmove@@GLIBC_2.2.5
00000000000ba6d0 i strcat@@GLIBC_2.2.5
00000000000ba840 i strcmp@@GLIBC_2.2.5
00000000000bb8b0 i strcpy@@GLIBC_2.2.5
00000000000bc400 i strlen@@GLIBC_2.2.5
$ gdb -q -batch -ex 'break 21' -ex run -ex 'break __strlen_avx2' -ex 'break __strlen_evex' \
-ex 'break __strlen_sse2' -ex continue -ex bt ./anatomia
Breakpoint 1, main () at anatomia.c:21
21 printf("saludo: sizeof = %zu, strlen = %zu\n", sizeof saludo, strlen(saludo));
Breakpoint 2 at 0x7ffff7d9a780: file ../sysdeps/x86_64/multiarch/strlen-avx2.S, line 52.
Breakpoint 3 at 0x7ffff7daaa00: file ../sysdeps/x86_64/multiarch/strlen-evex-base.S, line 72.
Breakpoint 4 at 0x7ffff7cc93d0: file ../sysdeps/x86_64/multiarch/strlen-sse2.S, line 57.
Breakpoint 2, __strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:52
#0 __strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:52
#1 0x0000555555555291 in main () at anatomia.c:21
La letra i en la salida de nm marca una función indirecta (ifunc): en esa dirección no está strlen, sino una función que, al cargar el programa, consulta qué instrucciones tiene el procesador y devuelve la dirección de la mejor versión disponible. Es una aplicación de los punteros a funciones del tema 20. En este procesador, que tiene instrucciones AVX2 pero no AVX-512, la elegida fue __strlen_avx2: el punto de interrupción se detuvo ahí, llamado desde la línea 21 de main. Esa versión no recorre la cadena de a un byte, como nuestra largo del tema 18, sino de a 32 bytes por instrucción, comparándolos todos con cero a la vez. Por eso conviene usar las funciones de la biblioteca en lugar de reescribirlas: son correctas y mucho más rápidas.
La primera línea de nm es la de la sección 10: memcpy@GLIBC_2.2.5, la versión que reciben los programas compilados antes de glibc 2.14, y memcpy@@GLIBC_2.14, con dos @, la versión actual, que reciben los programas compilados hoy.
14. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
Reservar strlen(s) bytes para copiar s | Falta el lugar del \0: se escribe un byte fuera. | strlen(s) + 1. |
| Un arreglo de caracteres sin terminador | printf y strlen siguen leyendo fuera del arreglo. | char s[] = "…", y respetar -Wunterminated-string-initialization. |
strcpy o strcat con datos externos | Desbordamiento: se pisan otras variables, a veces sin ningún síntoma. | Comprobar el largo antes, o snprintf (tema 22). |
Comparar cadenas con == | Compara direcciones: casi siempre falso. | strcmp(a, b) == 0. |
if (strcmp(a, b)) para «son iguales» | Es verdadero cuando son distintas. | Escribir siempre == 0 o != 0. |
strcmp(a, b) == -1 | glibc devuelve la diferencia de bytes, no −1. | strcmp(a, b) < 0. |
strlen(a) - strlen(b) < 0 | Es size_t: nunca negativo. | strlen(a) < strlen(b). |
strlen en la condición de un bucle | Recorre la cadena en cada vuelta. | Calcular el largo una vez antes del bucle. |
No comprobar el NULL de strchr o strstr | Segmentation fault si no se encontró. | Comprobar antes de usar el resultado. |
strcpy o memcpy con bloques superpuestos | Comportamiento indefinido, que suele funcionar hasta que no. | memmove. |
Tratar como cadena lo que devolvió read | read no agrega el \0. | Leer sizeof buf - 1 y hacer buf[n] = '\0'. |
Contar letras con strlen | Cuenta bytes: «año» mide 4. | Decodificar UTF-8, o trabajar con bytes a sabiendas. |
15. Resumen
- Una cadena de C es una sucesión de bytes terminada en
'\0', guardada en un arreglo dechary manejada con un puntero a su primer carácter. Su largo no está guardado: se calcula recorriéndola. - Una cadena de n caracteres necesita n + 1 bytes.
char s[4] = "hola"es válido en C y no deja terminador;gcc15 lo advierte con-Wextra. sizeofmide el arreglo, al compilar;strlenmide la cadena, al ejecutar, hasta el primer\0, y devuelve unsize_tsin signo.- Las funciones
strtrabajan hasta el\0; lasmem, con una cantidad de bytes. El destino va primero, y ninguna conoce el tamaño del destino. strcpycopia,strcatagrega al final buscando el\0cada vez,stpcpydevuelve el final para seguir copiando, ymemcpycopia una cantidad conocida.strcpyystrcatcon datos externos desbordan el destino y pisan lo que sigue en la memoria, a veces sin ningún síntoma. Es la forma esencial del desbordamiento de buffer.- El protector de pila detecta después del daño,
_FORTIFY_SOURCEantes de copiar cuando conoce el tamaño, y AddressSanitizer en el acceso, salvo dentro de una misma estructura. Ninguna reemplaza escribir el código correcto. strcmpcompara bytes sin signo y solo garantiza el signo del resultado;==compara direcciones.strncmpcompara prefijos ystrcasecmpignora mayúsculas.strchr,strrchrystrstrdevuelven un puntero a lo encontrado oNULL;strspnystrcspnmiden tramos. Con%.*sse muestra una parte de una cadena sin copiarla.- Con bloques superpuestos, solo
memmovees correcta. - Las llamadas que reciben nombres usan cadenas con
\0;readywriteusan bloques con tamaño, yreadno agrega el terminador./proc/PID/cmdlineguarda los argumentos separados por\0. - Las cadenas son de bytes: en UTF-8,
strlen("año")vale 4. gcccalcula al compilar las funciones de cadenas con argumentos conocidos, y glibc elige al cargar la versión más rápida para el procesador, como__strlen_avx2.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Cuántos bytes ocupa char s[] = "sistema";, y cuánto devuelve strlen(s)?
Ocupa 8 bytes: los 7 caracteres y el terminador \0, que el compilador agrega al inicializar un arreglo con una literal. strlen(s) devuelve 7, porque cuenta los caracteres antes del terminador.
2. Después de char nombre[10] = "ana";, ¿qué valen sizeof nombre y strlen(nombre), y qué hay en los bytes 4 a 9?
sizeof nombre vale 10, el tamaño del arreglo, y strlen(nombre) vale 3, el largo de la cadena. Los bytes 3 a 9 son ceros: el 3 es el terminador, y los demás quedan en cero porque, al inicializar un arreglo, lo que no se inicializa explícitamente vale cero.
3. ¿Qué tiene de peligroso char s[4] = "hola";?
Es válido en C, pero el arreglo solo tiene lugar para los cuatro caracteres, y el terminador se descarta. El resultado no es una cadena: printf("%s") o strlen seguirán leyendo fuera del arreglo hasta encontrar un cero, lo que es comportamiento indefinido. gcc 15 lo advierte con -Wunterminated-string-initialization, incluida en -Wextra.
4. ¿Por qué strlen(a) - strlen(b) < 0 nunca es verdadero?
Porque strlen devuelve size_t, un tipo sin signo, y la resta de dos valores sin signo también es sin signo: si b es más larga, el resultado da la vuelta a un número enorme y positivo. La comparación correcta es strlen(a) < strlen(b).
5. ¿Qué ventaja tiene stpcpy sobre una serie de llamadas a strcat?
Que devuelve un puntero al \0 que acaba de escribir, de modo que la siguiente copia empieza ahí. strcat tiene que buscar el final del destino recorriéndolo desde el comienzo en cada llamada, así que concatenar muchas partes recorre una y otra vez lo ya copiado.
6. En desborde.c, ¿por qué el nombre «maximiliano» dio permisos de administrador?
Porque ocupa 12 bytes con el terminador y nombre tiene 8. strcpy no conoce el tamaño del destino y escribió los 4 bytes sobrantes, a, n, o y \0, en el campo siguiente de la estructura, es_admin, que pasó a valer 0x006f6e61, distinto de cero y por lo tanto verdadero.
7. ¿Por qué AddressSanitizer no detectó el desbordamiento de desborde.c, y _FORTIFY_SOURCE sí?
AddressSanitizer marca los límites de cada objeto de la memoria, y la estructura entera es un solo objeto de 12 bytes: la copia no salió de ella. _FORTIFY_SOURCE usa el tamaño que el compilador conoce del destino, en este caso el campo nombre de 8 bytes, y aborta antes de copiar si el origen no cabe.
8. «Hola, Bartolomeo!» desbordó un arreglo de 16 bytes y el programa funcionó. ¿Por qué eso es malo?
Porque el error existe igual, y las pruebas no lo muestran: los 2 bytes de más cayeron en un lugar de la pila que nadie usaba. Con otro compilador, otra optimización u otra función, esos bytes pueden pisar algo que sí importa. Un programa con comportamiento indefinido no está casi bien; AddressSanitizer y _FORTIFY_SOURCE lo detectan.
9. ¿Qué devuelve strcmp("Zsh", "bash") en glibc, y qué se puede suponer sobre ese valor?
glibc devolvió −8, la diferencia entre 'Z' (90) y 'b' (98): «Zsh» va antes porque las mayúsculas tienen códigos menores que las minúsculas. Lo único que garantiza el estándar es el signo, negativo; el valor exacto puede cambiar con otra biblioteca o si el compilador calcula la comparación, así que se compara siempre con 0.
10. ¿Cómo se obtienen el directorio y el nombre de /usr/lib/libc.so.6 sin copiar la cadena?
Con strrchr(ruta, '/'), que devuelve un puntero a la última barra. El nombre es la cadena que empieza en barra + 1, y el directorio son los primeros barra - ruta caracteres, que se pueden mostrar con printf("%.*s", (int) (barra - ruta), ruta). Hay que comprobar antes que strrchr no haya devuelto NULL.
11. ¿Cuándo hay que usar memmove en lugar de memcpy o strcpy?
Cuando el origen y el destino pueden superponerse, como al desplazar una cadena dentro de su propio arreglo para quitarle los primeros caracteres. memcpy, strcpy y strcat no garantizan nada en ese caso, y pueden funcionar en un procesador y fallar en otro; memmove está obligada a copiar correctamente.
12. ¿Por qué cmdline.c lee sizeof buf - 1 bytes y después hace buf[n] = '\0'?
Porque read trabaja con bloques de bytes y no agrega ningún terminador. Para tratar el buffer con funciones de cadenas, el programa tiene que agregar el \0 después del último byte leído, y para eso deja libre la última posición del arreglo. Sin el terminador, strlen sobre la última cadena seguiría por los bytes sin inicializar del buffer.
13. ¿Qué significa la i de strlen en la salida de nm -D sobre libc?
Que es una función indirecta (ifunc): el símbolo apunta a una función que, al cargar el programa, mira las capacidades del procesador y elige entre varias versiones de strlen, como __strlen_sse2, __strlen_avx2 o __strlen_evex. Las llamadas posteriores van directamente a la versión elegida.
Para practicar
- Escribe tus propias versiones de
strcpy,strcatystrcmpcon punteros, y compara sus resultados con los de la biblioteca sobre los mismos datos, incluidas cadenas vacías y cadenas conñ. - Corrige
desborde.cpara que rechace con un mensaje los nombres que no caben, comprobando el largo constrlenantes de copiar. - Escribe
size_t contar(const char *s, const char *sub), que cuente cuántas veces aparecesubensusandostrstren un bucle. ¿Cuántas veces aparece «aa» en «aaaa»: 2 o 3? Decide y justifica. - Modifica
cmdline.cpara que lea/proc/PID/environy muestre solo las variables que empiezan con un prefijo recibido como argumento, usandostrncmp. - Escribe
void recortar(char *s), que quite los espacios y tabuladores del principio y del final de una cadena, en el mismo arreglo, usandostrspnymemmove. Pruébala con AddressSanitizer y con una cadena formada solo por blancos. - Escribe
basenameydirnamepropias constrrchr, y compara sus resultados con las órdenes del mismo nombre para/usr/lib/,libc.so.6y/. - Compila
saludo.ccon-O2y busca en el ensamblador, congcc -S, la llamada a__strcat_chk. ¿Qué tamaño recibe como último argumento?