Disponible la versión 6 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


2.2.6a  Orden de almacenamiento (endianness)

§1 Sinopsis

Además de las cuestiones relativas a la zona en que se almacenan los datos, que fueron objeto del epígrafe anterior ( 2.2.6), existe otro aspecto que también puede ser de interés para el programador C++; es la cuestión del orden en que se almacenan en memoria los objetos multibyte. Por ejemplo, como se almacenan los Bytes de un long ( 2.2.4) o de un wchar_t ( 2.2.1a1).

Nota: la cuestión no se refiere solo al orden de almacenamiento en la memoria interna. Puede ser también el caso de en un volcado de memoria a disco, o como se reciben los datos en una línea de comunicación.


La cuestión no es tan trivial como pudiera parecer a primera vista. Lo mismo que en el mundo real, donde donde existen sistemas de escritura que se leen de izquierda a derecha (el que está utilizando ahora) y otros que se leen en sentido contrario, también en el mundo de las computadoras existen sistemas que leen y escriben los Bytes de cada palabra en un sentido u otro. Naturalmente en el interior de la máquina no existe el concepto de izquierda o derecha, pero sí puede utilizarse un orden u otro para colocar los Bytes respecto al sentido ascendente de las posiciones de memoria, o respecto al orden de salida en una línea de transmisión.

Para concretar un ejemplo, tomemos los unsigned short, que en el compilador Linux GCC, en Borland C++ 5.5 y en MS Visual C++ 6.0 ocupan 2 Bytes. Supongamos ahora que una variable X de este tipo adopta el valor 255. La representación binaria convencional para los lectores humanos occidentales (que escribimos de izquierda a derecha) es del tipo 00000000 11111111. Al octeto de valor cero (0h) lo denominamos Byte más significativo, o byte alto (high byte) y al otro (FFh), Byte menos significativo, o byte bajo (low byte). Para su almacenamiento interno caben dos posibilidades: que se coloque primero el más significativo y a continuación el otro, o a la inversa (suponiendo el orden creciente de posiciones de memoria). Desgraciadamente no ha habido acuerdo entre los fabricantes respecto al sistema a adoptar y existen dispositivos hardware de ambos tipos.

Es tradición informática que la primera disposición se denomina big-endian y la segunda little-endian [1].  Si leemos la memoria desde las posiciones más bajas a las más altas, la zona que contiene el número X en una máquina que siga la convención big-endian contendrá los valores 00h FFh, mientras que en una little-endian los valores encontrados serán FFh 00h.  En concreto, las arquitecturas x86 de Intel y los procesadores Alpha de DEC son little-endian mientras que las plataformas Sun's SPARC, Motorola, e IBM PowerPC utilizan la convención big-endian.  En lo que respecta al software, Java utiliza el formato big-endian con independencia de la plataforma utilizada (es un lenguaje con una clara vocación hacia Internet, y los protocolos TCP/IP utilizan esta convención). Por contra, C y C++ utilizan la convención dictada por el Sistema Operativo. Los sistemas Windows utilizan la convención little-endian, mientras que la mayoría de plataformas Unix utilizan big-endian.

Nota: es tradición, que cuando se trata de cantidades de 32 bits. Por ejemplo, un long, la mitad más significativa se denomine palabra alta (high word), y la menos significativa palabra baja (low word).  Lo que supone evidentemente que nos referimos a palabras de 16 bits.

§2  Tratamiento

Normalmente el programador no debe preocuparse por estas cuestiones de orden ("endianness") mientras trabaja en una plataforma determinada, pero debe estar prevenido si maneja datos provenientes de otras plataformas o que deben ser compartidos con ellas [2].

Un ejemplo paradigmático es el de las comunicaciones TCP/IP. Este conjunto de protocolos utiliza la convención big-endian en todas sus estructuras. De forma que, por ejemplo, las direcciones IP, que son números de multiBytes (de 4 octetos), se construyen colocando primero el Byte más significativo. Este es el orden en que se transmiten, viajan y son recibidos las magnitudes multibyte en las comunicaciones de Internet (el denominado "network-byte order"). En caso de utilizar un equipo con hardware little-endian. Por ejemplo con un procesador Intel x86, la representación interna  (el denominado "host-byte order") seguirá esta convención y será preciso recolocar los Bytes en el orden adecuado, tanto en los flujos de entrada como en los de salida, para que los datos puedan ser interpretados correctamente.

§2.1  Una forma de realizar estas manipulaciones en C++ es recurriendo a los operadores de bit ( 4.9.3). Por ejemplo, si uShort es un unsigned short (de 2 Bytes) y debemos invertir el orden de sus octetos, pueden utilizarse las siguientes expresiones:

uShort        // Valor original a cambiar (por ejemplo big-endian)

unsigned short uS1 = uShort >> 8;   // valor del byte más significativo

unsigned short uS2 = uShort << 8;   // valor del byte menos significativo + 255

unsigned short uSwap = uS2 | uS1;   // valor little-endian

El resultado puede obtenerse en una sentencia:

unsigned short uSwap = (uShort << 8) | (uShort >>8);

También mediante una directiva de preproceso ( 4.9.10b):

#define SWAPSHORT(US) ((US << 8) | (US >>8))

...

unsigned short uSwap = SWAPSHORT(uShort);   // valor little-endian


§2.2  El procedimiento puede hacerse extensivo para los valores de 4 Bytes.  Por ejemplo, supongamos un unsigned long uLong cuyo valor es 4000967017 (puede ser cualquier otro). Su mapa de bits big-endian tiene el siguiente esquema:

11101110 01111001 11101001 01101001

Para colocarlos en posición invertida, aislamos sus 4 Bytes con el auxilio de unos patrones que responden a los siguientes valores:

unsigned long k = 0xFF;

00000000 00000000 00000000 11111111

unsigned long k1 = k | k << 8 | k << 16;  00000000 11111111 11111111 11111111
unsigned long k2 = k | k << 8 | k << 24;  11111111 00000000 11111111 11111111
unsigned long k3 = k | k << 16 | k << 24;  11111111 11111111 00000000 11111111
unsigned long k4 = k << 8 | k << 16 | k << 24; 11111111 11111111 11111111 00000000

Con ellos podemos construir las expresiones que proporcionan los Bytes individuales ( 4.9.3a):

unsigned long B1 = (uLong ^ k1 & uLong) >> 24;  00000000 00000000-00000000 11101110
unsigned long B2 = (uLong ^ k2 & uLong) >> 16;  00000000 00000000-00000000 01111001
unsigned long B3 = (uLong ^ k3 & uLong) >> 8;  00000000 00000000-00000000 11101001
unsigned long B4 = uLong ^ k4 & uLong; 00000000 00000000-00000000 01101001

A partir de aquí es trivial construir el valor deseado, con los Bytes en orden little-endian o en cualquier otro, mediante desplazamientos combinados con el operador OR inclusivo.

unsigned long uLong_Swap = B4 << 24 | B3 << 16 | B2 << 8 | B1;

Observe que es posible simplificar algo las expresiones anteriores aprovechando que los desplazamientos derecha + izquierda de B2 y B3 pueden ser combinados en uno solo.

§2.3  El procedimiento puede hacerse extensivo a cualquier valor value, expresado por una sucesión de n bytes. De forma que su representación big-endian puede expresarse:

 value = (byte[0] << 8*(n-1)) | (byte[1] << 8*(n-2)) | ... | byte[n-1];


Generalmente, estas cuestiones de "endianness" son manejadas mediante directivas de preproceso (#derfine) existentes al efecto en los ficheros de cabecera.  De esta forma, las aplicaciones son independientes de la plataforma (para adaptar el compilador a otra plataforma solo hay que modificar las directivas correspondientes). Para que el lector tenga una idea de la mecánica utilizada, a continuación se incluyen algunas muy frecuentes en la programación Windows.

#define LOWORD(x) ((WORD) (l))
#define HIWORD(x) ((WORD) (((DWORD) (l) >> 16) & 0xFFFF))

Con estas definiciones, y sabiendo que a su vez, WORD y DWORD están definidas como unsigned short y unsigned long respectivamente, supongamos que dos valores, ancho y alto de cierta propiedad, se reciben codificados en las mitades superior e inferior de un long, al que llamaremos param.  En este contexto, ambos valores pueden ser fácilmente determinados con las expresiones siguientes:

WORD alto = LOWORD(param);
WORD ancho = HIWORD(param);

Otras expresiones utilizadas en el compilador MS Visual C++ (BYTE está definida como unsigned char, y LONG es long):

#define MAKEWORD(a, b) ((WORD)(((BYTE)(a)) | ((WORD)((BYTE)(b))) << 8))
#define MAKELONG(a, b) ((LONG)(((WORD)(a)) | ((DWORD)((WORD)(b))) << 16))
#define LOBYTE(w) ((BYTE)(w))
#define HIBYTE(w) ((BYTE)(((WORD)(w) >> 8) & 0xFF))

Como el lector puede comprobar en todos estos casos, si se modifican las condiciones de entorno, la adaptación de las aplicaciones resulta muy fácil, ya que se limita a modificar adecuadamente los ficheros de cabecera.

§3  Identificación del endianness del sistema

Si estamos escribiendo una aplicación o librería muy general, que en algún punto deba tener en cuenta el endiannes de la máquina anfitriona, es posible averiguarlo fácilmente mediante una directiva que podemos utilizar posteriormente en las sentencias correspondientes según el resultado.  Serían las siguientes:

const unsigned int myOne = 1;
#define IS_BIGENDIAN (*(char*)(&myOne) == 0)
#define IS_LITTLEENDIAN (*(char*)(&myOne) == 1)

Observe que la dirección de myOne (que es un puntero-a-int) es convertida a puntero-a-char mediante el cast correspondiente; a continuación se compara el contenido de esta dirección con cero y con 1.  En realidad se está comparando el primer bite de myOne.  El truco está precisamente en comparar el contenido del primer byte (la comparación myOne == 1 sería siempre cierta). El resultado es 0 (falso) o 1 (cierto) según el modelo del sistema utilizado, que corresponderá o no, con el del hardware subyacente según el caso.  Por ejemplo, en una máquina Intel&Windows, el programa

#include <cstdlib>
#include <iostream>

const unsigned int myOne = 1;
#define IS_BIGENDIAN (*(char*)(&myOne) == 0)
#define IS_LITTLEENDIAN (*(char*)(&myOne) == 1)

int main(int argc, char *argv[]) {
   std::cout << "Esta maquina es Big-endian: " << 
            (IS_BIGENDIAN? "Cierto" : "Falso") << std::endl;
   std::cout << "Esta maquina es Little-endian: " <<
            (IS_LITTLEENDIAN? "Cierto" : "Falso") << std::endl;

   system("PAUSE");
   return EXIT_SUCCESS;
}

Produce la siguiente salida:

Esta maquina es Big-endian: Falso
Esta maquina es Little-endian: Cierto
Presione cualquier tecla para continuar . . .

 

  Inicio.


[1]  Según Christopher Brown y Michael Barr ("Introduction to Endianness" Embedded Systems Programming, January 2002 , pp. 55-56) la denominación tiene su origen en una sátira de Danny Cohen, publicada en IEEE Computer (vol. 14, no.10) y relativa a la disparidad de criterios existente en el mundo del hardware. La referida sátira se inspira a su vez en un pasaje de la novela "Los viajes de Gulliver", en la que se desata una guerra civil por una orden del emperador indicando que los huevos hervidos debían ser rotos por el lado pequeño ("Little-end").

[2] Naturalmente os referimos al manejo de datos a bajo nivel, cosa muy frecuente en algunas aplicaciones C++. Cuando el manejo se realiza mediante capas de software o servicios que realizan las modificaciones oportunas no es necesario considerar estas cuestiones.