Eu preciso de uma função que (como SecureZeroMemory do WinAPI) sempre zera a memória e não seja otimizada, mesmo se o compilador achar que a memória nunca mais será acessada depois disso. Parece um candidato perfeito para volátil. Mas estou tendo alguns problemas para realmente fazer isso funcionar com o GCC. Aqui está um exemplo de função:
void volatileZeroMemory(volatile void* ptr, unsigned long long size)
{
volatile unsigned char* bytePtr = (volatile unsigned char*)ptr;
while (size--)
{
*bytePtr++ = 0;
}
}
Simples o suficiente. Mas o código que o GCC realmente gera se você chamá-lo varia muito com a versão do compilador e a quantidade de bytes que você está tentando zerar. https://godbolt.org/g/cMaQm2
- GCC 4.4.7 e 4.5.3 nunca ignoram o volátil.
- GCC 4.6.4 e 4.7.3 ignoram voláteis para tamanhos de array 1, 2 e 4
- GCC 4.8.1 até 4.9.2 ignora volátil para tamanhos de array 1 e 2.
- GCC 5.1 até 5.3 ignora volátil para tamanhos de matriz 1, 2, 4, 8.
- O GCC 6.1 simplesmente o ignora para qualquer tamanho de array (pontos de bônus por consistência).
Qualquer outro compilador que eu testei (clang, icc, vc) gera os armazenamentos esperados, com qualquer versão do compilador e qualquer tamanho de array. Então, neste ponto, estou me perguntando, este é um bug do compilador GCC (muito antigo e grave?) Ou a definição de volátil no padrão imprecisa que este é realmente um comportamento em conformidade, tornando essencialmente impossível escrever um portátil " Função SecureZeroMemory "?
Edit: Algumas observações interessantes.
#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;
}
A possível gravação de callMeMaybe () fará com que todas as versões do GCC, exceto 6.1, gerem os armazenamentos esperados. Comentar na cerca de memória também fará com que o GCC 6.1 gere os armazenamentos, embora apenas em combinação com a possível escrita de callMeMaybe ().
Alguém também sugeriu esvaziar os caches. A Microsoft não tenta liberar o cache em "SecureZeroMemory". O cache provavelmente será invalidado muito rápido de qualquer maneira, então isso provavelmente não será um grande problema. Além disso, se outro programa estivesse tentando sondar os dados, ou se fosse gravado no arquivo de página, seria sempre a versão zerada.
Existem também algumas preocupações sobre o GCC 6.1 usando memset () na função autônoma. O compilador GCC 6.1 no godbolt pode ter uma compilação quebrada, já que o GCC 6.1 parece gerar um loop normal (como o 5.3 faz no godbolt) para a função autônoma para algumas pessoas. (Leia os comentários da resposta de zwol.)
volatile
é um bug, a menos que se prove o contrário. Mas provavelmente um bug.volatile
é tão subespecificado que pode ser perigoso - apenas não o use.volatile
é apropriado neste caso.memset
. O problema é que os compiladores sabem exatamente o quememset
faz.volatile
ponteiro para o qual queremos um ponteirovolatile
(não nos importamos se++
é estrito, mas se*p = 0
é estrito).Respostas:
O comportamento do GCC pode estar em conformidade e, mesmo que não seja, você não deve confiar em
volatile
fazer o que quiser em casos como esses. O comitê C projetadovolatile
para registradores de hardware mapeados em memória e para variáveis modificadas durante fluxo de controle anormal (por exemplo, manipuladores de sinal esetjmp
). Essas são as únicas coisas para as quais é confiável. Não é seguro usar como uma anotação geral "não otimize isto".Em particular, o padrão não é claro em um ponto chave. (Converti seu código para C; não deve haver nenhuma divergência entre C e C ++ aqui. Também fiz manualmente o inlining que ocorreria antes da otimização questionável, para mostrar o que o compilador "vê" naquele ponto .)
O loop de limpeza de memória acessa
arr
por meio de um lvalue qualificado por volátil, masarr
ele próprio não é declaradovolatile
. Portanto, é pelo menos indiscutivelmente permitido para o compilador C inferir que os armazenamentos feitos pelo loop estão "mortos" e excluir o loop completamente. Há um texto na justificativa C que implica que o comitê pretendia exigir que esses armazenamentos fossem preservados, mas o próprio padrão não faz esse requisito, conforme eu li.Para obter mais discussão sobre o que o padrão exige ou não, consulte Por que uma variável local volátil é otimizada de forma diferente de um argumento volátil e por que o otimizador gera um loop autônomo a partir do último? , O acesso a um objeto declarado não volátil por meio de uma referência / ponteiro volátil confere regras voláteis a esses acessos? e bug do GCC 71793 .
Para obter mais informações sobre o que o comitê pensava
volatile
, pesquise a justificativa do C99 para a palavra "volátil". O artigo de John Regehr " Volatiles are Miscompiled " ilustra em detalhes como as expectativas do programador devolatile
podem não ser satisfeitas pelos compiladores de produção. A série de ensaios da equipe do LLVM " O que Todo Programador C Deve Saber Sobre o Comportamento Indefinido " não aborda especificamente,volatile
mas o ajudará a entender como e por que os compiladores C modernos não são "montadores portáteis".Para a questão prática de como implementar uma função que faz o que você deseja
volatileZeroMemory
: independentemente do que o padrão exige ou deveria exigir, seria mais sensato presumir que você não pode usarvolatile
para isso. Não é uma alternativa que pode ser invocado para o trabalho, porque iria quebrar demasiado outras coisas, se ele não funcionou:No entanto, você deve ter certeza absoluta de que
memory_optimization_fence
não está inline em nenhuma circunstância. Ele deve estar em seu próprio arquivo de origem e não deve estar sujeito à otimização de tempo de link.Existem outras opções, dependendo de extensões do compilador, que podem ser utilizáveis em algumas circunstâncias e podem gerar código mais restrito (uma delas apareceu em uma edição anterior desta resposta), mas nenhuma é universal.
(Eu recomendo chamar a função
explicit_bzero
, porque ela está disponível com esse nome em mais de uma biblioteca C. Existem pelo menos quatro outros contendores para o nome, mas cada um foi adotado apenas por uma única biblioteca C).Você também deve saber que, mesmo que consiga fazer isso funcionar, pode não ser suficiente. Em particular, considere
Assumindo hardware com instruções de aceleração AES, se
expand_key
eencrypt_with_ek
estiverem embutidos, o compilador pode ser capaz de manterek
inteiramente no arquivo de registro vetorial - até a chamada deexplicit_bzero
, o que o força a copiar os dados confidenciais para a pilha apenas para apagá-los e, pior, não faz nada sobre as chaves que ainda estão nos registradores vetoriais!fonte
volatile
como [...] Portanto, qualquer expressão referente a tal objeto deve ser avaliada estritamente de acordo com as regras da máquina abstrata, conforme descrito em 5.1.2.3. Além disso, em cada ponto de sequência, o último valor armazenado no objeto deve corresponder ao prescrito pela máquina abstrata , exceto quando modificado pelos fatores desconhecidos mencionados anteriormente. O que constitui um acesso a um objeto que possui um tipo qualificado por volátil é definido pela implementação. ?volatile sig_atomic_t flag;
é um objeto volátil .*(volatile char *)foo
é meramente um acesso por meio de um lvalue qualificado volátil e a norma não exige que tenha quaisquer efeitos especiais.volatile
pode ser suficiente para torná-lo uma implementação "compatível", mas isso não significa que seja suficiente para ser "bom" ou "útil". Para muitos tipos de programação de sistemas, ele deve ser considerado lamentavelmente deficiente nesses aspectos.É para isso que serve a função padrão
memset_s
.Se esse comportamento com o volátil está em conformidade ou não, isso é um pouco difícil de dizer, e o volátil tem sido disse que o está infestado de bugs.
Um problema é que as especificações dizem que "os acessos a objetos voláteis são avaliados estritamente de acordo com as regras da máquina abstrata." Mas isso se refere apenas a 'objetos voláteis', não acessando um objeto não volátil por meio de um ponteiro que teve volátil adicionado. Então, aparentemente, se um compilador pode dizer que você não está realmente acessando um objeto volátil, não é necessário tratar o objeto como volátil, afinal.
fonte
memset_s
como padrão C11 é um exagero. Faz parte do Anexo K, que é opcional em C11 (e, portanto, também opcional em C ++). Basicamente, todos os implementadores, incluindo a Microsoft, cuja ideia foi em primeiro lugar (!), Se recusaram a pegá-lo; A última vez que ouvi que eles estavam falando sobre desmantelá-lo em C-next.size_t
é permitido. O Win64 ABI não está em conformidade com o C90. Isso teria sido ... não ok , mas não terrível ... se O MSVC havia captado coisas como C99uintmax_t
e%zu
em tempo hábil, mas não o fizeram .)Eu ofereço esta versão como C ++ portátil (embora a semântica seja sutilmente diferente):
Agora você tem acesso de gravação a um objeto volátil , não apenas acessos a um objeto não volátil feito por meio de uma visualização volátil do objeto.
A diferença semântica é que agora termina formalmente o tempo de vida de qualquer objeto que ocupou a região da memória, porque a memória foi reutilizada. Portanto, o acesso ao objeto após zerar seu conteúdo agora é certamente um comportamento indefinido (anteriormente, seria um comportamento indefinido na maioria dos casos, mas algumas exceções certamente existiam).
Para usar esse zeramento durante a vida útil de um objeto em vez de no final, o chamador deve usar o posicionamento
new
para colocar uma nova instância do tipo original de volta.O código pode ser mais curto (embora menos claro) usando a inicialização de valor:
e, neste ponto, é uma linha única e quase não justifica uma função auxiliar.
fonte
Deve ser possível escrever uma versão portátil da função usando um objeto volátil no lado direito e forçando o compilador a preservar os armazenamentos no array.
O
zero
objeto é declarado, ovolatile
que garante que o compilador não possa fazer suposições sobre seu valor, embora sempre seja avaliado como zero.A expressão de atribuição final lê um índice volátil na matriz e armazena o valor em um objeto volátil. Como essa leitura não pode ser otimizada, ela garante que o compilador gere os armazenamentos especificados no loop.
fonte
*ptr
durante aquele loop, ou na verdade qualquer coisa ... apenas loop. wtf, lá se vai meu cérebro.edx
: Eu entendo:.L16: subq $1, %rax; movzbl -1(%rsp), %edx; jne .L16
volatile unsigned char const
byte de preenchimento arbitrário ... ele nem lê . A chamada embutida gerada paravolatileFill()
é justa[load RAX with sizeof] .L9: subq $1, %rax; jne .L9
. Por que o otimizador (A) não relê o byte de preenchimento e (B) se preocupa em preservar o loop onde não faz nada?