Uso de los 16 bits adicionales en punteros de 64 bits

Leí que una máquina de 64 bits realmente usa solo 48 bits de dirección (específicamente, estoy usando Intel Core i7).

Esperaría que los 16 bits adicionales (bits 48-63) sean irrelevantes para la dirección, y serían ignorados. Pero cuando trato de acceder a dicha dirección recibí una señal EXC_BAD_ACCESS .

Mi código es:

 int *p1 = &val; int *p2 = (int *)((long)p1 | 1ll<<48);//set bit 48, which should be irrelevant int v = *p2; //Here I receive a signal EXC_BAD_ACCESS. 

¿Por qué esto es así? ¿Hay alguna forma de usar estos 16 bits?

Esto podría usarse para crear una lista vinculada más compatible con la caché. En lugar de utilizar 8 bytes para el siguiente ptr, y 8 bytes para la clave (debido a la restricción de alineación), la clave podría estar integrada en el puntero.

Los bits de orden superior están reservados en caso de que el bus de direcciones se incremente en el futuro, por lo que no puede usarlo simplemente así

La architecture AMD64 define un formato de dirección virtual de 64 bits, del cual se usan los 48 bits de orden inferior en las implementaciones actuales (…) La definición de architecture permite elevar este límite en implementaciones futuras a los 64 bits completos , extendiendo la espacio de direcciones virtuales a 16 EB (2 64 bytes). Esto se compara con solo 4 GB (2 32 bytes) para el x86.

http://en.wikipedia.org/wiki/X86-64#Architectural_features

Más importante aún, de acuerdo con el mismo artículo [Énfasis mío]:

… en las primeras implementaciones de la architecture, solo se usarían los 48 bits menos significativos de una dirección virtual en la traducción de direcciones (búsqueda en la tabla de páginas). Además, los bits 48 a 63 de cualquier dirección virtual deben ser copias del bit 47 (de una manera similar a la extensión del signo ), o el procesador generará una excepción. Las direcciones que cumplen con esta regla se conocen como “forma canónica”.

Como la CPU verificará los bits altos incluso si no se utilizan, en realidad no son “irrelevantes”. Debe asegurarse de que la dirección sea canónica antes de usar el puntero. Algunas otras architectures de 64 bits como ARM64 tienen la opción de ignorar los bits altos, por lo tanto, puede almacenar datos en punteros con mucha más facilidad.


Dicho esto, en x86_64 aún puede usar los 16 bits altos si es necesario, pero debe verificar y corregir el valor del puntero extendiendo la señal antes de quitarle la referencia.

Tenga en cuenta que convertir el valor del puntero a long no es la forma correcta de hacerlo, ya que no se garantiza que sea lo suficientemente largo como para almacenar punteros. Necesita usar uintptr_t o intptr_t .

 int *p1 = &val; // original pointer uint8_t data = ...; const uintptr_t MASK = ~(1ULL < < 48); // store data into the pointer // note: to be on the safe side and future-proof (because future implementations could // increase the number of significant bits in the pointer), we should store values // from the most significant bits down to the lower ones int *p2 = (int *)(((uintptr_t)p1 & MASK) | (data << 56)); // get the data stored in the pointer data = (uintptr_t)p2 >> 56; // deference the pointer // technically implementation defined. You may want a more // standard-compliant way to sign-extend the value intptr_t p3 = ((intptr_t)p2 < < 16) >> 16; // sign extend the pointer to make it canonical val = *(int*)p3; 

El motor JavaScriptCore de WebKit y el motor SpiderMonkey de Mozilla usan esto en la técnica del nan-boxeo . Si el valor es NaN, los 48 bits bajos almacenarán el puntero al objeto con los 16 bits altos como bits de etiqueta, de lo contrario, será un valor doble.


También puede usar los bits más bajos para almacenar datos. Se llama puntero etiquetado . Si int está alineado a 4 bytes, los 2 bits bajos siempre son 0 y puede usarlos como en las architectures de 32 bits. Para valores de 64 bits, puede usar los 3 bits bajos porque ya están alineados en 8 bytes. De nuevo, también necesita borrar esos bits antes de eliminar la referencia.

 int *p1 = &val; // the pointer we want to store the value into int tag = 1; const uintptr_t MASK = ~0x03ULL; // store the tag int *p2 = (int *)(((uintptr_t)p1 & MASK) | tag); // get the tag tag = (uintptr_t)p2 & 0x03; // get the referenced data intptr_t p3 = (uintptr_t)p2 & MASK; // clear the 2 tag bits before using the pointer val = *(int*)p3; 

Un usuario famoso de esto es la versión de 32 bits de V8 con optimización SMI (entero pequeño) (aunque no estoy seguro del V8 de 64 bits). Los bits más bajos servirán como una etiqueta para el tipo: si es 0 , es un número entero pequeño de 31 bits, haga un cambio a la derecha firmado por 1 para restablecer el valor; si es 1 , el valor es un puntero a los datos reales (objetos, flotantes o enteros más grandes), simplemente borre la etiqueta y desreferenciarla

Nota al margen: Usar la lista vinculada para los casos con valores de clave pequeños en comparación con los punteros es una gran pérdida de memoria, y también es más lenta debido a la mala ubicación en el caché. De hecho, no deberías usar la lista vinculada en la mayoría de los problemas de la vida real

  • Bjarne Stroustrup dice que debemos evitar las listas vinculadas
  • Por qué nunca, nunca, NUNCA usar linked-list en su código otra vez
  • Número de crujidos: ¿Por qué nunca, nunca, NUNCA use la lista enlazada en su código otra vez?
  • Bjarne Stroustrup: ¿Por qué deberías evitar las listas vinculadas?
  • ¿Son las listas malvadas? -Bjarne Stroustrup

Según los Manuales de Intel (volumen 1, sección 3.3.7.1), las direcciones lineales tienen que ser en forma canónica. Esto significa que, de hecho, solo se utilizan 48 bits y los 16 bits adicionales se extienden un poco. Además, se requiere la implementación para verificar si una dirección está en esa forma y si no genera una excepción. Es por eso que no hay forma de usar esos 16 bits adicionales.

La razón por la que se hace de esa manera es bastante simple. En la actualidad, el espacio de direcciones virtuales de 48 bits es más que suficiente (y debido al costo de producción de la CPU, no tiene sentido ampliarlo), pero indudablemente en el futuro serán necesarios los bits adicionales. Si las aplicaciones / kernels los usaran para sus propios fines, surgirán problemas de compatibilidad y eso es lo que los proveedores de CPU quieren evitar.

La memoria física está dirigida a 48 bits. Eso es suficiente para abordar una gran cantidad de RAM. Sin embargo, entre su progtwig se ejecuta en el núcleo de la CPU y la memoria RAM es la unidad de administración de memoria, parte de la CPU. Su progtwig se dirige a la memoria virtual, y la MMU es responsable de traducir entre direcciones virtuales y direcciones físicas. Las direcciones virtuales son de 64 bits.

El valor de una dirección virtual no le dice nada sobre la dirección física correspondiente. De hecho, debido a cómo funcionan los sistemas de memoria virtual, no hay garantía de que la dirección física correspondiente sea la misma en cada momento. Y si se vuelve creativo con mmap () puede hacer que dos o más direcciones virtuales apunten a la misma dirección física (donde sea que esté). Si luego escribe en cualquiera de esas direcciones virtuales, en realidad está escribiendo en una sola dirección física (donde sea que esté). Este tipo de truco es bastante útil en el procesamiento de señales.

Por lo tanto, cuando manipula el bit 48 de su puntero (que apunta a una dirección virtual), la MMU no puede encontrar esa nueva dirección en la tabla de memoria asignada a su progtwig por el SO (o usted mismo usando malloc ()) . Levanta una interrupción en protesta, el SO lo detecta y finaliza su progtwig con la señal que menciona.

Si quieres saber más, te sugiero que busques en Google la “architecture moderna de la computadora” y leas algo sobre el hardware que respalda tu progtwig.