Necesito una función que (como SecureZeroMemory de WinAPI) siempre ponga a cero la memoria y no se optimice, incluso si el compilador piensa que la memoria nunca se volverá a acceder después de eso. Parece un candidato perfecto para volátiles. Pero estoy teniendo algunos problemas para que esto funcione con GCC. Aquí hay una función de ejemplo:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
Suficientemente simple. Pero el código que GCC realmente genera si lo llama varía enormemente con la versión del compilador y la cantidad de bytes que está intentando poner a cero. https://godbolt.org/g/cMaQm2
- GCC 4.4.7 y 4.5.3 nunca ignoran lo volátil.
- GCC 4.6.4 y 4.7.3 ignoran los volátiles para los tamaños de matriz 1, 2 y 4.
- GCC 4.8.1 hasta 4.9.2 ignoran volátil para los tamaños de matriz 1 y 2.
- GCC 5.1 hasta 5.3 ignoran los volátiles para los tamaños de matriz 1, 2, 4, 8.
- GCC 6.1 simplemente lo ignora para cualquier tamaño de matriz (puntos de bonificación por coherencia).
Cualquier otro compilador que haya probado (clang, icc, vc) genera las tiendas que uno esperaría, con cualquier versión del compilador y cualquier tamaño de matriz. Entonces, en este punto, me pregunto, ¿se trata de un error del compilador de GCC (bastante antiguo y severo?), O la definición de volátil en el estándar es tan imprecisa que esto es realmente un comportamiento conforme, lo que hace que sea esencialmente imposible escribir un portátil " ¿Función SecureZeroMemory "?
Editar: Algunas observaciones interesantes.
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <atomic>
void callMeMaybe(char* buf);
void volatileZeroMemory(volatile void* ptr, std::size_t size)
{
for (auto bytePtr = static_cast<volatile std::uint8_t*>(ptr); size-- > 0; )
{
*bytePtr++ = 0;
}
//std::atomic_thread_fence(std::memory_order_release);
}
std::size_t foo()
{
char arr[8];
callMeMaybe(arr);
volatileZeroMemory(arr, sizeof arr);
return sizeof arr;
}
La posible escritura desde callMeMaybe () hará que todas las versiones de GCC excepto la 6.1 generen las tiendas esperadas.Comentar en la valla de memoria también hará que GCC 6.1 genere las tiendas, aunque solo en combinación con la posible escritura de callMeMaybe ().
Alguien también sugirió vaciar los cachés. Microsoft no intenta vaciar la caché en absoluto en "SecureZeroMemory".Es probable que la caché se invalide bastante rápido de todos modos, por lo que probablemente esto no sea un gran problema. Además, si otro programa intentaba sondear los datos, o si se iban a escribir en el archivo de paginación, siempre sería la versión puesta a cero.
También existen algunas preocupaciones sobre el uso de memset () en GCC 6.1 en la función independiente. El compilador GCC 6.1 en godbolt podría tener una compilación rota, ya que GCC 6.1 parece generar un bucle normal (como lo hace 5.3 en godbolt) para la función independiente para algunas personas. (Lea los comentarios de la respuesta de zwol).
volatile
es un error a menos que se demuestre lo contrario. Pero lo más probable es que sea un error.volatile
está tan subespecificado como para ser peligroso, simplemente no lo use.volatile
es apropiado en este caso.memset
. El problema es que los compiladores saben exactamente lo quememset
hace.volatile
puntero, queremos un puntero avolatile
(no nos importa si++
es estricto, pero si*p = 0
es estricto).Respuestas:
El comportamiento de GCC puede ser conforme, e incluso si no lo es, no debe confiar en
volatile
hacer lo que quiera en casos como estos. El comité C diseñadovolatile
para registros de hardware mapeados en memoria y para variables modificadas durante un flujo de control anormal (por ejemplo, manejadores de señales ysetjmp
). Esas son las únicas cosas para las que es confiable. No es seguro utilizarlo como una anotación general de "no optimizar esto".En particular, el estándar no está claro en un punto clave. (He convertido su código a C; no debería haber ninguna divergencia entre C y C ++ aquí. También hice manualmente la inserción que ocurriría antes de la optimización cuestionable, para mostrar lo que el compilador "ve" en ese punto .)
extern void use_arr(void *, size_t); void foo(void) { char arr[8]; use_arr(arr, sizeof arr); for (volatile char *p = (volatile char *)arr; p < (volatile char *)(arr + 8); p++) *p = 0; }
El bucle de borrado de memoria accede a
arr
través de un lvalue calificado como volátil, pero no se declara enarr
sí mismo . Por lo tanto, es posible argumentar que al menos se permite que el compilador de C infiera que las tiendas hechas por el ciclo están "muertas" y borre el ciclo por completo. Hay un texto en el fundamento C que implica que el comité tenía la intención de exigir que esas tiendas se preserven, pero el estándar en sí no establece ese requisito, como lo leo.volatile
Para obtener más información sobre lo que requiere o no el estándar, consulte ¿Por qué una variable local volátil se optimiza de manera diferente a un argumento volátil y por qué el optimizador genera un bucle no operativo a partir de este último? , ¿El acceso a un objeto no volátil declarado a través de una referencia / puntero volátil confiere reglas volátiles sobre dichos accesos? y error de GCC 71793 .
Para obtener más información sobre lo que pensó el comité
volatile
, busque en el C99 Justificación de la palabra "volátil". El artículo de John Regehr " Los volátiles están malcompilados " ilustra en detalle cómo las expectativas de los programadoresvolatile
pueden no ser satisfechas por los compiladores de producción. La serie de ensayos del equipo de LLVM " Lo que todo programador de C debe saber sobre el comportamiento indefinido " no trata específicamente,volatile
pero lo ayudará a comprender cómo y por qué los compiladores de C modernos no son "ensambladores portátiles".A la pregunta práctica de cómo implementar una función que hace lo que usted quería
volatileZeroMemory
hacer: Independientemente de lo que el estándar requiera o esté destinado a requerir, sería más prudente asumir que no puede usarlovolatile
para esto. No es una alternativa que puede ser invocada para el trabajo, ya que rompería demasiado otras cosas si no lo hizo el trabajo:extern void memory_optimization_fence(void *ptr, size_t size); inline void explicit_bzero(void *ptr, size_t size) { memset(ptr, 0, size); memory_optimization_fence(ptr, size); } /* in a separate source file */ void memory_optimization_fence(void *unused1, size_t unused2) {}
Sin embargo, debe asegurarse absolutamente de que
memory_optimization_fence
no esté incluido bajo ninguna circunstancia. Debe estar en su propio archivo fuente y no debe estar sujeto a la optimización del tiempo de enlace.Hay otras opciones, que dependen de las extensiones del compilador, que pueden usarse en algunas circunstancias y pueden generar un código más estricto (una de ellas apareció en una edición anterior de esta respuesta), pero ninguna es universal.
(Recomiendo llamar a la función
explicit_bzero
, porque está disponible con ese nombre en más de una biblioteca C. Hay al menos otros cuatro candidatos para el nombre, pero cada uno ha sido adoptado solo por una sola biblioteca C).También debe saber que, incluso si puede hacer que esto funcione, puede que no sea suficiente. En particular, considere
struct aes_expanded_key { __uint128_t rndk[16]; }; void encrypt(const char *key, const char *iv, const char *in, char *out, size_t size) { aes_expanded_key ek; expand_key(key, ek); encrypt_with_ek(ek, iv, in, out, size); explicit_bzero(&ek, sizeof ek); }
Suponiendo hardware con instrucciones de aceleración AES, si
expand_key
yencrypt_with_ek
están en línea, el compilador puede ser capaz de mantenerseek
completamente en el archivo de registro vectorial, hasta la llamada aexplicit_bzero
, lo que lo obliga a copiar los datos confidenciales en la pila solo para borrarlos y lo que es peor, ¡no hace nada con las claves que todavía están en los registros vectoriales!fuente
volatile
como [...] Por lo tanto, cualquier expresión que se refiera a tal objeto debe evaluarse estrictamente de acuerdo con las reglas de la máquina abstracta, como se describe en 5.1.2.3. Además, en cada punto de secuencia, el último valor almacenado en el objeto deberá coincidir con el prescrito por la máquina abstracta , salvo que lo modifiquen los factores desconocidos mencionados anteriormente. Lo que constituye un acceso a un objeto que tiene un tipo calificado volátil está definido por la implementación. ?volatile sig_atomic_t flag;
es un objeto volátil .*(volatile char *)foo
es simplemente un acceso a través de un valor l calificado para volátiles y el estándar no requiere que tenga efectos especiales.volatile
puede ser suficiente para convertirlo en una implementación "compatible", pero eso no significa que sea suficiente para ser "bueno" o "útil". Para muchos tipos de programación de sistemas, debería considerarse lamentablemente deficiente en esos aspectos.Para eso es la función estándar
memset_s
.En cuanto a si este comportamiento con volátiles se ajusta o no, es un poco difícil de decir, y se ha dicho que los volátiles han estado plagados de errores durante mucho tiempo.
Un problema es que las especificaciones dicen que "los accesos a objetos volátiles se evalúan estrictamente de acuerdo con las reglas de la máquina abstracta". Pero eso solo se refiere a 'objetos volátiles', sin acceder a un objeto no volátil a través de un puntero al que se le ha agregado volátil. Entonces, aparentemente, si un compilador puede decir que realmente no está accediendo a un objeto volátil, no es necesario que trate el objeto como volátil después de todo.
fuente
memset_s
como estándar C11 es una exageración. Forma parte del anexo K, que es opcional en C11 (y, por tanto, también opcional en C ++). Básicamente, todos los implementadores, incluido Microsoft, cuya idea fue en primer lugar (!), Se han negado a aceptarlo; Lo último que escuché fue que estaban hablando de eliminarlo en C-siguiente.size_t
se permite que sea el tipo subyacente de . La ABI de Win64 no es conforme con C90. Eso habría sido ... no está bien , pero no es terrible ... si MSVC en realidad había recogido cosas de C99 comouintmax_t
y%zu
de manera oportuna, pero no lo hicieron )Ofrezco esta versión como C ++ portátil (aunque la semántica es sutilmente diferente):
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { volatile unsigned char* bytePtr = new (ptr) volatile unsigned char[size]; while (size--) { *bytePtr++ = 0; } }
Ahora tiene acceso de escritura a un objeto volátil , no simplemente accesos a un objeto no volátil realizado a través de una vista volátil del objeto.
La diferencia semántica es que ahora termina formalmente la vida útil de cualquier objeto que ocupara la región de la memoria, porque la memoria ha sido reutilizada. Entonces, el acceso al objeto después de poner a cero su contenido ahora es seguramente un comportamiento indefinido (anteriormente habría sido un comportamiento indefinido en la mayoría de los casos, pero seguramente existieron algunas excepciones).
Para usar esta
new
puesta a cero durante la vida de un objeto en lugar de al final, la persona que llama debe usar la ubicación para volver a colocar una nueva instancia del tipo original.El código se puede hacer más corto (aunque menos claro) usando la inicialización de valor:
void volatileZeroMemory(volatile void* const ptr, unsigned long long size) { new (ptr) volatile unsigned char[size] (); }
y en este punto es de una sola línea y apenas justifica una función de ayuda.
fuente
Debería ser posible escribir una versión portátil de la función utilizando un objeto volátil en el lado derecho y obligando al compilador a conservar las tiendas en la matriz.
void volatileZeroMemory(void* ptr, unsigned long long size) { volatile unsigned char zero = 0; unsigned char* bytePtr = static_cast<unsigned char*>(ptr); while (size--) { *bytePtr++ = zero; } zero = static_cast<unsigned char*>(ptr)[zero]; }
El
zero
objeto se declara de maneravolatile
que garantiza que el compilador no pueda hacer suposiciones sobre su valor aunque siempre se evalúe como cero.La expresión de asignación final lee un índice volátil en la matriz y almacena el valor en un objeto volátil. Dado que esta lectura no se puede optimizar, asegura que el compilador debe generar las tiendas especificadas en el ciclo.
fuente
*ptr
durante ese bucle, o en realidad nada en absoluto ... solo bucle. wtf, ahí va mi cerebro.edx
: obtengo esto:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
volatile unsigned char const
byte de relleno arbitrario ... ni siquiera lo lee . La llamada en línea generada avolatileFill()
es solo[load RAX with sizeof] .L9: subq $1, %rax; jne .L9
. ¿Por qué el optimizador (A) no vuelve a leer el byte de relleno y (B) se molesta en preservar el bucle donde no hace nada?