c++ - compiler - gnuc
¿La definición de "volátil" es volátil, o GCC tiene algunos problemas de cumplimiento estándar? (4)
Necesito una función que (como SecureZeroMemory de WinAPI) siempre ponga a cero la memoria y no se optimice, incluso si el compilador cree que la memoria nunca volverá a tener acceso después de eso. Parece un candidato perfecto para volátiles. Pero tengo 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 llamas varía enormemente con la versión del compilador y la cantidad de bytes que realmente estás 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 ignora los volátiles para los tamaños de matriz 1 y 2.
- GCC 5.1 hasta 5.3 ignora volátil para 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 de compilador y cualquier tamaño de matriz. Entonces, en este punto, me pregunto si este es un error del compilador de GCC (¿bastante viejo y severo?), O la definición de volátil en el estándar es imprecisa de que este es 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 6.1 generen los almacenes esperados. Comentando en la cerca de la memoria también hará que GCC 6.1 genere las tiendas, aunque solo en combinación con la posible escritura desde callMeMaybe ().
Alguien también ha sugerido limpiar los cachés. Microsoft no intenta vaciar el caché en absoluto en "SecureZeroMemory". Es probable que el caché se invalide bastante rápido de todos modos, por lo que probablemente no sea un gran problema. Además, si otro programa intentara sondear los datos, o si se iba a escribir en el archivo de la página, siempre sería la versión puesta a cero.
También hay algunas preocupaciones sobre GCC 6.1 usando memset () 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 5.3 lo hace en godbolt) para la función independiente para algunas personas. (Lea los comentarios de la respuesta de zwol).
Necesito una función que (como SecureZeroMemory de WinAPI) siempre ponga a cero la memoria y no se optimice,
Para esto está la función estándar
memset_s
.
En cuanto a si este comportamiento con volátil es conforme o no, eso es un poco difícil de decir, y se ha said que volátil ha estado plagado 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'', no 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.
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 preservar 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 objeto
zero
se declara
volatile
que garantiza que el compilador no pueda hacer suposiciones sobre su valor, aunque siempre se evalúa como cero.
La expresión de asignación final se lee de un índice volátil en la matriz y almacena el valor en un objeto volátil. Como esta lectura no se puede optimizar, garantiza que el compilador debe generar los almacenes especificados en el bucle.
El comportamiento de GCC
puede
ser conforme, e incluso si no lo es, no debe confiar en
volatile
para hacer lo que quiera en casos como estos.
El comité C diseñó
volatile
para registros de hardware mapeados en memoria y para variables modificadas durante el flujo de control anormal (por ejemplo, manejadores de señales y
setjmp
).
Esas son las únicas cosas para las que es confiable.
No es seguro usarlo 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 he hecho manualmente la alineación que sucederí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 valor l calificado para volátiles, pero
arr
no se
declara
volatile
.
Es, por lo tanto, al menos posiblemente permitido que el compilador de C infiera que los almacenes creados por el bucle están "muertos" y elimine el bucle por completo.
Hay un texto en el Fundamento C que implica que el comité tenía la
intención
de exigir que se preservaran esas tiendas, pero el estándar en sí mismo no exige ese requisito, tal como lo leí.
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? , ¿Acceder a un objeto no volátil declarado a través de una referencia / puntero volátil confiere reglas volátiles a dichos accesos? y error de GCC 71793 .
Para obtener más información sobre lo que el comité
pensó que
era
volatile
, busque en el
Fundamento C99
la palabra "volátil".
El artículo de John Regehr "
Los volátiles están mal compilados
" ilustra en detalle cómo los compiladores de producción no pueden satisfacer las expectativas de los programadores para
volatile
.
La serie de ensayos del equipo de LLVM "
Lo que todo programador C debe saber sobre el comportamiento indefinido
" no se refiere específicamente a los
volatile
pero lo ayudará a comprender cómo y por qué los compiladores C modernos
no
son "ensambladores portátiles".
A la pregunta
práctica
de cómo implementar una función que haga lo que usted deseaba que
volatileZeroMemory
hiciera: independientemente de lo que el estándar requiera o supuestamente requiera, sería más inteligente asumir que no puede usar
volatile
para esto.
Hay una alternativa en la que se puede confiar para trabajar, porque rompería demasiadas cosas si no funcionara:
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é en línea bajo ninguna circunstancia.
Debe estar en su propio archivo fuente y no debe estar sujeto a la optimización del tiempo de enlace.
Existen otras opciones, que dependen de las extensiones del compilador, que pueden utilizarse 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 de C. Hay al menos otros cuatro contendientes para el nombre, pero cada uno ha sido adoptado solo por una biblioteca de 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
y
encrypt_with_ek
están en línea, el compilador puede mantener
ek
completamente en el archivo de registro de vectores, hasta la llamada a
explicit_bzero
, que lo obliga a
copiar los datos confidenciales en la pila
solo para borrar ¡y, lo que es peor, no hace nada por las teclas que todavía están en los registros de vectores!
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 solo 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 finaliza formalmente la vida útil de cualquier objeto que haya ocupado la región de memoria, porque la memoria se ha reutilizado. Por lo tanto, el acceso al objeto después de poner a cero su contenido ahora es un comportamiento seguramente indefinido (anteriormente habría sido un comportamiento indefinido en la mayoría de los casos, pero seguramente existieron algunas excepciones).
Para usar esta reducción a cero durante la vida útil de un objeto en lugar de al final, la persona que llama debe usar la ubicación
new
para volver a colocar una nueva instancia del tipo original.
El código se puede acortar (aunque menos claro) utilizando 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 un trazador de líneas y apenas garantiza una función auxiliar.