1. Los flujos estándar
Todo proceso en Unix arranca con tres descriptores de archivo abiertos, que conoces del curso de Bash: el 0 es la entrada estándar, el 1 la salida estándar y el 2 la salida de errores. La biblioteca de C los envuelve en tres flujos (streams), declarados en stdio.h como punteros a una estructura FILE:
| Flujo | Descriptor | Para | Funciones típicas | Buffer por omisión |
|---|---|---|---|---|
stdin | 0 | Leer datos | getchar, fgets, scanf | Sí |
stdout | 1 | Escribir resultados | printf, puts, putchar | Por líneas en una terminal; completo si no |
stderr | 2 | Escribir errores y diagnósticos | fprintf(stderr, …), perror | Ninguno |
Las funciones de stdio.h trabajan sobre flujos, no sobre descriptores, y agregan dos cosas a las llamadas al sistema del tema 1: el formato, que convierte números en texto y texto en números, y el buffer, que agrupa muchas escrituras pequeñas en pocas llamadas a write. Las dos tienen consecuencias que vale la pena conocer, empezando por la segunda.
2. Los buffers y el orden de la salida
Este programa alterna mensajes por stdout y por stderr:
#include <stdio.h>
int main(void)
{
printf("1. resultado a stdout\n");
fprintf(stderr, "2. aviso a stderr\n");
printf("3. otro resultado a stdout\n");
fprintf(stderr, "4. otro aviso a stderr\n");
return 0;
}
$ ./orden
1. resultado a stdout
2. aviso a stderr
3. otro resultado a stdout
4. otro aviso a stderr
$ ./orden > todo.txt 2>&1; cat todo.txt
2. aviso a stderr
4. otro aviso a stderr
1. resultado a stdout
3. otro resultado a stdout
$ strace -e trace=write -o traza.txt ./orden > todo.txt 2>&1; grep write traza.txt
write(2, "2. aviso a stderr\n", 18) = 18
write(2, "4. otro aviso a stderr\n", 23) = 23
write(1, "1. resultado a stdout\n3. otro re"..., 49) = 49
En la terminal, el orden es el del programa. Redirigido a un archivo, los avisos aparecen primero, y strace muestra por qué: los dos printf no produjeron ninguna llamada a write en su momento. Su texto quedó en el buffer de stdout, y se escribió todo junto, con una sola llamada de 49 bytes, al terminar el programa. stderr, en cambio, no tiene buffer, y cada fprintf fue un write inmediato.
stdout vive en el proceso, y el núcleo no sabe nada de él hasta que la biblioteca llama a write.La biblioteca elige el modo de buffer de stdout al arrancar, según adónde apunte el descriptor 1:
- Una terminal: buffer por líneas. Se vacía con cada
\n, porque una persona está mirando. - Un archivo o una tubería: buffer completo, de unos 4 KB. Se vacía solo al llenarse o al terminar, porque así hay muchas menos llamadas al sistema.
stderr: sin buffer, siempre. Un mensaje de error tiene que salir ya, aunque el programa muera en la línea siguiente.
Eso explica lo que vimos con _exit en el tema 3: el texto se perdía porque nunca había salido del buffer. Lo mismo le pasa a un programa que muere por una señal, como un Segmentation fault, con la salida redirigida: lo que printf «escribió» antes del error puede no aparecer nunca. Cuando el orden importa, o cuando el programa puede morir, fflush(stdout) vacía el buffer en el momento. También se puede cambiar el modo con setvbuf, pero casi nunca hace falta: lo normal es usar cada flujo para lo suyo y aceptar que, redirigidos al mismo lugar, pueden intercalarse distinto.
3. Anatomía de un especificador
La cadena de formato de printf es texto que se copia tal cual, salvo los especificadores de conversión, que empiezan con % y le indican cómo convertir cada argumento en texto. Un especificador tiene hasta seis partes, en un orden fijo, de las que solo la primera y la última son obligatorias:
%d es el caso mínimo: inicio y conversión.La conversión dice qué tipo de argumento se espera y cómo mostrarlo. El ancho es la cantidad mínima de caracteres: si el valor ocupa menos, se rellena con espacios. La precisión significa cosas distintas según la conversión: decimales para %f, cantidad máxima de caracteres para %s, dígitos mínimos para los enteros. Las banderas ajustan el relleno y los signos. Y la longitud indica el tamaño del argumento entero, de la que depende que printf lea los bytes correctos. Para escribir un % literal se usa %%, como vimos en el tema 3.
4. Conversiones, ancho y precisión
Este programa muestra la mayoría de las combinaciones útiles, y los corchetes permiten ver el ancho exacto de cada resultado:
#include <stdio.h>
int main(void)
{
const char *nombres[] = {"bash", "systemd", "sshd"};
int pids[] = {1532, 1, 845};
double memoria[] = {5.25, 12.5, 7.125};
printf("%-10s %6s %10s\n", "PROCESO", "PID", "MEMORIA");
for (int i = 0; i < 3; i++)
printf("%-10s %6d %8.2f MB\n", nombres[i], pids[i], memoria[i]);
printf("\n");
printf("[%d] [%5d] [%-5d] [%05d] [%+d]\n", 42, 42, 42, 42, 42);
printf("[%x] [%X] [%#x] [%08x] [%o] [%#o]\n", 255, 255, 255, 255, 8, 8);
printf("[%f] [%.2f] [%10.3f] [%e] [%g] [%g]\n", 3.14159, 3.14159, 3.14159, 31415.9, 3.14159, 0.00001234);
printf("[%s] [%10s] [%-10s] [%.3s]\n", "kernel", "kernel", "kernel", "kernel");
printf("[%c] [%c] [%%] [%p]\n", 'A', 66, (void *) nombres);
printf("[%*d] [%.*s]\n", 6, 42, 4, "sistema");
return 0;
}
$ ./tabla
PROCESO PID MEMORIA
bash 1532 5.25 MB
systemd 1 12.50 MB
sshd 845 7.12 MB
[42] [ 42] [42 ] [00042] [+42]
[ff] [FF] [0xff] [000000ff] [10] [010]
[3.141590] [3.14] [ 3.142] [3.141590e+04] [3.14159] [1.234e-05]
[kernel] [ kernel] [kernel ] [ker]
[A] [B] [%] [0x7ffde6660ab0]
[ 42] [sist]
| Conversión | Argumento | Muestra |
|---|---|---|
%d, %i | int | Entero con signo, en decimal. |
%u, %x, %X, %o | unsigned int | Sin signo, en decimal, hexadecimal o octal. |
%c | int | El carácter con ese código. |
%s | char * | La cadena, hasta el \0. |
%f, %e, %g | double | Decimal fijo, notación científica, o la más corta de las dos. |
%p | void * | Una dirección, en hexadecimal. |
%% | (ninguno) | Un signo %. |
| Bandera o campo | Efecto | Ejemplo |
|---|---|---|
ancho 5 | Mínimo de caracteres, alineado a la derecha. | [%5d] → [ 42] |
- | Alinear a la izquierda. | [%-5d] → [42 ] |
0 | Rellenar con ceros en lugar de espacios. | [%05d] → [00042] |
+ | Mostrar siempre el signo. | [%+d] → [+42] |
# | Forma alternativa: prefijo 0x u 0. | [%#x] → [0xff] |
.2 con %f | Decimales. | [%.2f] → [3.14] |
.3 con %s | Máximo de caracteres de la cadena. | [%.3s] → [ker] |
* | Ancho o precisión tomados de un argumento int. | [%*d] con 6 y 42 → [ 42] |
La primera tabla del programa es el uso más común de todo esto: columnas alineadas, con los textos a la izquierda (%-10s) y los números a la derecha (%6d), como las de ps o ls -l. Un detalle curioso: 7,125 se mostró como 7,12 y no 7,13. Ese número tiene representación exacta en binario, y la biblioteca redondea los empates al dígito par. El ancho es un mínimo, no un máximo: un valor más largo se muestra completo y desalinea la columna. Para recortar texto está la precisión de %s, y %.*s permite mostrar los primeros n bytes de una cadena, algo útil con datos que no terminan en \0.
5. Los modificadores de longitud
printf no sabe qué tipos recibe: los argumentos llegan uno tras otro, y solo el formato le dice cuántos bytes leer para cada uno y cómo interpretarlos. Por eso el modificador de longitud tiene que coincidir exactamente con el tipo del argumento:
| Modificador | Con d o i | Con u, x, o |
|---|---|---|
hh | signed char | unsigned char |
h | short | unsigned short |
| (ninguno) | int | unsigned int |
l | long | unsigned long |
ll | long long | unsigned long long |
z | ssize_t | size_t |
j | intmax_t | uintmax_t |
t | ptrdiff_t | — |
Los tipos de stdint.h usan las macros PRId64 y compañía de inttypes.h, como vimos en el tema 5. Para double no hace falta modificador, ni siquiera para float, que al pasar a una función con argumentos variables se convierte a double; L es para long double. Un desacuerdo entre formato y argumento es comportamiento indefinido, y es uno de los errores que -Wall mejor detecta:
$ gcc -std=c17 -Wall -Wextra -o mal mal.c 2>&1 | grep warning
mal.c:5:14: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long int’ [-Wformat=]
mal.c:5:17: warning: format ‘%s’ expects argument of type ‘char *’, but argument 3 has type ‘int’ [-Wformat=]
El segundo es el más peligroso: %s trata el 42 como una dirección y va a leer la memoria en la dirección 42, lo que termina el programa con Segmentation fault. El compilador solo puede hacer esta comprobación cuando el formato es una cadena literal, lo que lleva a la sección 8.
6. La familia de printf
printf tiene varios parientes que comparten el mismo lenguaje de formato y solo cambian el destino:
| Función | Escribe en | Comentario |
|---|---|---|
printf(fmt, …) | stdout | Equivale a fprintf(stdout, fmt, …). |
fprintf(flujo, fmt, …) | Cualquier flujo | stderr, o un archivo abierto con fopen. |
dprintf(fd, fmt, …) | Un descriptor | POSIX: sin pasar por un FILE. |
snprintf(buf, tam, fmt, …) | Una cadena en memoria | Nunca escribe más de tam bytes. La forma segura. |
sprintf(buf, fmt, …) | Una cadena en memoria | No conoce el tamaño del destino: puede desbordarlo. No usar. |
La diferencia entre las dos últimas se ve al intentar escribir un texto de 11 caracteres en un arreglo de 8:
$ gcc -std=c17 -Wall -Wextra -O2 -o spr spr.c
spr.c:5:25: warning: ‘%d’ directive writing 7 bytes into a region of size 4 [-Wformat-overflow=]
…
$ ./spr
*** buffer overflow detected ***: terminated
Aborted
$ ./snp
[pid=123] necesitaba 11
sprintf intentó escribir 12 bytes, con el \0 final, en 8. Esta vez lo salvó la protección _FORTIFY_SOURCE del tema 4, que con -O2 cambia sprintf por una versión que comprueba el tamaño y aborta; sin ella, habría pisado la memoria vecina en silencio. snprintf escribió lo que cabía, siempre con su \0, y devolvió 11, la cantidad de caracteres que habría necesitado: comparando ese valor con el tamaño se sabe si el texto se recortó. El tema 22 trata snprintf en detalle.
7. Cuando la escritura falla
printf devuelve la cantidad de caracteres que escribió, o un número negativo si falló. Casi nadie lo comprueba, y en una terminal casi nunca falla. Pero cuando la salida es un archivo, el disco puede llenarse, y un programa de sistemas que ignora eso puede terminar «bien» habiendo perdido datos. Linux tiene un dispositivo especial para probarlo, /dev/full, en el que toda escritura falla como si el disco estuviera lleno:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int n = printf("hola, disco\n");
fprintf(stderr, "printf devolvió %d\n", n);
if (fclose(stdout) != 0) {
perror("error al escribir la salida");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
$ ./lleno > /dev/full; echo $?
printf devolvió 12
error al escribir la salida: No space left on device
1
$ ./lleno2 > /dev/full; echo $? # solo printf, sin comprobar nada
0
printf devolvió 12, éxito, porque solo puso el texto en el buffer; el error ocurrió después, al intentar vaciarlo. Por eso comprobar el valor de printf no alcanza, y la forma correcta es comprobar el resultado de fflush o fclose sobre stdout antes de terminar. lleno2, que no comprueba nada, terminó con código 0 aunque no escribió un solo byte: un script que lo usara creería que todo salió bien. Las herramientas de GNU, como cat o sort, hacen exactamente esta comprobación al final, y perror, que imprime el mensaje del último error del sistema, es parte del tema 37.
8. La entrada como formato: una vulnerabilidad
Hay un error con printf que no es una cuestión de prolijidad sino de seguridad: usar un texto que viene de afuera como cadena de formato. Parece inofensivo, porque con un texto normal funciona:
#include <stdio.h>
int main(int argc, char *argv[])
{
int secreto = 0x5ec2e70;
if (argc < 2)
return 1;
printf(argv[1]); /* error: la entrada usada como formato */
printf("\n");
printf("%s\n", argv[1]); /* correcto */
return secreto == 0;
}
$ gcc -std=c17 -Wall -Wextra -o eco eco.c
eco.c: In function ‘main’:
eco.c:9:5: warning: format not a string literal and no format arguments [-Wformat-security]
9 | printf(argv[1]); /* error: la entrada usada como formato */
| ^~~~~~
$ ./eco hola
hola
hola
$ ./eco '%p %p %p %p %p %p'
0x7fffe0704838 0x7fffe0704850 0x653994c6ddb0 0x758ede412680 0x758ede413fa0 0x7fffe0704838
%p %p %p %p %p %p
Con un texto que contiene especificadores, printf los interpreta, y como no hay argumentos, toma lo que haya en los registros y en la pila donde deberían estar: direcciones de la pila (0x7fff…), del programa (0x6539…) y de las bibliotecas (0x758e…). Esa información es justamente la que ASLR esconde, y alcanza para que un atacante sepa dónde está cada cosa en la memoria. Con %s, printf lee la memoria apuntada por esos valores; y con %n, un especificador que escribe en la memoria la cantidad de caracteres impresos, se puede modificar el proceso. Es la vulnerabilidad de cadena de formato, que afectó a muchos programas reales.
La regla es absoluta: el formato es siempre una cadena literal escrita en el programa, y los datos van como argumentos, printf("%s", texto). Si solo hay que mostrar un texto, fputs(texto, stdout) lo hace sin interpretar nada. Ubuntu enciende -Wformat-security por omisión, y por eso la advertencia apareció aunque no la pedimos.
9. Leer con scanf
scanf es el espejo de printf: lee texto de stdin y lo convierte según un formato. Como tiene que guardar los valores en variables del llamador, recibe sus direcciones, con &; los arreglos de caracteres ya son direcciones, y van sin &. Devuelve la cantidad de conversiones que logró hacer, o EOF si la entrada terminó antes de la primera.
#include <stdio.h>
int main(void)
{
int edad;
char nombre[8];
printf("edad: ");
int leidos = scanf("%d", &edad);
printf("scanf devolvió %d, edad = %d\n", leidos, leidos == 1 ? edad : -1);
printf("nombre: ");
leidos = scanf("%s", nombre);
printf("scanf devolvió %d, nombre = \"%s\"\n", leidos, nombre);
return 0;
}
$ printf '30\nana\n' | ./edad
edad: scanf devolvió 1, edad = 30
nombre: scanf devolvió 1, nombre = "ana"
Con datos correctos, funciona. El problema de scanf es todo lo demás. Sus reglas de lectura son sutiles: %d y %s saltan los espacios y saltos de línea iniciales, %s lee hasta el siguiente espacio, no hasta el final de la línea, y lo que no se pudo convertir queda en la entrada esperando a la próxima lectura. Cada una de esas reglas produce un error distinto.
10. Los peligros de scanf
Con el mismo edad.c y otras entradas, aparecen cuatro problemas:
$ printf 'treinta\nana\n' | ./edad
edad: scanf devolvió 0, edad = -1
nombre: scanf devolvió 1, nombre = "treinta"
$ printf '99999999999\nana\n' | ./edad
edad: scanf devolvió 1, edad = 1215752191
nombre: scanf devolvió 1, nombre = "ana"
$ printf '30\nmaximiliano\n' | ./edad
*** stack smashing detected ***: terminated
Aborted
$ printf 'abc\n42\n' | ./bucle
ingrese un número: eso no es un número, otra vez: eso no es un número, otra vez: …
(cinco vueltas sin avanzar: abandono)
- La entrada inválida queda en la entrada. Con «treinta»,
%dno pudo convertir nada y devolvió 0, pero no consumió la palabra: la siguiente lectura, que esperaba el nombre, la encontró ahí. Las lecturas se desfasan de las preguntas. - El desbordamiento numérico no se detecta. 99999999999 no cabe en un
int, yscanfdevolvió 1, éxito, con un valor sin sentido. El estándar dice que es comportamiento indefinido, y no hay forma de detectarlo conscanf. %sno conoce el tamaño del destino. «maximiliano» tiene 11 letras más el\0, ynombretiene 8 bytes:scanfescribió más allá del arreglo y pisó la pila. El protector de pila del tema 4 lo detectó al salir demainy abortó. Es exactamente el mismo error quegets, una función tan peligrosa que C11 la eliminó del lenguaje. Se puede limitar con un ancho,%7s, pero es fácil olvidarlo o calcularlo mal, y el resto de la palabra sigue quedando en la entrada.- El bucle infinito.
bucle.crepitescanf("%d", …)mientras no devuelva 1, el intento clásico de «pedir de nuevo». Pero las letras nunca se consumen, de modo que cada vuelta vuelve a fallar con la misma «a», y el 42 de la línea siguiente nunca se lee. Sin el freno de cinco vueltas, el programa no terminaría.
Hay un quinto problema, más sutil, al mezclar scanf con lecturas de líneas: scanf("%d") deja en la entrada el \n que sigue al número, y un fgets posterior lee esa línea vacía en lugar de la siguiente. El origen de todos estos problemas es el mismo: scanf mezcla en una sola operación leer y convertir, y cuando la conversión falla, no queda claro cuánto se leyó. La solución es separar las dos cosas.
11. Leer líneas con fgets
fgets(buf, tam, flujo) lee una línea, incluido su \n, y la guarda en buf como cadena, terminada en \0. Nunca escribe más de tam bytes: si la línea es más larga, guarda lo que cabe, tam - 1 caracteres más el \0, y el resto queda para la próxima llamada. Devuelve NULL cuando ya no hay nada que leer.
#include <stdio.h>
#include <string.h>
int main(void)
{
char linea[16];
int numero = 1;
while (fgets(linea, sizeof linea, stdin) != NULL) {
size_t largo = strlen(linea);
int completa = largo > 0 && linea[largo - 1] == '\n';
linea[strcspn(linea, "\n")] = '\0'; /* quitar el \n, si lo hay */
printf("%2d: [%s]%s\n", numero++, linea, completa ? "" : " (sigue...)");
}
return 0;
}
$ printf 'corta\nuna línea bastante más larga que el buffer\n\nfin' | ./lineas
1: [corta]
2: [una línea bast] (sigue...)
3: [ante más larga] (sigue...)
4: [ que el buffer]
5: []
6: [fin] (sigue...)
La línea larga llegó en tres pedazos, sin desbordar nada: cada fgets tomó lo que cabía, y el siguiente siguió desde ahí. El programa sabe si una lectura completó la línea mirando si termina en \n; la última, «fin», no lo tiene porque el archivo no termina en salto de línea. Hay dos modismos que se repiten siempre con fgets:
sizeof lineacomo tamaño, para que coincida siempre con el arreglo, aunque se cambie su declaración.linea[strcspn(linea, "\n")] = '\0'para quitar el\nfinal.strcspndevuelve la posición del primer\n, o el largo de la cadena si no hay ninguno, de modo que funciona en los dos casos. Las funciones destring.hson del tema 21.
Con un buffer de tamaño razonable, como 256 o 4096 bytes, una línea partida es rara, pero un programa robusto la contempla: puede tratarla como un error, o seguir leyendo hasta encontrar el \n y descartar el resto.
12. Convertir lo leído
Una vez leída la línea, convertirla en un número es otro paso, que se puede hacer con cuidado. La función adecuada es strtol, de stdlib.h, que informa dónde dejó de leer y si el valor se salió de rango. Su uso completo es tema del tema 22; aquí la usamos en una función que pregunta hasta obtener un entero válido:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
/* lee un entero de una línea completa; devuelve 1 si lo logró, 0 si no */
static int leer_entero(const char *pregunta, int *resultado)
{
char linea[64];
for (;;) {
printf("%s", pregunta);
fflush(stdout);
if (fgets(linea, sizeof linea, stdin) == NULL)
return 0; /* fin de la entrada */
char *fin;
errno = 0;
long valor = strtol(linea, &fin, 10); /* detalles en el tema 22 */
while (*fin == ' ' || *fin == '\t' || *fin == '\n')
fin++;
if (fin == linea || *fin != '\0')
fprintf(stderr, " no es un número entero\n");
else if (errno == ERANGE || valor < INT_MIN || valor > INT_MAX)
fprintf(stderr, " fuera de rango\n");
else {
*resultado = (int) valor;
return 1;
}
}
}
int main(void)
{
int edad;
if (!leer_entero("edad: ", &edad)) {
fprintf(stderr, "no se ingresó la edad\n");
return EXIT_FAILURE;
}
printf("edad = %d\n", edad);
return EXIT_SUCCESS;
}
$ printf 'treinta\n30 años\n99999999999\n 42 \n' | ./numero
edad: no es un número entero
edad: no es un número entero
edad: fuera de rango
edad: edad = 42
$ printf 'abc\n' | ./numero; echo $?
edad: no es un número entero
edad: no se ingresó la edad
1
Cada problema de la sección 10 está resuelto. Una entrada inválida se descarta entera, porque se leyó la línea completa, y la pregunta se repite sin desfasarse ni quedar en un bucle infinito. «30 años» se rechaza, porque después del número queda texto que no es espacio; scanf lo habría aceptado como 30. El valor fuera de rango se detecta. Y si la entrada se termina, la función lo informa en lugar de repetir para siempre. El fflush(stdout) después de la pregunta asegura que se vea antes de esperar la respuesta, aunque la salida no esté conectada a una terminal.
Es más código que un scanf, y por eso se escribe una vez, en una función como leer_entero, y se reutiliza. Un programa de sistemas que lee configuración, argumentos o datos de otro programa no puede suponer que la entrada es correcta.
13. Líneas de cualquier largo y la terminal
fgets necesita un buffer de tamaño fijo. POSIX ofrece getline, que pide memoria y la agranda según haga falta, de modo que lee líneas de cualquier largo:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *linea = NULL; /* getline la pide y la agranda */
size_t capacidad = 0;
ssize_t largo;
while ((largo = getline(&linea, &capacidad, stdin)) != -1)
printf("línea de %zd bytes (capacidad del buffer: %zu)\n", largo, capacidad);
free(linea);
return 0;
}
$ (echo corta; head -c 5000 /dev/zero | tr '\0' 'x'; echo; echo fin) | ./largas
línea de 6 bytes (capacidad del buffer: 120)
línea de 5001 bytes (capacidad del buffer: 8194)
línea de 4 bytes (capacidad del buffer: 8194)
getline empezó con un buffer de 120 bytes y lo agrandó al encontrar la línea larga; después lo reutilizó. Además devuelve el largo de lo leído, de modo que funciona incluso con líneas que contienen bytes nulos. A cambio, la memoria la pidió la biblioteca, y el programa tiene que devolverla con free al terminar: la memoria dinámica es el bloque 7.
Por último, un programa de sistemas a veces necesita saber si está hablando con una persona o con otro programa: mostrar una pregunta solo si hay alguien que la lea, usar colores solo en una terminal, como hace ls. La función isatty, de unistd.h, lo responde para cada descriptor:
$ ./terminal
stdin es una terminal
stdout es una terminal
stderr es una terminal
$ echo | ./terminal | cat
stdin no es una terminal
stdout no es una terminal
stderr es una terminal
Es la misma pregunta que se hace la biblioteca de C al decidir el modo de buffer de stdout, en la sección 2, y la que se hacía Bash con [ -t 0 ].
14. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
printf(texto) | Vulnerabilidad de cadena de formato. | printf("%s", texto) o fputs(texto, stdout). |
| Formato que no coincide con el tipo | Comportamiento indefinido; %s con un entero termina el programa. | Respetar la tabla de la sección 5; compilar con -Wall. |
%d para size_t o long | Se leen los bytes equivocados. | %zu, %ld. |
sprintf | Desborda el destino. | snprintf con sizeof. |
| No comprobar la escritura | Datos perdidos con el disco lleno, y código de salida 0. | Comprobar fflush o fclose(stdout). |
Esperar el orden entre stdout y stderr | Redirigidos, se intercalan distinto por el buffer. | fflush(stdout) donde importe. |
Olvidar el & en scanf("%d", n) | scanf escribe en una dirección inválida. | &n; -Wall lo advierte. |
scanf("%s", buf) | Desborda buf. | fgets. |
Repetir scanf hasta que funcione | Bucle infinito: la entrada inválida no se consume. | Leer la línea con fgets y convertirla. |
fgets después de scanf | Lee el \n que dejó scanf: una línea vacía. | No mezclar: leer todo por líneas. |
No quitar el \n de fgets | Comparaciones que fallan, nombres de archivo con \n. | linea[strcspn(linea, "\n")] = '\0'. |
Usar gets | Desborda siempre que la línea sea larga; ya no existe en C11. | fgets o getline. |
15. Resumen
stdin,stdoutystderrenvuelven los descriptores 0, 1 y 2, y agregan formato y buffer.stdoutusa buffer por líneas en una terminal y completo si se redirige;stderrno tiene buffer. Por eso, redirigidos juntos, pueden cambiar de orden.fflushvacía el buffer.- Un especificador tiene la forma
%[banderas][ancho][.precisión][longitud]conversión; solo%y la conversión son obligatorios. - El ancho es un mínimo;
-alinea a la izquierda,0rellena con ceros,#agrega prefijos. La precisión da los decimales de%fy el máximo de%s. - El modificador de longitud debe coincidir con el tipo:
hh,h,l,ll,z,j. Un desacuerdo es comportamiento indefinido. fprintfescribe en un flujo,snprintfen una cadena sin desbordarla;sprintfno se usa.- Un error de escritura puede aparecer recién al vaciar el buffer: hay que comprobar
fflushofclose./dev/fullpermite probarlo. - El formato es siempre una cadena literal: pasar un texto externo como formato permite leer y escribir la memoria del proceso.
scanfdeja la entrada inválida sin consumir, no detecta desbordamientos numéricos, y%sno conoce el tamaño del destino.- La forma segura de leer es una línea completa con
fgets, que nunca desborda, y convertirla después constrtolcomprobando el resultado. getlinelee líneas de cualquier largo pidiendo memoria, que hay que liberar confree.isattyindica si un descriptor es una terminal.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Por qué, al ejecutar ./orden > todo.txt 2>&1, los avisos de stderr aparecen antes que los resultados de stdout?
Porque al redirigir a un archivo, stdout usa un buffer completo: los printf guardaron el texto en la memoria del proceso y no lo escribieron hasta el final del programa. stderr no tiene buffer, y cada fprintf fue un write inmediato. strace muestra los dos write de stderr antes del único write de stdout.
2. ¿Qué significan las partes de %-08.3ld?
% inicia el especificador; - y 0 son banderas (alinear a la izquierda y rellenar con ceros, aunque con - el 0 se ignora); 8 es el ancho mínimo; .3 la precisión, que en un entero es la cantidad mínima de dígitos; l indica que el argumento es long; y d, que se muestra como entero con signo en decimal.
3. ¿Qué diferencia hay entre %10s y %.10s?
%10s es un ancho mínimo: si la cadena tiene menos de 10 caracteres se rellena con espacios a la izquierda, y si tiene más se muestra completa. %.10s es una precisión: muestra como máximo 10 caracteres de la cadena, recortándola si es más larga.
4. ¿Por qué printf("%d", sizeof x) es un error en Linux de 64 bits?
Porque sizeof devuelve un size_t, de 8 bytes, y %d le dice a printf que lea un int, de 4. printf no conoce los tipos de sus argumentos; el desacuerdo es comportamiento indefinido. El formato correcto es %zu.
5. En lleno.c, ¿por qué printf devolvió 12 si la escritura en /dev/full falló?
Porque printf solo copió el texto al buffer de stdout, y eso funcionó. El error ocurrió después, cuando la biblioteca llamó a write para vaciar el buffer. Por eso hay que comprobar el resultado de fflush o de fclose(stdout), que es cuando se descubre el fallo.
6. ¿Por qué printf(argv[1]) es una vulnerabilidad y no solo un descuido?
Porque si el texto contiene especificadores, printf los interpreta y toma «argumentos» de los registros y de la pila: con %p o %x revela direcciones y datos del proceso, con %s lee memoria arbitraria, y con %n escribe en ella. Un atacante que controle el texto controla parte del comportamiento del programa. Se usa siempre un formato literal: printf("%s", argv[1]).
7. ¿Por qué bucle.c no logra leer el 42 de la segunda línea?
Porque scanf("%d") falla con «abc» sin consumir los caracteres: la «a» queda en la entrada. Cada vuelta del bucle vuelve a intentar convertir la misma «a» y vuelve a fallar, de modo que el bucle nunca avanza hasta el 42. Sin un límite, sería un bucle infinito.
8. ¿Qué pasó con scanf("%s", nombre) y la entrada «maximiliano»?
%s no conoce el tamaño del arreglo y copió los 11 caracteres más el \0 en un arreglo de 8 bytes, escribiendo fuera de él en la pila. El protector de pila detectó que se había pisado su valor testigo al terminar main y abortó con stack smashing detected.
9. ¿Qué hace fgets con una línea más larga que su buffer, y cómo se sabe que ocurrió?
Guarda lo que cabe, tam - 1 caracteres y el \0, y deja el resto de la línea para la siguiente llamada. Se sabe que la línea no se completó porque lo leído no termina en \n (salvo la última línea de un archivo sin salto de línea final).
10. ¿Por qué la combinación de fgets y strtol es más segura que scanf("%d")?
Porque separa la lectura de la conversión. fgets consume la línea completa sin desbordar, de modo que una entrada inválida se descarta entera y no queda en la entrada. strtol indica dónde dejó de convertir, lo que permite rechazar texto sobrante como «30 años», e informa con errno si el valor está fuera de rango, algo que scanf no puede detectar.
11. ¿Qué ventaja tiene getline sobre fgets, y qué obligación agrega?
getline lee líneas de cualquier largo, pidiendo y agrandando la memoria según haga falta, y devuelve el largo leído. A cambio, esa memoria la pidió la biblioteca para el programa, y el programa tiene que liberarla con free cuando termina de usarla.
Para practicar
- Modifica
orden.cpara que, redirigido a un archivo, conserve el orden del programa. Prueba dos soluciones:fflush(stdout)después de cadaprintf, ysetvbuf(stdout, NULL, _IOLBF, 0)al principio. Compara las llamadas awriteconstrace. - Escribe un programa que muestre, a partir de un número leído con
leer_entero, una tabla de sus representaciones en decimal, octal, hexadecimal con y sin prefijo, y con 8 dígitos rellenos de ceros. - Imita la salida de
ls -lpara tres archivos de ejemplo conprintf: permisos, cantidad de enlaces alineada a la derecha, dueño alineado a la izquierda, tamaño alineado a la derecha y nombre. - Prueba
./tabla > /dev/fully./lleno > /dev/full, y compara los códigos de salida. Agrega atabla.cla comprobación defclose(stdout). - Escribe
leer_linea(char *buf, size_t tam)que usefgets, quite el\n, y si la línea era más larga que el buffer, descarte el resto hasta el\ny devuelva un código que lo indique. - Modifica
numero.cpara queleer_enteroreciba un mínimo y un máximo, y rechace los valores fuera de ese rango con un mensaje que los indique. - Usa
getlinepara escribir un programa que muestre la línea más larga de su entrada y su número de línea. Pruébalo con./masLarga < /etc/services.