Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Campos de Bits y Operaciones Bitwise

Operadores a nivel de bits y máscaras en C

Universidad Nacional de Río Negro

Campos de Bits (Bit-fields): Ahorro Extremo de Memoria

Los bit-fields permiten definir miembros de una struct con un ancho en bits exacto, ideal para empaquetar flags o valores pequeños.

Sintaxis y Ejemplo

typedef struct {
    unsigned int activo      : 1; // 1 bit
    unsigned int modo_op     : 3; // 3 bits (valores 0-7)
    unsigned int prioridad   : 4; // 4 bits (valores 0-15)
} config_t;

El compilador empaquetará estos 8 bits en un solo byte (si es posible).

Acceso y Type Punning Seguro

Cuando se trabaja con estructuras de campos de bits o representaciones de bajo nivel, suele ser necesario interpretar una estructura empaquetada como una secuencia cruda de bytes (por ejemplo, para transmitirla por red) o viceversa.

Un error común para lograr esto es castear la dirección de la estructura directamente:

packed_byte_t data;
uint8_t byte_crudo = *(uint8_t*)&data; // ¡ERROR! Violación de strict aliasing

Esta técnica, llamada type punning mediante casteo de punteros, está prohibida en C moderno. El compilador asume que dos punteros de tipos incompatibles no apuntan al mismo objeto en memoria (regla de strict aliasing). Optimizar el código bajo este supuesto permite mejoras de rendimiento significativas, pero si violamos la regla, el compilador puede reorganizar los accesos y producir un comportamiento indefinido.

Existen dos formas válidas y seguras de realizar type punning en C:

  1. El uso de uniones (union): En C estándar, escribir en un miembro de una unión y leer de otro diferente es un comportamiento bien definido y el método preferido para reinterpretación de datos.

  2. Uso de memcpy: Copiar los bytes mediante memcpy es seguro y los optimizadores modernos suelen eliminar la llamada física a la función, generando código máquina óptimo.

Laboratorio 2: Inspección de Bit-fields con Uniones

bitfield_inspect.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdint.h>

typedef struct {
    uint8_t a : 2;
    uint8_t b : 3;
    uint8_t c : 3;
} packed_byte_t;

typedef union {
    packed_byte_t campos;
    uint8_t valor_raw;
} packed_byte_u;

int main() {
    packed_byte_u data;
    data.campos.a = 3; // 11b
    data.campos.b = 5; // 101b
    data.campos.c = 7; // 111b

    // Imprimimos la estructura de forma segura respetando el strict aliasing
    printf("sizeof(packed_byte_t) = %zu\n", sizeof(packed_byte_t));
    printf("Byte resultante: 0x%02X\n", data.valor_raw);
    return 0;
}

Compilación y Ejecución:

gcc -Wextra -Wall bitfield_inspect.c -o bitfield_inspect
./bitfield_inspect

Análisis: La salida 0xFD (o 11111101 en binario) puede parecer sorprendente. El orden en que el compilador asigna los bits dentro del byte es implementation-defined. No asumas un orden específico si necesitás portabilidad.

Solution to Exercise 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdint.h>

typedef struct {
    uint8_t es_ack : 1;
    uint8_t es_fin : 1;
    uint8_t tipo_paquete : 3;
    uint8_t checksum : 3;
} estado_paquete_t;

typedef union {
    estado_paquete_t campos;
    uint8_t byte_completo;
} paquete_decoder_t;

void imprimir_estado_paquete(uint8_t byte_estado) {
    paquete_decoder_t decoder;
    decoder.byte_completo = byte_estado;

    printf("--- Estado del Paquete (0x%02X) ---\n", byte_estado);
    printf("  ACK: %s\n", decoder.campos.es_ack ? "Sí" : "No");
    printf("  FIN: %s\n", decoder.campos.es_fin ? "Sí" : "No");
    printf("  Tipo: %u\n", decoder.campos.tipo_paquete);
    printf("  Checksum: %u\n", decoder.campos.checksum);
    printf("----------------------------------\n");
}

int main() {
    // Ejemplo: ACK=1, FIN=0, Tipo=5 (101b), Checksum=3 (011b)
    // Binario: 011 101 0 1 -> 0xDA
    uint8_t paquete = 0b01110101;
    imprimir_estado_paquete(paquete);
    return 0;
}

Operadores a Nivel de Bits

Los Campos de Bits del apartado anterior operan físicamente con bits individuales. Para manipularlos de forma explícita en código, C provee un conjunto de operadores bitwise que actúan sobre la representación binaria de los enteros.

Introducción: El Poder a Nivel de Bit

En el corazón de cada operación que realiza una computadora se encuentran los bits. La manipulación de bits es el conjunto de técnicas que nos permite interactuar directamente con esta representación binaria de los datos. En C, dominar estas técnicas es una habilidad práctica y poderosa para optimizar el rendimiento, ahorrar memoria e interactuar con hardware.

Representación Binaria

Todos los tipos de datos en C se almacenan como una secuencia de bits. Un unsigned char (1 byte) que representa el número 200, en binario es 11001000.

Bit:      7   6   5   4   3   2   1   0
Valor:  128  64  32  16   8   4   2   1
-----------------------------------------
Binario:  1   1   0   0   1   0   0   0
Suma:   128 + 64+ 0 + 0 + 8 + 0 + 0 + 0 = 200

Endianness

El endianness define el orden en que se almacenan en memoria los bytes que componen un tipo de dato multi-byte (como int o double). Es un detalle de la arquitectura del procesador que puede ser crucial en programación de sistemas, redes o al trabajar con formatos de archivo binarios.

Ejemplo con el valor 0x1A2B3C4D (un int de 32 bits):

DirecciónLittle-endianBig-endian
0x1004D1A
0x1013C2B
0x1022B3C
0x1031A4D

¿Por qué es importante?

Si escribís un int a un archivo en una máquina little-endian y lo leés en una big-endian, el valor será incorrecto.

Cómo detectar el endianness en C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h> 
#include <stdint.h> 

int main(void) {
    uint32_t i = 1;
    char *c = (char*)&i;

    if (*c) {
        printf("Little-endian\n");
    } else {
        printf("Big-endian\n");
    }

    return 0;
}

Este código funciona porque si es little-endian, el byte 01 se almacena en la primera dirección, y *c será 1. Si es big-endian, el primer byte será 00, y *c será 0.

Los Operadores a Nivel de Bits

1. AND a nivel de bits (&)

El bit del resultado es 1 solo si ambos bits correspondientes en los operandos son 1. Su uso principal es para verificar o apagar bits.

Solution to Exercise 2
#include <stdbool.h>

bool es_par(int numero) {
    // La máscara 1 (00000001) aísla el último bit.
    // Si el resultado de (numero & 1) es 0, el bit era 0.
    return (numero & 1) == 0;
}
<!-- {solution} espar -->

(2-or-a-nivel-de-bits)=
### 2. OR a nivel de bits (`|`)

El bit del resultado es `1` si al menos uno de los bits correspondientes es `1`.
Su uso principal es para **encender** bits.

:::::::::{exercise} activar
:label: activar

Encender un Flag Dado un `unsigned char` que representa un
conjunto de 8 flags, escribí una función que encienda el 4to bit (bit en la
posición 3) sin modificar los demás. :::

:::{solution} activar
:class: dropdown

```{code-block} c
void activar_flag_4(unsigned char *estado) {
    // La máscara (1 << 3) es 8 (00001000).
    // El OR encenderá ese bit sin tocar los otros.
    *estado = *estado | (1 << 3);
}

```
<!-- {code-block} c -->

3. XOR a nivel de bits (^)

El bit del resultado es 1 solo si los bits correspondientes son diferentes. Su uso principal es para alternar (toggle) bits.

Solution to Exercise 4
// Máscara para el 2do bit: (1 << 1) -> 00000010
// Máscara invertida: ~(1 << 1) -> 11111101
unsigned char mascara_apagado = ~(1 << 1);

5. Desplazamientos (<< y >>)