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

80

No conozco muy bien el estándar C, así que tengan paciencia conmigo.

Me gustaría saber si está garantizado, según el estándar, 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í?

Matthieu M.
fuente
7
Matemáticamente, la intersección de dos conjuntos vacíos está vacía.
Benoit
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 madrugada :)
Kevin
16
+1 Me encanta esta pregunta tanto porque es un caso extremo tan extraño como porque creo que memcpy(0,0,0)es una de las piezas más extrañas de código C que he visto.
templatetypedef
2
@eq ¿De verdad quieres saber o estás insinuando que no hay situaciones en las que lo desearías? ¿Ha considerado que la llamada real podría ser, digamos memcpy(outp, inp, len),? ¿Y que esto podría ocurrir en el código donde outpy inpse asignan dinámicamente y se encuentran inicialmente 0? Esto funciona, por ejemplo, con p = realloc(p, len+n)when py lenare 0. Yo mismo he utilizado una memcpyllamada de este tipo, 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 lo haya esperado.
Jim Balter
7
memcpy(0, 0, 0)Es muy probable que @templatetypedef represente una invocación dinámica, no estática ... es decir, los valores de los parámetros no necesitan ser literales.
Jim Balter

Respuestas:

71

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 memcpyque

Donde un argumento declarado como size_tn 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 aún tendrán valores válidos, como se describe en 7.1.4 . En tal llamada, una función que localiza un carácter no encuentra ocurrencia, una función que compara dos secuencias de caracteres devuelve cero y una función que copia caracteres copia cero caracteres.

La referencia indicada aquí 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 nulo , o un puntero al almacenamiento no modificable cuando el parámetro correspondiente no está calificado const) o un tipo (después de la promoción) no esperado por una función con un número variable de argumentos, el comportamiento no está definido .

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

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

Dicho esto, me sorprendería muchísimo si alguna implementación real de memcpyrompió si hiciera esto, ya que la mayoría de las implementaciones intuitivas en las que puedo pensar no harían nada en absoluto si dijera que se debe copiar cero bytes.

templatetypedef
fuente
3
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 tal llamada, pero aún sería un comportamiento indefinido en el que confía. Entonces, la respuesta a "está garantizado" es "no".
DevSolar
8
Ninguna implementación que utilice en producción producirá otra cosa que no sea una operación no operativa para dicha llamada, pero las implementaciones que hacen lo contrario están permitidas y son razonables ... por ejemplo, un intérprete de C o un compilador aumentado con comprobación de errores que rechaza llamar porque no es conforme. Por supuesto, eso no sería razonable si el Estándar permitiera la llamada, como lo hace realloc(0, 0). Los casos de uso son similares y los he usado ambos (vea mi comentario debajo de la pregunta). Es inútil y desafortunado que el Standard haga este UB.
Jim Balter
5
"Me sorprendería muchísimo 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, realmente copió 65536 bytes. (Su bucle disminuyó la longitud y luego se probó).
MM
14
@MattMcNabb Esa implementación está rota.
Jim Balter
3
@MattMcNabb: Agregue "correcto" a "actual", tal vez. Creo que todos tenemos recuerdos no tan agradables de las antiguas bibliotecas C del gueto y no estoy seguro de cuántos de nosotros apreciamos que se recuerden esos recuerdos. :)
tmyklebu
24

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

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

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

user1998586
fuente
2
Interesante. Entiendo su 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.
2
@tmyklebu: Dado char *p = 0; int i=something;, la evaluación de la expresión (p+i)producirá un comportamiento indefinido incluso cuando isea ​​cero.
supercat
1
@tmyklebu: Tener toda la aritmética de punteros (aparte de las comparaciones) en una trampa de puntero nulo sería en mi humilde opinión algo bueno; si se memcpy()debe permitir realizar cualquier aritmética de puntero en sus argumentos antes de garantizar un recuento distinto de cero es otra cuestión [si estuviera diseñando los estándares, probablemente especificaría que si pes nulo, p+0podría atrapar, pero memcpy(p,p,0)no haría nada]. Un problema mucho mayor, en mi humilde opinión, es el carácter abierto 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)...
supercat
1
... y posteriormente realizar p[0]=1;) hay muchas cosas que deben especificarse para producir un resultado indeterminado (por ejemplo, una comparación relacional entre punteros no relacionados no debe especificarse como coherente con ninguna otra comparación, sino que debe especificarse que arroje un 0 o un 1), o debería especificarse como que produce un comportamiento ligeramente más flexible que el definido por la implementación (debería exigirse a los compiladores que documenten todas las posibles consecuencias de, por ejemplo, desbordamiento de enteros, pero no especificar qué consecuencia ocurriría en un caso particular).
supercat
8
alguien, por favor, dígame, ¿por qué no obtengo una insignia de
desbordamiento de pila
0

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

Consulte la confirmación 168e635 (16 de julio de 2017) y la confirmación 1773664 , la confirmación f331ab9 y la confirmación 5783980 (15 de julio de 2017) de René Scharfe ( rscharfe) .
(Combinado por Junio ​​C Hamano - gitster- en el compromiso 32f9025 , 11 de agosto de 2017)

Utiliza una macro auxiliarMOVE_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.
Las memmove(3)llamadas sin procesar con NULLpueden hacer que el compilador optimice (demasiado ansiosamente) las NULLcomprobaciones posteriores .

MOVE_ARRAYagrega un ayudante seguro y conveniente para mover rangos potencialmente superpuestos de entradas de matriz.
Infiere el tamaño del elemento, se multiplica de forma automática y segura para obtener el tamaño en bytes, realiza una verificación de seguridad de tipo básica comparando los tamaños de los elementos y, a diferencia de memmove(3), admite NULLpunteros si se deben mover 0 elementos.

Ejemplos :

Utiliza la macroBUILD_ASSERT_OR_ZERO que afirma una dependencia en tiempo de compilación, como expresión ( @condsiendo la condición en tiempo de compilación que debe ser verdadera).
La compilación fallará si la condición no es verdadera o el compilador no puede evaluarla.

Ejemplo:

VonC
fuente
2
La existencia de optimizadores que piensan que "inteligente" y "tonto" son antónimos hace que la prueba de n sea necesaria, pero un código más eficiente generalmente sería posible en una implementación que garantizara que memmove (any, any, 0) sería un no-op . 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 los compiladores inteligentes / estúpidos generalmente resultará en una comparación adicional que un compilador no podrá eliminar.
supercat
-3

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, es posible que haya algo de código adicional en memcpy () para detectar saturaciones de búfer y otros problemas. Y cómo esa versión especial de memcpy () reacciona a los punteros NULL es, bueno, indefinido.

Kai Petzke
fuente
No veo nada que su respuesta agregue a las respuestas existentes que ya han señalado que esto no es seguro, con extractos del estándar.
Matthieu M.
Escribí una razón por la que ciertas implementaciones de memcpy ("en un entorno de prueba") pueden provocar problemas con memcpy (0, 0, 0). Creo que eso es nuevo.
Kai Petzke