← Programación en C para Sistemas Operativos Bloque 2 · Tipos, operadores y control · Tema 8 de 42

Tema 8

Operadores de bits

Un sistema operativo empaqueta información en bits todo el tiempo. Los permisos de un archivo son nueve bits; las opciones de open se combinan encendiendo bits en un entero; el estado con que termina un proceso guarda el código de salida en unos bits y la señal que lo mató en otros. Para leer y escribir esa información, C tiene seis operadores que trabajan bit a bit. En este tema los vemos uno por uno, aprendemos las cuatro operaciones con máscaras que se repiten en todo programa de sistemas, y las aplicamos a casos reales: los permisos de ls -l, la umask, las banderas de open y el estado de salida de un proceso.

1. Por qué trabajar con bits

En la mayoría de los programas, un entero es un número. En la programación de sistemas, muchas veces es otra cosa: un conjunto de interruptores, cada uno de un bit, empaquetados en un solo valor. Es una forma compacta y rápida de guardar muchas condiciones de sí o no, y el núcleo y la biblioteca de C la usan por todas partes:

DóndeQué guarda cada bit o grupo de bitsLo veremos
Permisos de archivoLectura, escritura y ejecución, para el dueño, el grupo y los demás.Secciones 9 y 10
Opciones de openCrear, truncar, agregar al final, no bloquear…Sección 8, tema 38
Estado de un proceso terminadoEl código de salida, o la señal que lo terminó.Sección 11
Conjuntos de señalesUn bit por cada señal bloqueada o pendiente.Curso Procesos e Hilos
Tablas de páginasPresente, escribible, de usuario, ejecutable…Curso de memoria
Registros de dispositivosEstado y órdenes del hardware.Controladores

Ya usamos bits sin explicarlos: la función bits del tema 5 mostraba los bits de un byte, y en el tema 7 vimos que permisos & 4 == 4 no hace lo que parece. Ahora toca entenderlos.

2. Binario, octal y hexadecimal

Para trabajar con bits hay que poder escribirlos y leerlos. El binario es exacto pero largo; por eso se usan dos bases que lo resumen, porque cada uno de sus dígitos corresponde a un grupo fijo de bits:

BaseEn CBits por dígitoSe usa para
Binario0b111101101 (C23)1Ver cada bit, en ejemplos pequeños.
Octal07553Los permisos de archivo, que se agrupan de a tres.
Hexadecimal0x1ed4Todo lo demás: direcciones, máscaras, bytes (dos dígitos por byte).

Pasar de hexadecimal a binario es mecánico: cada dígito son cuatro bits, de 0 = 0000 a f = 1111. 0xca es 1100 1010; 0xff, un byte con todos los bits encendidos. Conviene memorizar los dígitos con un solo bit encendido: 1, 2, 4, 8. Con el octal pasa lo mismo de a tres bits, y por eso los permisos se escriben en octal: 7 es 111, 5 es 101, y 0755 es 111 101 101.

printf muestra un entero sin signo en cada base: %o octal, %x hexadecimal, y desde C23 y glibc 2.35, %b binario. El modificador # agrega el prefijo:

binario.c
#include <stdio.h>

int main(void)
{
    unsigned int permisos = 0b111101101;     /* C23: constante binaria */

    printf("decimal %u, octal %o, hexadecimal %x, binario %b\n",
           permisos, permisos, permisos, permisos);
    printf("con prefijos: %#o %#x %#b\n", permisos, permisos, permisos);
    return 0;
}
terminal · compilado como C23
$ gcc -std=c23 -Wall -Wextra -o binario binario.c && ./binario
decimal 493, octal 755, hexadecimal 1ed, binario 111101101
con prefijos: 0755 0x1ed 0b111101101

El mismo número: 493 en decimal no dice nada, pero 0755 se reconoce enseguida como los permisos rwxr-xr-x. Elegir la base adecuada para escribir una constante es parte de escribir código legible.

3. Los cuatro operadores lógicos de bits

Los operadores &, | y ^ toman dos enteros y combinan cada bit de uno con el bit de la misma posición del otro, de forma independiente; ~ toma un solo entero e invierte cada uno de sus bits:

OperadorNombreEl bit resultado es 1 si…Uso típico
a & bY (AND)Los dos bits son 1.Consultar o apagar bits.
a | bO (OR)Al menos uno es 1.Encender bits, combinar banderas.
a ^ bO exclusivo (XOR)Los bits son distintos.Invertir bits, comparar.
~aComplemento (NOT)El bit de a es 0.Construir la máscara inversa.

Este programa aplica cada operador a dos valores y muestra los 8 bits más bajos del resultado:

operadores.c
#include <stdio.h>

/* muestra los 8 bits más bajos de x, del más alto al más bajo */
static void bits8(unsigned int x)
{
    for (int i = 7; i >= 0; i--)
        putchar((x >> i) & 1u ? '1' : '0');
}

static void fila(const char *nombre, unsigned int valor)
{
    printf("%-8s ", nombre);
    bits8(valor);
    printf("  0x%02x  %3u\n", valor & 0xffu, valor & 0xffu);
}

int main(void)
{
    unsigned int a = 0xcau;      /* 1100 1010 */
    unsigned int b = 0x5cu;      /* 0101 1100 */

    fila("a", a);
    fila("b", b);
    fila("a & b", a & b);
    fila("a | b", a | b);
    fila("a ^ b", a ^ b);
    fila("~a", ~a);
    fila("a << 1", a << 1);
    fila("a >> 3", a >> 3);
    return 0;
}
terminal
$ ./operadores
a        11001010  0xca  202
b        01011100  0x5c   92
a & b    01001000  0x48   72
a | b    11011110  0xde  222
a ^ b    10010110  0x96  150
~a       00110101  0x35   53
a << 1   10010100  0x94  148
a >> 3   00011001  0x19   25

Leyendo las columnas de bits de arriba abajo se ve cada regla. En a & b solo quedan los dos bits que estaban encendidos en ambos; en a | b, todos los que estaban en alguno; en a ^ b, los que estaban en uno solo. ~a invierte cada bit; aquí vemos solo los 8 más bajos, pero ~ invierte los 32 bits del unsigned int, y los 24 de arriba, que eran ceros, ahora son unos. La sección 13 muestra por qué eso importa.

Dos propiedades del XOR merecen recordarse: aplicar dos veces el mismo XOR deja el valor como estaba, (a ^ b) ^ b == a, y a ^ a es siempre 0. La primera es la base de la operación «invertir» de la sección 6, y de muchas técnicas de cifrado y de detección de errores. Y no hay que confundir estos operadores con los lógicos del tema 7: & y | operan sobre cada bit y siempre evalúan los dos operandos; && y || tratan cada operando como un todo verdadero o falso, y hacen cortocircuito.

4. Los desplazamientos

x << n desplaza los bits de x n posiciones hacia la izquierda, rellenando con ceros por la derecha; los bits que salen por la izquierda se pierden. x >> n los desplaza hacia la derecha. Como cada posición vale el doble de la anterior, desplazar a la izquierda multiplica por 2n, y desplazar a la derecha un número sin signo divide por 2n, descartando el resto:

desplazar.c
#include <stdio.h>

int main(void)
{
    unsigned int x = 5;
    int negativo = -16;

    printf("5 << 1 = %u,  5 << 3 = %u,  40 >> 2 = %u\n", x << 1, x << 3, 40u >> 2);
    printf("-16 >> 2 = %d\n", negativo >> 2);
    printf("1u << 31 = %u\n", 1u << 31);
    printf("1ULL << 40 = %llu\n", 1ULL << 40);
    printf("1 << 31 = %d\n", 1 << 31);
    printf("1 << 40 = %d\n", 1 << 40);
    return 0;
}
terminal · compilado normalmente, y con el sanitizador
$ gcc -std=c17 -Wall -Wextra -o desplazar desplazar.c
desplazar.c: In function ‘main’:
desplazar.c:13:32: warning: left shift count >= width of type [-Wshift-count-overflow]
   13 |     printf("1 << 40 = %d\n", 1 << 40);
      |                                ^~
$ ./desplazar
5 << 1 = 10,  5 << 3 = 40,  40 >> 2 = 10
-16 >> 2 = -4
1u << 31 = 2147483648
1ULL << 40 = 1099511627776
1 << 31 = -2147483648
1 << 40 = 0
$ gcc -std=c17 -Wall -Wextra -fsanitize=undefined -o desplazar desplazar.c 2>/dev/null && ./desplazar 2>&1 | grep runtime
desplazar.c:12:32: runtime error: left shift of 1 by 31 places cannot be represented in type 'int'
desplazar.c:13:32: runtime error: shift exponent 40 is too large for 32-bit type 'int'

La expresión más importante de esa lista es 1u << n: un número con un solo bit encendido, el de la posición n. Es la forma de construir cualquier máscara de un bit, y la usaremos en todo el tema. Las cuatro primeras líneas son correctas; las dos últimas son comportamiento indefinido, y la sección siguiente explica por qué.

El caso de -16 >> 2 merece un comentario: desplazar a la derecha un número negativo da un resultado definido por la implementación. gcc hace un desplazamiento aritmético: rellena por la izquierda con copias del bit de signo, de modo que el número sigue siendo negativo y el resultado es −4, la división por 4. Otro compilador podría rellenar con ceros. Desde C23 se garantiza la representación en complemento a dos, pero el desplazamiento de negativos sigue sin estar definido del todo; la solución, como con todas las operaciones de bits, es trabajar con tipos sin signo.

5. Lo que no se debe desplazar

Los desplazamientos tienen tres casos de comportamiento indefinido, y los tres son fáciles de cometer:

CasoEjemploPor quéCorrección
Desplazar tanto o más que el ancho del tipo1 << 40, 1u << 32El estándar no define el resultado; cada procesador hace algo distinto.Usar un tipo más ancho: 1ULL << 40.
Desplazar un 1 hasta el bit de signo1 << 31 con intEl resultado no se puede representar en el tipo con signo.1u << 31.
Desplazar una cantidad negativax << -1No es un desplazamiento hacia el otro lado.Comprobar la cantidad.

El primer caso tiene una trampa adicional cuando la cantidad viene de una variable, porque entonces el compilador no puede advertirlo:

terminal · variable.c: muestra 1u << n, con n tomado del primer argumento
$ ./variable 31
1 << 31 = 2147483648
$ ./variable 32
1 << 32 = 1
$ ./variable 33
1 << 33 = 2
$ gcc -std=c17 -Wall -Wextra -fsanitize=undefined -o variable variable.c && ./variable 32
variable.c:9:37: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
1 << 32 = 1

Uno podría esperar que 1u << 32 diera 0, porque el bit «se cae» por la izquierda. En x86-64 da 1: la instrucción de desplazamiento del procesador solo mira los 5 bits más bajos de la cantidad, y 32 es 100000, cuyos 5 bits bajos son 0. Desplazar 33 equivale a desplazar 1. En un procesador ARM, el mismo programa daría 0. No hay una respuesta correcta, porque el lenguaje no la define. Si un programa calcula la cantidad de desplazamiento, tiene que comprobar que sea menor que el ancho del tipo.

6. Las cuatro operaciones con máscaras

Una máscara es un valor que tiene encendidos exactamente los bits sobre los que se quiere operar. Con una máscara y los operadores de bits se hacen cuatro operaciones, que conviene aprender como frases hechas, porque se repiten en todo el código de sistemas:

OperaciónExpresiónPor qué funciona
Encender los bits de la máscarax |= mascara;OR con 1 da 1; OR con 0 deja el bit como estaba.
Apagar los bits de la máscarax &= ~mascara;~mascara tiene 0 en esos bits y 1 en el resto; AND con 0 da 0, AND con 1 deja el bit.
Invertir los bits de la máscarax ^= mascara;XOR con 1 invierte; XOR con 0 deja el bit.
Consultar si alguno está encendido(x & mascara) != 0AND deja solo los bits de la máscara; si queda alguno, no es cero.
Consultar si todos están encendidos(x & mascara) == mascaraDeben quedar exactamente los bits de la máscara.

La operación de apagar es la que más cuesta al principio: x &= ~mascara, con el ~. Escribir x &= mascara hace lo contrario: apaga todo menos los bits de la máscara. Y la de consultar lleva paréntesis obligatorios, por la precedencia que vimos en el tema 7. Aplicada a un solo bit, la máscara se escribe con el desplazamiento: x |= 1u << 5 enciende el bit 5, y (x >> 5) & 1u lo lee como 0 o 1, que es lo que hacía la función bits.

7. Banderas

Una bandera (flag) es una condición de sí o no representada por un bit. Cuando una función o un programa tiene varias opciones independientes, se define una constante por cada una, con un bit distinto, y el conjunto de opciones elegidas es un solo entero con los bits correspondientes encendidos. Este programa simula las opciones de una herramienta de línea de órdenes:

banderas.c
#include <stdio.h>

/* opciones de una herramienta, una por bit */
#define OPT_VERBOSO   (1u << 0)
#define OPT_RECURSIVO (1u << 1)
#define OPT_FORZAR    (1u << 2)
#define OPT_OCULTOS   (1u << 3)

static void mostrar(const char *momento, unsigned int opciones)
{
    printf("%-22s 0x%02x:", momento, opciones);
    if (opciones & OPT_VERBOSO)
        printf(" verboso");
    if (opciones & OPT_RECURSIVO)
        printf(" recursivo");
    if (opciones & OPT_FORZAR)
        printf(" forzar");
    if (opciones & OPT_OCULTOS)
        printf(" ocultos");
    printf("\n");
}

int main(void)
{
    unsigned int opciones = 0;

    opciones |= OPT_VERBOSO | OPT_FORZAR;          /* encender */
    mostrar("encender V y F", opciones);

    opciones |= OPT_RECURSIVO;                     /* encender otra */
    mostrar("encender R", opciones);

    opciones &= ~OPT_FORZAR;                       /* apagar */
    mostrar("apagar F", opciones);

    opciones ^= OPT_OCULTOS;                       /* invertir */
    mostrar("invertir O", opciones);
    opciones ^= OPT_OCULTOS;
    mostrar("invertir O otra vez", opciones);

    if ((opciones & (OPT_VERBOSO | OPT_RECURSIVO)) == (OPT_VERBOSO | OPT_RECURSIVO))
        printf("verboso y recursivo están las dos encendidas\n");
    return 0;
}
terminal
$ ./banderas
encender V y F         0x05: verboso forzar
encender R             0x07: verboso recursivo forzar
apagar F               0x03: verboso recursivo
invertir O             0x0b: verboso recursivo ocultos
invertir O otra vez    0x03: verboso recursivo
verboso y recursivo están las dos encendidas

Hay varios detalles de estilo en ese programa que vale la pena copiar. Las constantes se definen con 1u << n, que deja ver qué bit es cada una y evita el problema del bit de signo. Van entre paréntesis, porque una macro se pega como texto en la expresión donde se usa, y sin ellos la precedencia podría cambiar el resultado, como veremos en el tema 14. Y para combinar banderas se usa |, no +: con banderas distintas dan lo mismo, pero si una bandera aparece dos veces, OPT_FORZAR | OPT_FORZAR sigue siendo esa bandera, mientras que OPT_FORZAR + OPT_FORZAR es otro bit.

En mostrar, la consulta se escribe if (opciones & OPT_VERBOSO) sin != 0: como cualquier valor distinto de cero es verdadero, es equivalente, y es la forma habitual. Lo que no se puede hacer es comparar con 1, (opciones & OPT_FORZAR) == 1, porque el resultado del AND es 4, no 1.

8. Las banderas de open

La llamada al sistema open, que abre un archivo y que usaremos en el tema 38, recibe sus opciones exactamente así. Su SYNOPSIS dice int open(const char *pathname, int flags, ...), y las banderas se definen en fcntl.h:

terminal · los valores de algunas banderas de open en Linux
$ grep -n 'define O_WRONLY\|define O_CREAT\|define O_TRUNC\|define O_APPEND' /usr/include/x86_64-linux-gnu/bits/fcntl-linux.h
44:#define O_WRONLY	     01
47:# define O_CREAT	   0100	/* Not fcntl.  */
56:# define O_TRUNC	  01000	/* Not fcntl.  */
59:# define O_APPEND	  02000
$ ./of
O_RDONLY=0 O_WRONLY=01 O_RDWR=02 O_CREAT=0100 O_TRUNC=01000 O_APPEND=02000
O_WRONLY | O_CREAT | O_TRUNC = 01101

Cada bandera es un bit, escrito en octal por tradición. O_WRONLY | O_CREAT | O_TRUNC, «abrir para escribir, crear si no existe y vaciarlo si existe», es la combinación que usa la shell para una redirección >, y vale 01101: los tres bits juntos. Con strace, que vimos en el tema 1, se ve que la shell las usa así:

terminal · la llamada que hace bash para una redirección >
$ strace -e trace=openat bash -c 'echo hola > salida.txt' 2>&1 | grep salida
openat(AT_FDCWD, "salida.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3

strace descompone el entero de banderas en sus bits y muestra el nombre de cada uno. El último argumento, 0666, son los permisos que la shell pide para el archivo si lo crea; la sección 10 explica por qué el archivo no termina con esos permisos.

Hay una excepción instructiva: O_RDONLY vale 0. No es un bit, sino la ausencia de O_WRONLY y O_RDWR, de modo que flags & O_RDONLY es siempre 0 y no sirve para consultar nada. Los dos bits más bajos forman un campo con tres valores posibles, y se consulta con su propia máscara: (flags & O_ACCMODE) == O_RDONLY. Los manuales avisan de estos casos, y es una razón más para leerlos.

9. Los permisos de un archivo

Los permisos que muestra ls -l son los 9 bits más bajos del campo st_mode que el núcleo guarda para cada archivo, más 3 bits especiales por encima:

Los 12 bits bajos de st_mode para el modo 0755: tres bits especiales (setuid, setgid, sticky) apagados, que forman el dígito octal 0; los bits del usuario r, w y x encendidos, que forman el 7; los del grupo r y x encendidos y w apagado, que forman el 5; y los de los demás r y x encendidos y w apagado, que forman otro 5 especiales usuario grupo otros s g t r w x r w x r w x 0 0 0 1 1 1 1 0 1 1 0 1 4 2 1 4 2 1 4 2 1 4 2 1 0 7 5 5 Cada grupo de tres bits es un dígito octal: lectura vale 4, escritura 2 y ejecución 1.
El modo 0755, bit a bit. En verde, los bits encendidos: rwxr-xr-x.

Por eso los permisos se escriben en octal: cada dígito es un grupo de tres bits, y basta sumar 4, 2 y 1. sys/stat.h define una constante para cada bit, con nombres que siguen el patrón S_I + permiso (R, W, X) + a quién (USR, GRP, OTH):

terminal · sc.c: el valor de algunas constantes de sys/stat.h
$ ./sc
S_IRUSR=0400 S_IWUSR=0200 S_IXUSR=0100 S_IRWXG=0070 S_IROTH=0004 S_IWOTH=0002 S_ISUID=4000

Con eso podemos escribir una pequeña versión de lo que hace ls -l con los permisos. Este programa usa stat, una llamada al sistema que llena una estructura con la información de un archivo; las estructuras son del bloque 6, pero aquí solo nos interesa su campo st_mode:

permisos.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <sys/stat.h>

/* convierte los 9 bits de permisos en el texto rwxr-xr-x de ls */
static void texto_permisos(mode_t modo, char salida[10])
{
    const char letras[] = "rwxrwxrwx";

    for (int i = 0; i < 9; i++)
        salida[i] = (modo & (1u << (8 - i))) ? letras[i] : '-';
    salida[9] = '\0';
}

int main(int argc, char *argv[])
{
    for (int i = 1; i < argc; i++) {
        struct stat info;
        char texto[10];

        if (stat(argv[i], &info) != 0) {
            perror(argv[i]);
            continue;
        }
        texto_permisos(info.st_mode, texto);
        printf("%s  %04o  %s%s\n", texto, (unsigned int) (info.st_mode & 07777),
               argv[i], S_ISDIR(info.st_mode) ? "/" : "");
        if ((info.st_mode & S_IWOTH) != 0)
            printf("   atención: cualquiera puede modificar %s\n", argv[i]);
    }
    return 0;
}
terminal · comparado con ls -l
$ ./permisos notas.txt dir publico.txt permisos /etc/shadow noexiste
rw-r--r--  0644  notas.txt
rwxr-xr-x  0755  dir/
rw-rw-rw-  0666  publico.txt
   atención: cualquiera puede modificar publico.txt
rwxr-xr-x  0755  permisos
rw-r-----  0640  /etc/shadow
noexiste: No such file or directory
$ ls -ld notas.txt dir publico.txt /etc/shadow
-rw-r--r-- 1 ana  ana       0 oct 15 22:28 notas.txt
drwxr-xr-x 2 ana  ana    4096 oct 15 22:28 dir
-rw-rw-rw- 1 ana  ana       0 oct 15 22:28 publico.txt
-rw-r----- 1 root shadow  777 oct 12 04:44 /etc/shadow

El bucle de texto_permisos recorre los 9 bits del más alto al más bajo: para la letra i, construye la máscara 1u << (8 - i), consulta el bit, y pone la letra o un guion. st_mode & 07777 descarta los bits superiores de st_mode, que guardan el tipo de archivo, y deja los 12 de permisos; S_ISDIR es una macro que consulta precisamente esos bits de tipo. Y la advertencia final es una consulta con una máscara de la biblioteca, S_IWOTH. Un detalle: stat pudo leer los permisos de /etc/shadow, aunque no podamos leer su contenido; los permisos protegen los datos del archivo, no su información.

10. La umask

Cuando un programa crea un archivo, pide unos permisos, y el núcleo no se los da tal cual: les quita los bits que estén encendidos en la umask del proceso, una máscara que cada proceso hereda de su padre. La cuenta es exactamente la operación «apagar» de la sección 6: permisos_finales = pedidos & ~umask.

terminal · la misma creación, con dos umask distintas
$ umask
0022
$ touch nuevo1; mkdir nuevod
$ umask 077; touch nuevo2; umask 022
$ ls -ld nuevo1 nuevo2 nuevod
-rw-r--r-- 1 ana ana    0 oct 15 22:28 nuevo1
-rw------- 1 ana ana    0 oct 15 22:28 nuevo2
drwxr-xr-x 2 ana ana 4096 oct 15 22:28 nuevod
$ printf '%o %o\n' $(( 0666 & ~022 )) $(( 0666 & ~077 ))
644 600

touch pide 0666, lectura y escritura para todos, y mkdir pide 0777. Con la umask habitual, 022, el núcleo apaga el bit de escritura del grupo y de los demás: 0666 & ~022 es 0644, y 0777 & ~022 es 0755. Con 077, se apaga todo lo que no sea del dueño. La última orden hace la misma cuenta en la aritmética de Bash, que tiene los mismos operadores de bits que C. La umask no es una resta: 0666 - 077 daría 0567, que no tiene sentido. Es una máscara de bits a apagar, y por eso se aplica con & ~.

11. Extraer campos

No todos los datos empaquetados son de un bit. A veces un grupo de bits forma un número, un campo, y para leerlo se combinan las dos herramientas: desplazar el campo hasta la posición 0 y quitar con una máscara lo que quede a la izquierda. La fórmula general es (x >> posicion) & mascara_del_ancho.

Un ejemplo real es el estado que el núcleo informa cuando termina un proceso hijo. system, de stdlib.h, ejecuta una orden de la shell y devuelve ese estado, que empaqueta en un solo int el código de salida y la señal que terminó el proceso:

estado.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(void)
{
    int estado = system("exit 3");

    printf("system devolvió %d = 0x%04x\n", estado, (unsigned int) estado);
    printf("a mano:      código de salida = %d\n", (estado >> 8) & 0xff);
    printf("WEXITSTATUS: código de salida = %d\n", WEXITSTATUS(estado));

    estado = system("kill -9 $$");
    printf("system devolvió %d = 0x%04x\n", estado, (unsigned int) estado);
    printf("a mano:      señal = %d\n", estado & 0x7f);
    printf("WTERMSIG:    señal = %d\n", WTERMSIG(estado));
    return 0;
}
terminal · y cómo define glibc las macros
$ ./estado
system devolvió 768 = 0x0300
a mano:      código de salida = 3
WEXITSTATUS: código de salida = 3
system devolvió 9 = 0x0009
a mano:      señal = 9
WTERMSIG:    señal = 9
$ grep -n 'define.__WEXITSTATUS\|define.__WTERMSIG' /usr/include/x86_64-linux-gnu/bits/waitstatus.h
28:#define	__WEXITSTATUS(status)	(((status) & 0xff00) >> 8)
31:#define	__WTERMSIG(status)	((status) & 0x7f)

system no devolvió 3, sino 768: el código de salida está en el segundo byte del estado, 0x0300, y hay que desplazarlo 8 posiciones para leerlo. Cuando la shell murió por la señal 9, SIGKILL, el estado fue 9: la señal está en los 7 bits más bajos. Esta es la otra cara del byte de código de salida del tema 3: el núcleo solo reserva 8 bits para él porque comparte el entero con otra información. Las macros de sys/wait.h hacen exactamente lo mismo que nuestra versión a mano, como muestra su definición, y son las que se usan en la práctica: el formato es interno de cada sistema, y las macros lo esconden. Las usaremos con wait en el curso Procesos e Hilos.

12. Trucos con bits

Algunas operaciones aparecen con frecuencia en el código de sistemas, sobre todo en los administradores de memoria, y tienen soluciones de bits muy compactas:

trucos.c
#include <stdio.h>
#include <stdbit.h>

static int es_potencia_de_2(unsigned int x)
{
    return x != 0 && (x & (x - 1)) == 0;
}

static unsigned long redondear_a_8(unsigned long n)
{
    return (n + 7) & ~7ul;
}

int main(void)
{
    unsigned int valores[] = {1, 6, 64, 100, 4096};

    for (int i = 0; i < 5; i++) {
        unsigned int x = valores[i];
        printf("%5u: potencia de 2: %s, bits en 1: %u, bit más bajo: %u\n",
               x, es_potencia_de_2(x) ? "sí" : "no",
               stdc_count_ones(x), x & -x);
    }
    for (unsigned long n = 13; n <= 17; n++)
        printf("redondear_a_8(%lu) = %lu\n", n, redondear_a_8(n));
    return 0;
}
terminal · compilado como C23
$ gcc -std=c23 -Wall -Wextra -o trucos trucos.c && ./trucos
    1: potencia de 2: sí, bits en 1: 1, bit más bajo: 1
    6: potencia de 2: no, bits en 1: 2, bit más bajo: 2
   64: potencia de 2: sí, bits en 1: 1, bit más bajo: 64
  100: potencia de 2: no, bits en 1: 3, bit más bajo: 4
 4096: potencia de 2: sí, bits en 1: 1, bit más bajo: 4096
redondear_a_8(13) = 16
redondear_a_8(14) = 16
redondear_a_8(15) = 16
redondear_a_8(16) = 16
redondear_a_8(17) = 24
ExpresiónQué calculaPor qué funciona
(x & (x - 1)) == 0Si x es potencia de 2 (con x != 0).Restar 1 apaga el bit más bajo encendido y enciende los de abajo; si era el único, el AND da 0. Tamaños de página y de bloque son siempre potencias de 2.
x & -xEl bit encendido más bajo.En complemento a dos, -x es ~x + 1, que coincide con x solo en ese bit.
(n + 7) & ~7n redondeado hacia arriba al múltiplo de 8.Sumar 7 pasa al siguiente múltiplo salvo que ya lo fuera; apagar los 3 bits bajos baja al múltiplo. Así alinean la memoria malloc y el compilador (tema 25).
stdc_count_ones(x)Cuántos bits están en 1.C23, stdbit.h; antes, __builtin_popcount de gcc. Usa una instrucción del procesador.

stdbit.h trae otras funciones útiles, como stdc_leading_zeros o stdc_bit_width. Un consejo general: los trucos de bits valen la pena cuando expresan una operación conocida, como alinear o comprobar una potencia de 2, y conviene encapsularlos en una función con un nombre claro, como en trucos.c. Reemplazar x * 8 por x << 3 «para optimizar», en cambio, no gana nada: el compilador ya lo hace solo, y el código queda menos claro.

13. Bits, signo y promoción

Los operadores de bits están sujetos a las mismas promociones y conversiones del tema 7, y es ahí donde aparecen sus peores sorpresas. La que anticipamos entonces:

sorpresa.c
#include <stdio.h>

int main(void)
{
    unsigned char byte = 0x0f;

    printf("~byte vale %d\n", ~byte);
    if (~byte == 0xf0)
        printf("~byte es 0xf0\n");
    else
        printf("~byte NO es 0xf0\n");

    unsigned char invertido = ~byte;
    printf("guardado en un unsigned char: 0x%02x\n", invertido);
    printf("con máscara: 0x%02x\n", ~byte & 0xff);
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -o sorpresa sorpresa.c
sorpresa.c: In function ‘main’:
sorpresa.c:8:15: warning: comparison of promoted bitwise complement of an unsigned value with constant [-Wsign-compare]
    8 |     if (~byte == 0xf0)
      |               ^~
$ ./sorpresa
~byte vale -16
~byte NO es 0xf0
guardado en un unsigned char: 0xf0
con máscara: 0xf0

Antes de aplicar ~, el unsigned char se promueve a int: 0x0f pasa a ser 0x0000000f, y su complemento es 0xfffffff0, que como int vale −16. La comparación con 0xf0 falla. El compilador lo advierte con un mensaje muy específico, porque es un error clásico. Las dos soluciones son volver a guardar el resultado en un unsigned char, que conserva solo los 8 bits bajos, o aplicar una máscara, & 0xff.

De esta sección y de la 4 sale la regla más importante del tema: las operaciones de bits se hacen con tipos sin signo, preferentemente de tamaño conocido como uint32_t o uint8_t, y con constantes sin signo como 1u. Con signo, el bit más alto tiene un significado especial, los desplazamientos pueden ser indefinidos, y el resultado se interpreta como negativo cuando no corresponde.

14. Errores frecuentes

ErrorQué pasaCorrección
if (x & MASK == MASK)== se evalúa primero.if ((x & MASK) == MASK)
x &= MASK; para apagarApaga todo lo demás y deja la máscara.x &= ~MASK;
(x & FLAG) == 1El AND da el valor del bit (4, 8…), no 1.(x & FLAG) != 0
flags & O_RDONLYSiempre 0: O_RDONLY no es un bit.(flags & O_ACCMODE) == O_RDONLY
1 << 31Comportamiento indefinido: llega al bit de signo de un int.1u << 31
1 << 40, o 1u << n con n >= 32Comportamiento indefinido; en x86-64, 1u << 32 da 1.1ULL << 40; comprobar n.
Desplazar a la derecha un negativoDefinido por la implementación.Usar tipos sin signo.
~byte == 0xf0 con unsigned charLa promoción a int enciende los 24 bits altos.(~byte & 0xff) == 0xf0
Combinar banderas con +Una bandera repetida se suma y produce otro bit.Combinar con |.
Confundir & con &&1 & 2 es 0; 1 && 2 es 1.&& para condiciones, & para bits.
Escribir permisos en decimal: chmod(f, 755)755 decimal es 01363 octal: permisos absurdos.0755, con el cero, o las constantes S_I….

15. Resumen

16. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué los permisos de archivo se escriben en octal y las máscaras de bytes en hexadecimal?

Porque cada dígito octal representa exactamente 3 bits, y los permisos se agrupan de a tres: lectura, escritura y ejecución para cada clase de usuario. Cada dígito hexadecimal representa 4 bits, de modo que un byte son exactamente dos dígitos, lo que hace al hexadecimal ideal para bytes, direcciones y máscaras en general.

2. Con a = 0xca y b = 0x5c, ¿cuánto valen a & b, a | b y a ^ b?

a es 11001010 y b es 01011100. El AND deja los bits comunes, 01001000 = 0x48; el OR, todos los encendidos en alguno, 11011110 = 0xde; el XOR, los que difieren, 10010110 = 0x96.

3. ¿Cómo se apaga el bit 3 de x sin tocar los demás, y por qué hace falta el ~?

Con x &= ~(1u << 3);. 1u << 3 tiene encendido solo el bit 3; su complemento tiene todos los bits encendidos salvo ese. El AND con esa máscara deja intactos los bits donde la máscara tiene 1 y pone en 0 el bit 3. Sin el ~, el AND apagaría todos los bits salvo el 3.

4. ¿Por qué 1 << 31 es comportamiento indefinido y 1u << 31 no?

Porque 1 es un int con signo, y llevar el 1 hasta el bit 31 produce un valor que no se puede representar como int positivo: el bit de signo. 1u es un unsigned int, donde el bit 31 es un bit más, y el resultado, 2147483648, es válido.

5. ¿Por qué ./variable 32 muestra 1 y no 0 en x86-64?

Porque desplazar tanto o más que el ancho del tipo es comportamiento indefinido, y el compilador simplemente usa la instrucción del procesador. En x86-64, esa instrucción solo considera los 5 bits más bajos de la cantidad para un operando de 32 bits, y 32 tiene esos 5 bits en cero: es como desplazar 0. En otros procesadores el resultado sería distinto.

6. ¿Por qué no sirve if ((opciones & OPT_FORZAR) == 1) con OPT_FORZAR igual a 1u << 2?

Porque el AND conserva el bit en su posición: si la bandera está encendida, el resultado es 4, no 1. La comparación correcta es != 0, o simplemente usar el resultado como condición.

7. ¿Por qué flags & O_RDONLY no sirve para saber si un archivo se abrió solo para lectura?

Porque O_RDONLY vale 0: no es un bit, sino uno de los tres valores posibles del campo formado por los dos bits más bajos, junto con O_WRONLY y O_RDWR. Un AND con 0 da siempre 0. Hay que aislar el campo con su máscara y comparar: (flags & O_ACCMODE) == O_RDONLY.

8. Con una umask de 027, ¿qué permisos tiene un archivo creado con 0666? ¿Y un directorio con 0777?

0666 & ~027: la umask apaga la escritura del grupo y todos los permisos de los demás, y queda 0640, rw-r-----. El directorio queda 0777 & ~027 = 0750, rwxr-x---.

9. ¿Por qué system("exit 3") devuelve 768, y cómo se obtiene el 3?

Porque system devuelve el estado de terminación del proceso, que guarda el código de salida en el segundo byte: 3 desplazado 8 posiciones es 768, 0x0300. Se obtiene con (estado >> 8) & 0xff, o mejor con la macro WEXITSTATUS(estado), que hace lo mismo y es portable.

10. ¿Por qué (x & (x - 1)) == 0 detecta las potencias de 2?

Una potencia de 2 tiene un solo bit encendido. Restarle 1 apaga ese bit y enciende todos los de abajo, de modo que x y x - 1 no tienen ningún bit en común y el AND da 0. Si x tiene más de un bit encendido, el más alto sobrevive a la resta y el AND no es 0. Hay que excluir aparte el 0.

11. ¿Por qué ~byte == 0xf0 es falso con unsigned char byte = 0x0f?

Por la promoción entera: antes de aplicar ~, byte se convierte a int, 0x0000000f, y su complemento es 0xfffffff0, que vale −16 y no es igual a 0xf0. Se corrige aplicando una máscara, (~byte & 0xff) == 0xf0.

Para practicar

  1. Escribe cuatro funciones, encender, apagar, invertir y consultar, que reciban un uint32_t y un número de bit, y devuelvan el resultado. Haz que rechacen números de bit mayores que 31.
  2. Extiende permisos.c para que muestre también el primer carácter de ls -l: d para directorios, l para enlaces simbólicos (usa lstat y S_ISLNK) y - para archivos comunes. Pruébalo con /usr/bin/passwd y averigua qué bit especial tiene encendido.
  3. Escribe un programa que reciba un modo en octal como argumento, por ejemplo ./modo 0750 (usa strtol(argv[1], NULL, 8)), y muestre el texto rwx y qué constantes S_I… están encendidas.
  4. Calcula a mano y luego con un programa los permisos finales de un archivo creado con 0666 y de un directorio creado con 0777 para las umask 000, 002, 022 y 077.
  5. Guarda una dirección IPv4, como 192.168.1.20, en un uint32_t construyéndola con desplazamientos y |, y luego extrae cada uno de los cuatro bytes con desplazamientos y máscaras para volver a mostrarla.
  6. Modifica estado.c para probar exit 0, exit 255, exit 256 y kill -15 $$, y explica cada estado con las macros WIFEXITED, WEXITSTATUS, WIFSIGNALED y WTERMSIG.
  7. Escribe redondear(n, a) que redondee n hacia arriba al múltiplo de a, sabiendo que a es una potencia de 2, y verifica con es_potencia_de_2 antes de calcular.