¿Se garantiza que sea seguro realizar memcpy (0,0,0)?

9 minutos de lectura

avatar de usuario
Matthieu M.

No estoy muy versado en el estándar C, así que tengan paciencia conmigo.

Me gustaría saber si está garantizado, por la norma, que memcpy(0,0,0) es seguro.

La única restricción que pude encontrar es que si las regiones de memoria se superponen, entonces el comportamiento no está definido…

Pero, ¿podemos considerar que las regiones de memoria se superponen aquí?

  • Matemáticamente, la intersección de dos conjuntos vacíos es vacía.

    – Benoit

    9 de marzo de 2011 a las 8:22

  • Quería comprobar si quieres que (x)libC lo haga por ti, pero como es asm (elibc/glibc aquí), es un poco complicado para una mañana temprano 🙂

    –Kevin

    9 de marzo de 2011 a las 8:25

  • +1 Me encanta esta pregunta porque es un caso extremo tan extraño y porque creo memcpy(0,0,0) es una de las piezas de código C más raras que he visto.

    – templatetypedef

    9 de marzo de 2011 a las 8:29

  • @eq ¿Realmente quiere saber, o está insinuando que no hay situaciones en las que lo quiera? ¿Ha considerado que la llamada real podría ser, digamos, memcpy(outp, inp, len)? Y que esto podría ocurrir en código donde outp y inp se asignan dinámicamente y son inicialmente 0? Esto funciona, por ejemplo, con p = realloc(p, len+n) Cuándo p y len están 0. Yo mismo he usado tal memcpy llamada: aunque técnicamente es UB, nunca me he encontrado con una implementación en la que no sea una operación no operativa y nunca espere hacerlo.

    –Jim Balter

    9 de marzo de 2011 a las 8:50


  • @templatetypedef memcpy(0, 0, 0) lo más probable es que represente una invocación dinámica, no estática… es decir, esos valores de parámetros no necesitan ser literales.

    –Jim Balter

    9 de marzo de 2011 a las 8:53

Tengo una versión preliminar del estándar C (ISO/IEC 9899:1999) y tiene algunas cosas divertidas que decir sobre esa llamada. Para empezar, menciona (§7.21.1/2) con respecto a memcpy que

Cuando un argumento declarado como size_t n especifica la longitud de la matriz para una función, n puede tener el valor cero en una llamada a esa función. A menos que se indique explícitamente lo contrario en la descripción de una función particular en esta subcláusula, los argumentos de puntero en dicha llamada seguirán teniendo valores válidos, como se describe en 7.1.4. En tal llamada, una función que localiza un carácter no encuentra ninguna ocurrencia, una función que compara dos secuencias de caracteres devuelve cero y una función que copia caracteres copia cero caracteres.

La referencia aquí indicada apunta a esto:

Si un argumento de una función tiene un valor no válido (como un valor fuera del dominio de la función o un puntero fuera del espacio de direcciones del programa,
o un puntero nuloo un puntero a un almacenamiento no modificable cuando el parámetro correspondiente no está calificado como constante) o un tipo (después de la promoción) no esperado por una función con un número variable de argumentos, el comportamiento es indefinido.

Entonces parece que de acuerdo con la especificación C, llamando

memcpy(0, 0, 0)

da como resultado un comportamiento indefinido, porque los punteros nulos se consideran “valores no válidos”.

Dicho esto, estaría completamente asombrado si alguna implementación real de memcpy se rompió si hizo esto, ya que la mayoría de las implementaciones intuitivas que se me ocurren no harían nada si dijera copiar cero bytes.

  • Puedo afirmar que las partes citadas del proyecto de norma son idénticas en el documento final. No debería haber ningún problema con una llamada de este tipo, pero seguiría siendo un comportamiento indefinido en el que confía. Así que la respuesta a “está garantizado” es “no”.

    – DevSolar

    9 de marzo de 2011 a las 8:33


  • Ninguna implementación que vaya a utilizar en producción producirá otra cosa que no sea una llamada no operativa, pero las implementaciones que hagan lo contrario están permitidas y son razonables… p. ej., un intérprete de C o un compilador aumentado con comprobación de errores que rechace la llamar porque no es conforme. Por supuesto, eso no sería razonable si el Estándar permitiera la llamada, como lo hace para realloc(0, 0). Los casos de uso son similares y los he usado a ambos (vea mi comentario debajo de la pregunta). Es inútil y desafortunado que la Norma haga este UB.

    –Jim Balter

    9 de marzo de 2011 a las 9:01


  • “Me sorprendería mucho si alguna implementación real de memcpy se rompiera si hicieras esto”: he usado una que lo haría; de hecho, si pasó la longitud 0 con punteros válidos, en realidad copió 65536 bytes. (Su bucle disminuyó la longitud y luego se probó).

    –MM

    12 de julio de 2014 a las 6:01

  • @MattMcNabb Esa implementación está rota.

    –Jim Balter

    13 de julio de 2014 a las 22:16

  • @MattMcNabb: Agregue “correcto” a “real”, tal vez. Creo que todos tenemos recuerdos no tan buenos de las antiguas bibliotecas del gueto C y no estoy seguro de cuántos de nosotros apreciamos que se recuerden esos recuerdos. 🙂

    – tmyklebu

    9 sep 2014 a las 11:20

avatar de usuario
usuario1998586

Solo por diversión, las notas de la versión para gcc-4.9 indican que su optimizador hace uso de estas reglas y, por ejemplo, puede eliminar el condicional en

int copy (int* dest, int* src, size_t nbytes) {
    memmove (dest, src, nbytes);
    if (src != NULL)
        return *src;
    return 0;
}

que luego da resultados inesperados cuando copy(0,0,0) se llama (ver https://gcc.gnu.org/gcc-4.9/porting_to.html).

Soy algo ambivalente sobre el comportamiento de gcc-4.9; el comportamiento puede cumplir con los estándares, pero poder llamar a memmove(0,0,0) a veces es una extensión útil para esos estándares.

  • Interesante. Entiendo tu ambivalencia, pero este es el corazón de las optimizaciones en C: el compilador asume que los desarrolladores siguen ciertas reglas y por lo tanto deduce que algunas optimizaciones son válidas (que lo son si se siguen las reglas).

    – Matthieu M.

    19 de julio de 2014 a las 12:45

  • @tmyklebu: Dado char *p = 0; int i=something;evaluación de la expresión (p+i) producirá un comportamiento indefinido incluso cuando i es cero

    – Super gato

    06/12/2014 a las 21:32

  • @tmyklebu: En mi humilde opinión, tener toda la aritmética de punteros (aparte de las comparaciones) en una trampa de puntero nulo sería algo bueno; ya sea memcpy() se debe permitir realizar cualquier aritmética de punteros en sus argumentos antes de garantizar un recuento distinto de cero es otra cuestión [if I were designing the standards, I would probably specify that if p is null, p+0 could trap, but memcpy(p,p,0) would do nothing]. Un problema mucho mayor, en mi humilde opinión, es la apertura de la mayoría de los comportamientos indefinidos. Si bien hay algunas cosas que realmente deberían representar un comportamiento indefinido (por ejemplo, llamar free(p)

    – Super gato

    10 de diciembre de 2014 a las 16:43


  • …y posteriormente interpretando p[0]=1;) hay muchas cosas que deben especificarse como que producen un resultado indeterminado (por ejemplo, una comparación relacional entre punteros no relacionados no debe especificarse como consistente con ninguna otra comparación, pero debe especificarse como que produce un 0 o un 1), o debe especificarse que produce un comportamiento ligeramente más flexible que el definido por la implementación (se debe exigir a los compiladores que documenten todas las posibles consecuencias de, por ejemplo, un desbordamiento de enteros, pero no especificar qué consecuencia ocurriría en un caso particular).

    – Super gato

    10 de diciembre de 2014 a las 16:52


  • alguien, por favor, dígame, ¿por qué no obtengo una insignia de stackoverflow “comenzó una guerra de llamas” 🙂

    – usuario1998586

    20 de junio de 2015 a las 8:28

También puede considerar este uso de memmove visto en Git 2.14.x (Q3 2017)

Ver cometer 168e635 (16 de julio de 2017), y cometer 1773664, cometer f331ab9, cometer 5783980 (15 de julio de 2017) por René Scharfe (rscharfe).
(Combinado por Junio ​​C Hamano — gitster en cometer 32f902511 de agosto de 2017)

utiliza un macro ayudante MOVE_ARRAY que calcula el tamaño en función del número especificado de elementos para nosotros y admite NULL
punteros cuando ese número es cero.
Crudo memmove(3) llamadas con NULL puede hacer que el compilador se optimice (demasiado) más tarde NULL cheques

MOVE_ARRAY agrega un asistente seguro y conveniente para mover rangos potencialmente superpuestos de entradas de matrices.
Infiere el tamaño del elemento, lo multiplica de forma automática y segura para obtener el tamaño en bytes, realiza una comprobación básica de seguridad de tipos comparando los tamaños de los elementos y, a diferencia de memmove(3) es compatible NULL punteros iff 0 elementos se van a mover.

#define MOVE_ARRAY(dst, src, n) move_array((dst), (src), (n), sizeof(*(dst)) + \
    BUILD_ASSERT_OR_ZERO(sizeof(*(dst)) == sizeof(*(src))))
static inline void move_array(void *dst, const void *src, size_t n, size_t size)
{
    if (n)
        memmove(dst, src, st_mult(size, n));
}

Ejemplos:

- memmove(dst, src, (n) * sizeof(*dst));
+ MOVE_ARRAY(dst, src, n);

utiliza el macro BUILD_ASSERT_OR_ZERO que afirma una dependencia de tiempo de construcción, como una expresión (con @cond siendo la condición de tiempo de compilación la que debe ser verdadera).
La compilación fallará si la condición no es verdadera o si el compilador no puede evaluarla.

#define BUILD_ASSERT_OR_ZERO(cond) \
(sizeof(char [1 - 2*!(cond)]) - 1)

Ejemplo:

#define foo_to_char(foo)                \
     ((char *)(foo)                     \
      + BUILD_ASSERT_OR_ZERO(offsetof(struct foo, string) == 0))

  • La existencia de optimizadores que piensan que “inteligente” y “tonto” son antónimos hace que la prueba de n sea necesaria, pero generalmente sería posible un código más eficiente en una implementación que garantizara que memmove(any,any,0) no sería operativo . A menos que un compilador pueda reemplazar una llamada a memmove() con una llamada a memmoveAtLeastOneByte(), la solución para protegerse contra la “optimización” de compiladores inteligentes/estúpidos generalmente resultará en una comparación adicional que un compilador no podrá eliminar.

    – Super gato

    14 de agosto de 2017 a las 23:34


No, memcpy(0,0,0) no es seguro. Es probable que la biblioteca estándar no falle en esa llamada. Sin embargo, en un entorno de prueba, puede haber algún código adicional en memcpy() para detectar desbordamientos de búfer y otros problemas. Y cómo esa versión especial de memcpy() reacciona a los punteros NULL es, bueno, indefinido.

¿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