Skip to content

A small project that aims to learn how endianness is used to order bytes and to understand the process and related concepts. This project includes a brief implementation and comments explaining the process so you don't get lost.

License

Notifications You must be signed in to change notification settings

imlostish/endianness-playground

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 

Repository files navigation

Endian Playground

A small project that aims to learn how endianness is used to order bytes and to understand the process and related concepts. This project includes a brief implementation and comments explaining the process so you don't get lost.

Spanish: Un pequeño proyecto que busca aprender cómo se usa el orden de bytes y comprender el proceso y los conceptos relacionados. Este proyecto incluye una breve implementación y comentarios que explican el proceso para que no se pierda.


English

Original:   0x12345678
Bytes:      12    34    56    78
            ^     ^     ^     ^
           B3    B2    B1    B0

After swap:
            78    56    34    12
            ^     ^     ^     ^
           B3    B2    B1    B0

Result:  0x78563412

Swap is a function used to reverse the byte order, if the value is reversed twice it returns the original value.

What happens if you swap a uint32 to a uint16_t?

You lose data because the uint16_t swap function only expects 2 bytes, and you give it a 4-byte value. It interprets the remaining bits as nonexistent, which causes incorrect behavior.

uint32_t number = 0x12345678;

uint16_t unsafe_swap = swap_uint16(number);
uint16_t unsafe_swap2 = swap_uint16(unsafe_swap);
Original: 0x12345678
> Unsafe swap: 0x00007856
> Unsafe swap2: 0x00005678

Bitmask is a value that allows you to isolate, extract, or clean certain bits from binary strings using a bitwise operation (such as &, |, ^, ~).

And this is precisely what we need to obtain the MSB and LSB, and move the intermediate bytes to the correct positions to correctly form a BE and LE.

Mask Binary Goal
0x000000FF 00000000 00000000 00000000 11111111 Extract LSByte (0x78)
0x0000FF00 00000000 00000000 11111111 00000000 Extract the second byte (0x56)
0x00FF0000 00000000 11111111 00000000 00000000 Extract the third byte (0x34)
0xFF000000 11111111 00000000 00000000 00000000 Extract MSByte (0x12)

How are they used?

(val & 0x000000FF) << 24Moves byte 0 (LSByte) to byte location 3 (MSByte)
(val & 0x0000FF00) << 8Moves byte 1 to byte location 2
(val & 0x00FF0000) >> 8Moves byte 2 to byte location 1
(val & 0xFF000000) >> 24Moves byte 3 to byte location 0

This operation is repeated from 32 bits in ascending order.

How are these masks constructed?

// 0xFF = 11111111 → 1byte
// << (8 * i) → Move 0xFF to the byte you are interested in
uint32_t mask_byte = 0xFF << (8 * <i>);
// mask_byte = 0x000000FF
// mask_byte = 0x0000FF00
// mask_byte = 0x00FF0000
// mask_byte = 0xFF000000

With this, I get the exact position of the byte I'm interested in; all we need to do is move it. To represent this, we have a good example in endian.c → swap_uint32.

uint32_t swap_uint32(uint32_t val) {
    return ((val & 0x000000FF) << 24) | // Byte 0 → Byte 3
           ((val & 0x0000FF00) << 8)  | // Byte 1 → Byte 2
           ((val & 0x00FF0000) >> 8)  | // Byte 2 → Byte 1
           ((val & 0xFF000000) >> 24);  // Byte 3 → Byte 0
}

The (val & 0x000000FF) operation isolates the desired byte. With >> <i> or << <i> we move the byte position, where <i> can be the size in bits of how many bytes we want to move.

Extra: You use & to extract, | to join, ^ to alternate, ~ to negate.

This pattern generalizes to any type (uint8_t, uint32_t, uint64_t, even raw structs).

Serialize and Deserialize, Alignment and padding of struct in C


Spanish

Original:   0x12345678
Bytes:      12    34    56    78
            ^     ^     ^     ^
           B3    B2    B1    B0

After swap:
            78    56    34    12
            ^     ^     ^     ^
           B3    B2    B1    B0

Result:  0x78563412

Swap es una función que se utiliza para invertir el orden de bytes. Si el valor se invierte dos veces, devuelve el valor original.

¿Qué ocurre si se intercambia un uint32 por un uint16_t?

Se pierden datos porque la función de intercambio uint16_t solo espera 2 bytes y se le asigna un valor de 4 bytes. Interpreta los bits restantes como inexistentes, lo que provoca un comportamiento incorrecto.

uint32_t number = 0x12345678;

uint16_t unsafe_swap = swap_uint16(number);
uint16_t unsafe_swap2 = swap_uint16(unsafe_swap);
Original: 0x12345678
> Unsafe swap: 0x00007856
> Unsafe swap2: 0x00005678

La máscara de bits es un valor que permite aislar, extraer o limpiar ciertos bits de cadenas binarias mediante una operación bit a bit (como &, |, ^, ~).

Y esto es precisamente lo que necesitamos para obtener el bit más significativo (MSB) y el bit menos significativo (LSB), y mover los bytes intermedios a las posiciones correctas para formar correctamente un bit inicial (BE) y un bit final (LE).

Mask Binary Goal
0x000000FF 00000000 00000000 00000000 11111111 Extract LSByte (0x78)
0x0000FF00 00000000 00000000 11111111 00000000 Extract the second byte (0x56)
0x00FF0000 00000000 11111111 00000000 00000000 Extract the third byte (0x34)
0xFF000000 11111111 00000000 00000000 00000000 Extract MSByte (0x12)

Esta operación se repite desde los 32 bits en orden ascendente.

¿Cómo se construyen estas máscaras?

(val & 0x000000FF) << 24Mueve el byte 0 (LSByte) a la posición de byte 3 (MSByte)
(val & 0x0000FF00) << 8Mueve el byte 1 a la posición de byte 2
(val & 0x00FF0000) >> 8Mueve el byte 2 a la posición de byte 1
(val & 0xFF000000) >> 24Mueve el byte 3 a la posición de byte 0

Esta operación se repite desde los 32 bits en orden ascendente.

¿Cómo se construyen estas máscaras?

// 0xFF = 11111111 → 1byte
// << (8 * i) → Mueva 0xFF al byte que le interesa
uint32_t mask_byte = 0xFF << (8 * <i>);
// mask_byte = 0x000000FF
// mask_byte = 0x0000FF00
// mask_byte = 0x00FF0000
// mask_byte = 0xFF000000

Con esto, obtengo la posición exacta del byte que me interesa; solo tenemos que moverlo. Para representar esto, tenemos un buen ejemplo en endian.c → swap_uint32.

uint32_t swap_uint32(uint32_t val) {
    return ((val & 0x000000FF) << 24) | // Byte 0 → Byte 3
           ((val & 0x0000FF00) << 8)  | // Byte 1 → Byte 2
           ((val & 0x00FF0000) >> 8)  | // Byte 2 → Byte 1
           ((val & 0xFF000000) >> 24);  // Byte 3 → Byte 0
}

La operación (val & 0x000000FF) aísla el byte deseado. Con >> <i> o << <i> movemos la posición del byte, donde <i> puede ser el tamaño en bits de los bytes que queremos mover.

Extra: Se usa & para extraer, | para unir, ^ para alternar, ~ para negar.

Este patrón se generaliza a cualquier tipo (uint8_t, uint32_t, uint64_t, incluso estructuras sin formato).


Serializar y deserializar, alineación y relleno de struct en C

En esta seccion aprenderemos sobre los conceptos de serializacion, deserializacion, alineación y relleno. Los conceptos se entre lanzaran en el desarrollo de una implementación que explique practicamente como manejarlos que se detallan en sd.c.

Si quieres una referencia de como usar parte de los conceptos manejados en esta seccion, consulta reference para una documentacion en codigo.

Para empezar, aprendamos dos conceptos muy importantes, explicados por nach131 en Serialize and Deserialize.

La serialización es el proceso de convertir un objeto o estructura de datos en una representación binaria o textual que puede ser almacenada en un archivo, enviada por red o guardada en una base de datos. La idea detrás de la serialización es convertir los datos del objeto en una forma más compacta y fácil de transportar.

El proceso de deserializar se refiere a la conversión inversa, donde un archivo binario o textual se lee y se convierte de nuevo en un objeto o estructura de datos en memoria. Esto permite recuperar los datos del archivo original y volverlos accesibles para ser utilizados por el programa.

Estos son conceptos fundamentales en programación, especialmente cuando trabajas con comunicación de red, almacenamiento persistente de datos o cualquier situación donde necesitas convertir estructuras de datos en formatos que puedan ser gestionados de manera más fácil.

A diferencia del ejemplo de @nach131, no usaremos C++.

📦 Serialización

  • Tome una estructura como entrada.
  • Crea un uint<n>_t buffer[N].
  • Copia cada campo manualmente en orden usando memcpy, shift o casting seguro.
  • Aplique endianismo por campo.

📥 Deserialización

  • Reciba un uint<n>_t* buffer[N] y tamaño.
  • Lea byte por byte.
  • Reconstruya los campos.
  • Llene la estructura destino.

No exploraremos mucho de los raw pointers pero puedes consultar "Raw pointers (C++)" o why do void* pointers even exist? by low level tv.

Usaremos offsets y sizes para ubicar y recorrer los datos con IDs para identificar tipos o estructuras serializadas.

Aunque #pragma pack(1) deshabilita el padding en las estructuras y permite el conflicto de desalineamiento que genera dificultades en la generacion de codigo ensamblador (para RISC).

About

A small project that aims to learn how endianness is used to order bytes and to understand the process and related concepts. This project includes a brief implementation and comments explaining the process so you don't get lost.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages