¿Por qué el acceso no alineado a la memoria mmap’ed a veces falla en AMD64?

4 minutos de lectura

avatar de usuario
kasperd

Tengo este fragmento de código que falla cuando se ejecuta en Ubuntu 14.04 en una CPU compatible con AMD64:

#include <inttypes.h>
#include <stdlib.h>

#include <sys/mman.h>

int main()
{
  uint32_t sum = 0;
  uint8_t *buffer = mmap(NULL, 1<<18, PROT_READ,
                         MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
  uint16_t *p = (buffer + 1);
  int i;

  for (i=0;i<14;++i) {
    //printf("%d\n", i);
    sum += p[i];
  }

  return sum;
}

Esto solo falla si la memoria se asigna usando mmap. si uso mallocun búfer en la pila o una variable global que no falla.

Si reduzco el número de iteraciones del ciclo a menos de 14, ya no fallará. Y si imprimo el índice de la matriz desde dentro del ciclo, ya no fallará.

¿Por qué el acceso a la memoria no alineada falla de segmento en una CPU que puede acceder a direcciones no alineadas, y por qué solo en circunstancias tan específicas?

  • No se puede reproducir esto en Debian. ¿Puedes publicar el código ensamblador generado? (Si está utilizando gccpuede obtener la salida del ensamblador con el -S opción, la salida del ensamblador se escribirá en un .s expediente.)

    – cmaster – reincorporar a monica

    27 de noviembre de 2017 a las 12:22

  • ¿Estás seguro de que tu mmap() tiene éxito? Tal vez devuelve un error…

    – CTX

    27 de noviembre de 2017 a las 12:25

  • ¿Qué sucede si llamas a malloc y luego agregas la línea? volatile uint8_t dummy = buffer[0]; justo después de la llamada malloc? Mismo error? Lo que busco es que la asignación real del montón se retrase hasta que los datos se utilicen realmente. Dado que se garantiza que el contenido del búfer devuelto por malloc contiene valores no especificados, el compilador de C podría pensar que en realidad no tiene que asignar nada.

    – Lundin

    27 de noviembre de 2017 a las 12:25

  • @Lundin: estás suponiendo que el malloc intrínseco puede omitirse por completo?

    –Oliver Charlesworth

    27 de noviembre de 2017 a las 12:27

  • @Lundin pero el OP afirma que obras con malloc() y no con mmap()

    – CTX

    27 de noviembre de 2017 a las 12:29

  • No estoy seguro de dónde surgió esta idea de que gcc alguna vez admitió punteros desalineados más allá de lo que requieren los estándares C o C ++. Por lo que he visto, siempre compila el código de puntero hasta prácticamente el código más simple, al igual que la mayoría de los compiladores modernos. Por supuesto, dado que x86 admite el acceso desalineado en todas partes, eso a menudo funciona bien en muchos casos, ¡pero no creo que eso sea una evidencia de que gcc lo “respalde”! ¿Cómo sería la falta de soporte? ¿Comprobando activamente los punteros desalineados y abortando? Por supuesto, ningún compilador hace eso (fuera de la desinfección).

    – BeeOnRope

    28 de noviembre de 2017 a las 18:59


  • En los pocos casos que conozco que podrían ser capaces de separar el soporte del no soporte, gcc sigue adelante y genera instrucciones que esperan la alineación estándar requerida. Por ejemplo, es feliz usar movaps para mover valores de 16 bits cuando el tipo de puntero indica una alineación de 16 bytes. Ciertamente, tampoco hace nada para admitir desalineados atomic_t o std::atomic<> valores: emite instrucciones que solo tienen las garantías de atomicidad adecuadas si el puntero está correctamente alineado. Gcc parece manejar la alineación tanto como lo hacen otros compiladores.

    – BeeOnRope

    28 de noviembre de 2017 a las 19:02


  • Sí, podrías imaginar que los compiladores caen en tres campos con respecto a cualquier tipo particular de UB: (1) haciendo todo lo posible para permitirlo, (2) generando código asumiendo que no ocurre pero sin ningún esfuerzo para detectarlo o prevenirlo y (3) hacer todo lo posible para detectarlo y hacer algo al respecto. En mi opinión, la gran mayoría de los compiladores tradicionalmente han tomado la pista (2) para la mayoría de los tipos de UB. Eso es lo más fácil, después de todo, y genera el código más rápido. Los problemas de seguridad han hecho que (3) sea mucho más popular últimamente, incluso a un costo de tiempo de ejecución (por ejemplo, comprobaciones de búfer).

    – BeeOnRope

    28 de noviembre de 2017 a las 20:57

  • Curiosamente, para el malloc La versión del código clang del OP compila toda la función para xor eax, eax ya que presumiblemente ve el acceso a la memoria no inicializada devuelta por malloc y toma un atajo. @PeterCordes – el memcpy versión parece generar código vectorizado bien sin ninguna copia (parece ser el mismo código que el acceso de puntero directo original). Mi experiencia es que ambos icc y msvc son una mierda en el manejo de este tipo de memcpy comparado con gcc o clang. El acceso de puntero directo (posiblemente UB) es mucho mejor para esos compiladores.

    – BeeOnRope

    28 de noviembre de 2017 a las 22:08


  • “Me pregunto si los encabezados glibc más nuevos usan __attribute__((assume_aligned(4096))) para marcar mmap” – no, no lo hacen y no deberían hacerlo. mmap devuelve MAP_FAILED, también conocido como (void*)-1 en caso de falla, que no está alineado con 4096, por lo que GCC eliminaría las comprobaciones de errores. gcc.godbolt.org/z/gVrLWT

    – Alcaro

    6 de noviembre de 2018 a las 2:25

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad