J'ai besoin d'une fonction qui (comme SecureZeroMemory de WinAPI) remet toujours à zéro la mémoire et ne soit pas optimisée, même si le compilateur pense que la mémoire ne sera plus jamais accessible après cela. Semble être un candidat parfait pour volatile. Mais j'ai des problèmes pour que cela fonctionne avec GCC. Voici un exemple de fonction:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
Assez simple. Mais le code que GCC génère réellement si vous l'appelez varie énormément avec la version du compilateur et la quantité d'octets que vous essayez de mettre à zéro. https://godbolt.org/g/cMaQm2
- GCC 4.4.7 et 4.5.3 n'ignorent jamais le volatile.
- GCC 4.6.4 et 4.7.3 ignorent volatile pour les tailles de tableau 1, 2 et 4.
- GCC 4.8.1 jusqu'à 4.9.2 ignore volatile pour les tailles de tableau 1 et 2.
- GCC 5.1 jusqu'à 5.3 ignore volatile pour les tailles de tableau 1, 2, 4, 8.
- GCC 6.1 l'ignore simplement pour n'importe quelle taille de tableau (points bonus pour la cohérence).
Tout autre compilateur que j'ai testé (clang, icc, vc) génère les magasins attendus, avec n'importe quelle version de compilateur et n'importe quelle taille de tableau. Donc, à ce stade, je me demande, est-ce un bogue du compilateur GCC (assez ancien et grave?), Ou est-ce que la définition de volatile dans la norme indique imprécise qu'il s'agit en fait d'un comportement conforme, ce qui rend pratiquement impossible d'écrire un portable " Fonction SecureZeroMemory "?
Edit: Quelques observations intéressantes.
#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;
}
L'écriture possible depuis callMeMaybe () fera que toutes les versions de GCC sauf la 6.1 génèrent les magasins attendus. Commenter dans la clôture de la mémoire fera également générer les magasins par GCC 6.1, bien qu'en combinaison avec l'écriture possible de callMeMaybe ().
Quelqu'un a également suggéré de vider les caches. Microsoft n'essaye pas du tout de vider le cache dans "SecureZeroMemory". Le cache va probablement être invalidé assez rapidement de toute façon, donc ce n'est probablement pas un gros problème. De plus, si un autre programme essayait de sonder les données, ou s'il allait être écrit dans le fichier d'échange, ce serait toujours la version remise à zéro.
Il y a aussi quelques inquiétudes à propos de GCC 6.1 utilisant memset () dans la fonction autonome. Le compilateur GCC 6.1 sur godbolt pourrait être une version cassée, car GCC 6.1 semble générer une boucle normale (comme 5.3 le fait sur godbolt) pour la fonction autonome pour certaines personnes. (Lisez les commentaires de la réponse de zwol.)
volatile
est un bogue, sauf preuve du contraire. Mais très probablement un bug.volatile
est tellement sous-spécifié qu'il est dangereux - ne l'utilisez pas.volatile
c'est approprié dans ce cas.memset
. Le problème est que les compilateurs savent exactement ce quememset
fait.volatile
pointeur, nous voulons un pointeur survolatile
(nous ne nous soucions pas de savoir si++
c'est strict, mais si*p = 0
c'est strict).Réponses:
Le comportement de GCC peut être conforme, et même s'il ne l'est pas, vous ne devriez pas vous fier
volatile
à ce que vous voulez dans des cas comme ceux-ci. Le comité C est conçuvolatile
pour les registres matériels mappés en mémoire et pour les variables modifiées pendant un flux de contrôle anormal (par exemple les gestionnaires de signaux etsetjmp
). Ce sont les seules choses pour lesquelles il est fiable. Il n'est pas sûr de l'utiliser comme une annotation générale «n'optimisez pas ceci».En particulier, la norme n'est pas claire sur un point clé. (J'ai converti votre code en C; il ne devrait pas y avoir de divergence entre C et C ++ ici. J'ai également fait manuellement l'inlining qui se produirait avant l'optimisation discutable, pour montrer ce que le compilateur "voit" à ce stade .)
La boucle d'effacement de la mémoire accède
arr
via une valeur l qualifiée volatile, maisarr
elle-même n'est pas déclaréevolatile
. Il est donc au moins sans doute permis au compilateur C de déduire que les magasins créés par la boucle sont «morts» et de supprimer complètement la boucle. Il y a un texte dans la justification C qui laisse entendre que le comité avait l' intention d'exiger que ces magasins soient préservés, mais la norme elle-même n'impose pas cette exigence, d'après ce que je lis.Pour plus de détails sur ce que la norme exige ou non, voir Pourquoi une variable locale volatile est-elle optimisée différemment d'un argument volatil, et pourquoi l'optimiseur génère-t-il une boucle no-op à partir de ce dernier? , Est - ce que l'accès à un objet non volatil déclaré via une référence / pointeur volatile confère des règles volatiles auxdits accès? , et le bogue GCC 71793 .
Pour en savoir plus sur ce à quoi le comité a pensé
volatile
, recherchez la justification C99 du mot «volatile». L'article de John Regehr " Les volatiles sont mal compilés " illustre en détail comment les attentes des programmeursvolatile
peuvent ne pas être satisfaites par les compilateurs de production. La série d'essais de l'équipe LLVM " Ce que tout programmeur C devrait savoir sur un comportement indéfini " ne traite pas spécifiquementvolatile
mais vous aidera à comprendre comment et pourquoi les compilateurs C modernes ne sont pas des "assembleurs portables".À la question pratique de savoir comment implémenter une fonction qui fait ce que vous vouliez
volatileZeroMemory
faire: indépendamment de ce que la norme exige ou était censée exiger, il serait plus sage de supposer que vous ne pouvez pas l'utiliservolatile
pour cela. Il existe une alternative sur laquelle on peut compter pour fonctionner, car cela casserait beaucoup trop d'autres choses si cela ne fonctionnait pas:Cependant, vous devez absolument vous assurer que cela
memory_optimization_fence
n'est en aucun cas intégré. Il doit se trouver dans son propre fichier source et ne doit pas être soumis à une optimisation au moment de la liaison.Il existe d'autres options, reposant sur des extensions de compilateur, qui peuvent être utilisables dans certaines circonstances et peuvent générer un code plus précis (l'une d'elles est apparue dans une édition précédente de cette réponse), mais aucune n'est universelle.
(Je recommande d'appeler la fonction
explicit_bzero
, car elle est disponible sous ce nom dans plus d'une bibliothèque C. Il y a au moins quatre autres prétendants pour le nom, mais chacun n'a été adopté que par une seule bibliothèque C.)Vous devez également savoir que, même si vous pouvez faire fonctionner cela, cela peut ne pas suffire. En particulier, considérez
En supposant que le matériel avec des instructions d'accélération AES, si
expand_key
etencrypt_with_ek
sont en ligne, le compilateur peut être en mesure de resterek
entièrement dans le fichier de registre vectoriel - jusqu'à l'appel àexplicit_bzero
, qui l'oblige à copier les données sensibles sur la pile juste pour l'effacer, et, pire, ne fait rien pour les clés qui sont toujours présentes dans les registres vectoriels!la source
volatile
as [...] Par conséquent, toute expression faisant référence à un tel objet doit être évaluée strictement selon les règles de la machine abstraite, comme décrit en 5.1.2.3. En outre, à chaque point de séquence, la dernière valeur stockée dans l'objet doit correspondre à celle prescrite par la machine abstraite , sauf si elle est modifiée par les facteurs inconnus mentionnés précédemment. Ce qui constitue un accès à un objet de type volatile qualifié est défini par l'implémentation. ?volatile sig_atomic_t flag;
est un objet volatil .*(volatile char *)foo
est simplement un accès via une valeur l volatile qualifiée et la norme ne l'exige pas pour avoir des effets spéciaux.volatile
peut être suffisant pour en faire une implémentation "conforme", mais cela ne veut pas dire qu'il suffit d'être "bon" ou "utile". Pour de nombreux types de programmation de systèmes, il devrait être considéré comme terriblement déficient à cet égard.C'est à cela que sert la fonction standard
memset_s
.Quant à savoir si ce comportement avec volatil est conforme ou non, qui est un peu difficile à dire, et volatile a été dit avoir été longtemps en proie à des bugs.
Un problème est que les spécifications disent que «les accès aux objets volatils sont évalués strictement selon les règles de la machine abstraite». Mais cela ne fait référence qu'aux `` objets volatils '', n'accédant pas à un objet non volatile via un pointeur auquel a été ajouté volatile. Donc, apparemment, si un compilateur peut dire que vous n'accédez pas vraiment à un objet volatil, il n'est pas nécessaire de traiter l'objet comme volatil après tout.
la source
memset_s
comme la norme C11 est une surestimation. Il fait partie de l'Annexe K, qui est facultative en C11 (et donc également facultative en C ++). En gros, tous les implémenteurs, y compris Microsoft, dont c'était l'idée en premier lieu (!), Ont refusé de le reprendre; la dernière fois que j'ai entendu dire qu'ils parlaient de le mettre au rebut dans C-next.size_t
est autorisé à être. L'ABI Win64 n'est pas conforme à C90. Cela aurait été ... pas bien , mais pas terrible ... si MSVC avait en fait repris des choses comme C99uintmax_t
et%zu
en temps opportun, mais ils ne l'ont pas fait .)J'offre cette version en C ++ portable (bien que la sémantique soit subtilement différente):
Vous avez maintenant des accès en écriture à un objet volatil , et pas seulement des accès à un objet non volatile effectués via une vue volatile de l'objet.
La différence sémantique est qu'elle met maintenant officiellement fin à la durée de vie de tout objet (s) occupant la région mémoire, car la mémoire a été réutilisée. Ainsi, l'accès à l'objet après la remise à zéro de son contenu est maintenant un comportement indéfini (auparavant, il aurait été un comportement non défini dans la plupart des cas, mais certaines exceptions existaient sûrement).
Pour utiliser cette remise à zéro pendant la durée de vie d'un objet plutôt qu'à la fin, l'appelant doit utiliser le placement
new
pour remettre à nouveau une nouvelle instance du type d'origine.Le code peut être raccourci (bien que moins clair) en utilisant l'initialisation de la valeur:
et à ce stade, il s'agit d'un one-liner et justifie à peine une fonction d'aide du tout.
la source
Il devrait être possible d'écrire une version portable de la fonction en utilisant un objet volatile sur le côté droit et en forçant le compilateur à conserver les magasins dans le tableau.
L'
zero
objet est déclarévolatile
qui garantit que le compilateur ne peut faire aucune hypothèse sur sa valeur même s'il évalue toujours comme zéro.L'expression d'affectation finale lit à partir d'un index volatil dans le tableau et stocke la valeur dans un objet volatile. Étant donné que cette lecture ne peut pas être optimisée, elle garantit que le compilateur doit générer les magasins spécifiés dans la boucle.
la source
*ptr
pendant cette boucle, ou en fait quoi que ce soit du tout ... juste une boucle. wtf, voilà mon cerveau.edx
: j'obtiens ceci:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
volatile unsigned char const
octet de remplissage arbitraire ... il ne le lit même pas . L'appel intégré généré àvolatileFill()
est juste[load RAX with sizeof] .L9: subq $1, %rax; jne .L9
. Pourquoi l'optimiseur (A) ne relit-il pas l'octet de remplissage et (B) se donne-t-il la peine de conserver la boucle là où il ne fait rien?