Ao perguntar sobre o comportamento indefinido comum em C , as pessoas às vezes se referem à regra estrita de alias.
Sobre o que eles estão falando?
804
Ao perguntar sobre o comportamento indefinido comum em C , as pessoas às vezes se referem à regra estrita de alias.
Sobre o que eles estão falando?
c
ec++faq
.Respostas:
Uma situação típica em que você encontra problemas estritos de aliasing é sobrepor uma estrutura (como uma mensagem de dispositivo / rede) em um buffer do tamanho da palavra do seu sistema (como um ponteiro para
uint32_t
s ouuint16_t
s). Quando você sobrepõe uma estrutura a esse buffer ou um buffer a essa estrutura por meio da conversão de ponteiro, é possível violar facilmente regras estritas de alias.Portanto, nesse tipo de configuração, se eu quiser enviar uma mensagem para algo, eu teria que ter dois ponteiros incompatíveis apontando para o mesmo pedaço de memória. Eu poderia então ingenuamente codificar algo como isto (em um sistema com
sizeof(int) == 2
):A regra estrita de alias torna ilegal essa configuração: a exclusão de um ponteiro que alias um objeto que não seja de um tipo compatível ou um dos outros tipos permitidos pelo parágrafo 7 1 do C 2011 6.5 é um comportamento indefinido. Infelizmente, você ainda pode codificar dessa maneira, talvez receber alguns avisos, compilar bem, apenas para ter um comportamento inesperado e estranho ao executar o código.
(O GCC parece um pouco inconsistente em sua capacidade de emitir avisos de alias, às vezes nos dando um aviso amigável e às vezes não.)
Para ver por que esse comportamento é indefinido, temos que pensar sobre o que a regra de aliasing estrita compra o compilador. Basicamente, com essa regra, ele não precisa pensar em inserir instruções para atualizar o conteúdo de
buff
cada execução do loop. Em vez disso, ao otimizar, com algumas suposições irritantemente não aplicadas sobre alias, ele pode omitir essas instruções, carregarbuff[0]
ebuff[1
] nos registros da CPU uma vez antes da execução do loop e acelerar o corpo do loop. Antes da introdução do aliasing estrito, o compilador tinha que viver em um estado de paranóia que o conteúdo debuff
poderia mudar a qualquer momento e de qualquer lugar por qualquer pessoa. Portanto, para obter uma vantagem extra de desempenho e assumindo que a maioria das pessoas não digita dicas, foi introduzida a regra estrita de alias.Lembre-se, se você acha que o exemplo é artificial, isso pode até acontecer se você estiver passando um buffer para outra função que está enviando para você, se tiver.
E reescreveu nosso loop anterior para tirar proveito dessa função conveniente
O compilador pode ou não ser capaz ou inteligente o suficiente para tentar incorporar o SendMessage e pode ou não decidir carregar ou não o buff novamente. Se
SendMessage
faz parte de outra API que é compilada separadamente, provavelmente possui instruções para carregar o conteúdo do buff. Por outro lado, talvez você esteja em C ++ e esta é uma implementação apenas de cabeçalho modelado que o compilador acha que pode incorporar. Ou talvez seja apenas algo que você escreveu em seu arquivo .c para sua própria conveniência. De qualquer forma, um comportamento indefinido ainda pode ocorrer. Mesmo quando sabemos um pouco do que está acontecendo sob o capô, ainda é uma violação da regra, portanto, nenhum comportamento bem definido é garantido. Portanto, apenas agrupar uma função que usa nosso buffer delimitado por palavras não ajuda necessariamente.Então, como faço para contornar isso?
Use uma união. A maioria dos compiladores suporta isso sem reclamar sobre aliasing estrito. Isso é permitido no C99 e explicitamente permitido no C11.
Você pode desativar o aliasing estrito no seu compilador ( f [no-] aliasing estrito no gcc))
Você pode usar o
char*
alias em vez da palavra do seu sistema. As regras permitem uma exceção parachar*
(incluindosigned char
eunsigned char
). Sempre se assume quechar*
aliases outros tipos. No entanto, isso não funcionará da outra maneira: não há suposição de que sua estrutura aliase um buffer de caracteres.Cuidado para iniciantes
Este é apenas um campo minado em potencial ao sobrepor dois tipos um ao outro. Você também deve aprender sobre endianness , alinhamento de palavras e como lidar com problemas de alinhamento através de estruturas de embalagem corretamente.
Nota de rodapé
1 Os tipos que o C 2011 6.5 7 permite que um lvalue acesse são:
fonte
unsigned char*
ser usado agorachar*
? I tendem a usarunsigned char
, em vez dechar
como o tipo subjacente parabyte
porque meus bytes não são assinados e eu não quero que a estranheza do comportamento assinado (nomeadamente wrt para overflow)unsigned char *
é bom.uint32_t* buff = malloc(sizeof(Msg));
união e as subsequentesunsigned int asBuffer[sizeof(Msg)];
terão tamanhos diferentes e nenhuma está correta. Amalloc
ligação é baseada no alinhamento de 4 bytes sob o capô (não faça isso) e a união será 4 vezes maior do que precisa ser ... Entendo que é por clareza, mas isso não me incomoda. menos ...A melhor explicação que encontrei é de Mike Acton, Entendendo o aliasing estrito . Ele se concentrou um pouco no desenvolvimento do PS3, mas isso é basicamente apenas o GCC.
Do artigo:
Então, basicamente, se você
int*
apontar para alguma memória que contenha umint
e, em seguida, apontar afloat*
para essa memória e usá-la como umafloat
violação da regra. Se seu código não respeitar isso, o otimizador do compilador provavelmente quebrará seu código.A exceção à regra é a
char*
, que pode apontar para qualquer tipo.fonte
Esta é a regra estrita de alias, encontrada na seção 3.10 do padrão C ++ 03 (outras respostas fornecem uma boa explicação, mas nenhuma forneceu a própria regra):
Redação C ++ 11 e C ++ 14 (alterações enfatizadas):
Duas mudanças foram pequenas: glvalue em vez de lvalue e esclarecimento do caso agregado / união.
A terceira alteração oferece uma garantia mais forte (relaxa a forte regra de alias): O novo conceito de tipos semelhantes que agora são seguros para alias.
Também a redação C (C99; ISO / IEC 9899: 1999 6.5 / 7; exatamente a mesma redação é usada na ISO / IEC 9899: 2011 §6.5 ¶7):
fonte
wow(&u->s1,&u->s2)
seria necessário ser legal mesmo quando um ponteiro for usado para modificaru
e isso negaria a maioria das otimizações que o regra de aliasing foi projetada para facilitar.Nota
Isso foi extraído do meu "O que é a regra estrita de alias e por que nos importamos?" escrever.
O que é aliasing estrito?
Em C e C ++, o aliasing tem a ver com quais tipos de expressão temos permissão para acessar valores armazenados. Em C e C ++, o padrão especifica quais tipos de expressão são permitidos para alias quais tipos. O compilador e o otimizador podem assumir que seguimos estritamente as regras de alias, daí o termo regra estrita de alias . Se tentarmos acessar um valor usando um tipo não permitido, ele será classificado como comportamento indefinido ( UB ). Depois de ter um comportamento indefinido, todas as apostas estão desativadas, os resultados do nosso programa não são mais confiáveis.
Infelizmente, com violações estritas de alias, geralmente obtemos os resultados esperados, deixando a possibilidade de que uma versão futura de um compilador com uma nova otimização decida o código que julgávamos válido. Isso é indesejável e é um objetivo que vale a pena entender as regras estritas de alias e como evitar violá-las.
Para entender mais sobre por que nos importamos, discutiremos questões que surgem ao violar regras estritas de aliasing, punição de tipo, já que técnicas comuns usadas na punição de tipo geralmente violam regras estritas de alias e como digitar trocadilho corretamente.
Exemplos preliminares
Vejamos alguns exemplos, depois podemos falar exatamente sobre o que dizem os padrões, examinar alguns exemplos adicionais e ver como evitar aliases estritos e capturar violações que perdemos. Aqui está um exemplo que não deve surpreender ( exemplo ao vivo ):
Temos um int * apontando para a memória ocupada por um int e esse é um alias válido. O otimizador deve assumir que as atribuições por meio de ip podem atualizar o valor ocupado por x .
O próximo exemplo mostra um alias que leva a um comportamento indefinido ( exemplo ao vivo ):
Na função foo, pegamos um int * e um float * , neste exemplo, chamamos foo e configuramos os dois parâmetros para apontar para o mesmo local de memória que neste exemplo contém um int . Observe que o reinterpret_cast está dizendo ao compilador para tratar a expressão como se tivesse o tipo especificado por seu parâmetro de modelo. Nesse caso, estamos dizendo para tratar a expressão & x como se tivesse o tipo float * . Podemos esperar ingenuamente que o resultado do segundo corte seja 0, mas com a otimização ativada usando -O2, tanto gcc quanto clang produzem o seguinte resultado:
O que pode não ser esperado, mas é perfeitamente válido, pois invocamos um comportamento indefinido. Um flutuador não pode validamente alias um objeto int . Portanto, o otimizador pode assumir a constante 1 armazenada ao remover a referência i, que será o valor de retorno, pois um armazenamento através de f não pode afetar validamente um objeto int . A inserção do código no Compiler Explorer mostra que é exatamente isso que está acontecendo ( exemplo ao vivo ):
O otimizador que usa a Análise de alias baseada em tipo (TBAA) assume que 1 será retornado e move diretamente o valor constante no registro eax, que carrega o valor de retorno. O TBAA usa as regras de idiomas sobre quais tipos têm permissão de alias para otimizar cargas e armazenamentos. Nesse caso, o TBAA sabe que um float não pode alias e int e otimiza a carga de i .
Agora, para o livro de regras
O que exatamente o padrão diz que somos permitidos e não permitidos? O idioma padrão não é simples, portanto, para cada item, tentarei fornecer exemplos de código que demonstram o significado.
O que o padrão C11 diz?
O padrão C11 diz o seguinte na seção 6.5 Expressões, parágrafo 7 :
gcc / clang tem uma extensão e também que permite atribuir unsigned int * para int * mesmo que eles não são tipos compatíveis.
O que o Draft 17 Draft Standard diz
O rascunho do padrão C ++ 17 na seção [basic.lval], parágrafo 11, diz:
Vale ressaltar que o caractere assinado não está incluído na lista acima. Essa é uma diferença notável de C, que indica um tipo de caractere .
O que é Type Punning
Chegamos a esse ponto e podemos estar se perguntando: por que queremos usar o apelido? A resposta normalmente é digitar trocadilho , geralmente os métodos usados violam regras estritas de alias.
Às vezes, queremos contornar o sistema de tipos e interpretar um objeto como um tipo diferente. Isso é chamado de punção de tipo , para reinterpretar um segmento de memória como outro tipo. A punção de tipo é útil para tarefas que desejam acessar a representação subjacente de um objeto para visualizar, transportar ou manipular. Áreas típicas que encontramos como punição de tipo sendo usadas são compiladores, serialização, código de rede, etc.
Tradicionalmente, isso é conseguido pegando o endereço do objeto, convertendo-o em um ponteiro do tipo que queremos reinterpretá-lo como e depois acessando o valor, ou seja, usando o alias. Por exemplo:
Como vimos anteriormente, este não é um alias válido, por isso estamos invocando um comportamento indefinido. Mas tradicionalmente os compiladores não tiravam vantagem das regras estritas de alias e esse tipo de código geralmente funcionava; infelizmente, os desenvolvedores se acostumaram a fazer as coisas dessa maneira. Um método alternativo comum para punição de tipo é por meio de uniões, que são válidas em C, mas com comportamento indefinido em C ++ ( veja o exemplo ao vivo ):
Isso não é válido em C ++ e alguns consideram que o objetivo das uniões é unicamente para implementar tipos de variantes e consideram que o uso de uniões para punção de tipo é um abuso.
Como digitamos o trocadilho corretamente?
O método padrão para punção de tipo em C e C ++ é memcpy . Isso pode parecer um pouco pesado, mas o otimizador deve reconhecer o uso do memcpy para punções de tipo, otimizá-lo e gerar um registro para registrar a movimentação. Por exemplo, se sabemos que int64_t tem o mesmo tamanho que o dobro :
podemos usar o memcpy :
Em um nível de otimização suficiente, qualquer compilador moderno decente gera código idêntico ao método reinterpret_cast ou método de união mencionado anteriormente para punção de tipo . Examinando o código gerado, vemos que ele usa apenas registre mov (exemplo ao vivo do Compiler Explorer ).
C ++ 20 e bit_cast
No C ++ 20, podemos obter bit_cast ( implementação disponível no link da proposta ), que fornece uma maneira simples e segura de digitar trocadilhos, além de ser utilizável em um contexto constexpr.
A seguir, é apresentado um exemplo de como usar bit_cast para digitar pun um int não assinado a flutuar ( veja ao vivo ):
No caso em que os tipos Para e De não têm o mesmo tamanho, é necessário usar uma estrutura intermediária15. Usaremos uma estrutura que contém uma matriz de caracteres sizeof (int não assinada) ( assume int não assinada de 4 bytes ) como o tipo From e int sem assinatura como o tipo To . :
É lamentável precisarmos desse tipo intermediário, mas essa é a restrição atual do bit_cast .
Capturando violações estritas de aliasing
Não temos muitas ferramentas boas para capturar aliasing estrito em C ++, as ferramentas que possuímos capturam alguns casos de violações estritas de aliasing e alguns casos de cargas e armazenamentos desalinhados.
O gcc usando o sinalizador -fstrict-aliasing e -Wstrict-aliasing pode capturar alguns casos, embora não sem falsos positivos / negativos. Por exemplo, os seguintes casos gerarão um aviso no gcc ( veja ao vivo ):
embora não capte este caso adicional ( veja ao vivo ):
Embora o clang permita esses sinalizadores, aparentemente não implementa os avisos.
Outra ferramenta que temos à nossa disposição é o ASan, que pode capturar cargas e lojas desalinhadas. Embora essas não sejam violações estritamente diretas de alias, elas são um resultado comum de violações estritas de alias. Por exemplo, os seguintes casos gerarão erros de tempo de execução quando criados com clang usando -fsanitize = address
A última ferramenta que vou recomendar é específica para C ++ e não é estritamente uma ferramenta, mas uma prática de codificação, não permite transmissões no estilo C. Tanto o gcc quanto o clang produzirão um diagnóstico para os elencos no estilo C usando o elenco -Wold-style . Isso forçará qualquer trocadilho de tipo indefinido a usar reinterpret_cast; em geral, reinterpret_cast deve ser um sinalizador para uma revisão mais detalhada do código. Também é mais fácil pesquisar em sua base de códigos por reinterpret_cast para realizar uma auditoria.
Para C, já temos todas as ferramentas abordadas e também temos o tis-intérprete, um analisador estático que analisa exaustivamente um programa para um grande subconjunto da linguagem C. Dadas as versões C do exemplo anterior, em que o uso de -fstrict-aliasing perde um caso ( veja ao vivo )
tis-interpeter é capaz de capturar todos os três, o exemplo a seguir chama tis-kernal como tis-intérprete (a saída é editada por questões de brevidade):
Finalmente, há o TySan, que está atualmente em desenvolvimento. Este desinfetante adiciona informações de verificação de tipo em um segmento de memória de sombra e verifica os acessos para ver se eles violam as regras de alias. A ferramenta deve ser capaz de detectar todas as violações de aliasing, mas pode ter uma grande sobrecarga de tempo de execução.
fonte
reinterpret_cast
podem fazer ou o quecout
pode significar. (Está tudo bem mencionar C ++, mas a questão original sobre C e IIUC estes exemplos poderia apenas como validamente ser escrito em C.)O aliasing estrito não se refere apenas a ponteiros, mas também a referências; escrevi um artigo sobre o wiki do desenvolvedor do impulso e foi tão bem recebido que o transformei em uma página no meu site de consultoria. Explica completamente o que é, por que confunde tanto as pessoas e o que fazer sobre isso. White paper estrito sobre alias . Em particular, explica por que as uniões são um comportamento arriscado para C ++ e por que usar memcpy é a única correção portátil em C e C ++. Espero que isso seja útil.
fonte
Como adendo ao que Doug T. já escreveu, aqui está um caso de teste simples que provavelmente o aciona com o gcc:
check.c
Compile com
gcc -O2 -o check check.c
. Normalmente (com a maioria das versões do gcc que eu tentei), isso gera um "problema estrito de alias", porque o compilador assume que "h" não pode ser o mesmo endereço que "k" na função "check". Por isso, o compilador otimiza aif (*h == 5)
distância e sempre chama o printf.Para quem está interessado aqui é o código do assembler x64, produzido pelo gcc 4.6.3, rodando no ubuntu 12.04.2 para x64:
Portanto, a condição if desapareceu completamente do código do assembler.
fonte
long long*
eint64_t
*). Pode-se esperar que um compilador sadio reconheça issolong long*
eint64_t*
possa acessar o mesmo armazenamento se eles forem armazenados de forma idêntica, mas esse tratamento não está mais na moda.A punção de tipo por meio de projeções de ponteiro (ao contrário de usar uma união) é um exemplo importante de quebrar o aliasing estrito.
fonte
fpsync()
diretiva entre escrever como fp e ler como int ou vice-versa [em implementações com pipelines e caches inteiros e FPU separados , essa diretiva pode ser cara, mas não tão cara quanto o compilador executar essa sincronização em todos os acessos à união]. Ou uma implementação pode especificar que o valor resultante nunca será utilizável, exceto em circunstâncias usando Sequências Iniciais Comuns.De acordo com a lógica C89, os autores da Norma não queriam exigir que os compiladores recebessem código como:
deve ser necessário recarregar o valor de
x
entre a atribuição e a declaração de retorno, a fim de permitir a possibilidade quep
possa apontar parax
, e a atribuição para,*p
consequentemente, alterar o valor dex
. A noção de que um compilador deve ter o direito de presumir que não haverá apelidos em situações como a acima não é controversa.Infelizmente, os autores do C89 escreveram sua regra de uma maneira que, se lida literalmente, faria até mesmo a seguinte função chamar Undefined Behavior:
porque usa um lvalue do tipo
int
para acessar um objeto do tipostruct S
eint
não está entre os tipos que podem ser usados acessando astruct S
. Como seria absurdo tratar todo uso de membros de estruturas e uniões sem caráter de caractere como comportamento indefinido, quase todo mundo reconhece que há pelo menos algumas circunstâncias em que um valor l de um tipo pode ser usado para acessar um objeto de outro tipo . Infelizmente, o Comitê de Padrões C não conseguiu definir quais são essas circunstâncias.Grande parte do problema é resultado do Relatório de Defeitos # 028, que perguntou sobre o comportamento de um programa como:
O Relatório de Defeitos # 28 declara que o programa chama Comportamento indefinido porque a ação de escrever um membro da união do tipo "double" e ler um do tipo "int" chama o comportamento definido pela implementação. Esse raciocínio é absurdo, mas forma a base para as regras do Tipo Efetivo que desnecessariamente complicam a linguagem sem fazer nada para resolver o problema original.
A melhor maneira de resolver o problema original provavelmente seria tratar a nota de rodapé sobre o objetivo da regra como se fosse normativa e tornar a regra inaplicável, exceto nos casos que realmente envolvem acessos conflitantes usando aliases. Dado algo como:
Não há conflito interno
inc_int
porque todos os acessos ao armazenamento acessado*p
são feitos com um valor de tipoint
e não há conflitotest
porquep
é derivado visivelmente de umstruct S
e, na próxima vez em ques
for usado, todos os acessos ao armazenamento que serão feitos atravésp
já terá acontecido.Se o código fosse ligeiramente alterado ...
Aqui, existe um conflito de aliasing entre
p
e o acesso às.x
linha marcada, porque nesse ponto da execução existe outra referência que será usada para acessar o mesmo armazenamento .Se o Relatório de Defeitos 028 dissesse que o exemplo original invocava o UB por causa da sobreposição entre a criação e o uso dos dois ponteiros, isso tornaria as coisas muito mais claras sem a necessidade de adicionar "Tipos eficazes" ou outra complexidade.
fonte
Depois de ler muitas das respostas, sinto a necessidade de adicionar algo:
O aliasing estrito (que descreverei um pouco) é importante porque :
O acesso à memória pode ser caro (desempenho), e é por isso que os dados são manipulados nos registros da CPU antes de serem gravados de volta na memória física.
Se dados em dois registros diferentes de CPU forem gravados no mesmo espaço de memória, não podemos prever quais dados "sobreviverão" quando codificarmos em C.
Na montagem, onde codificamos o carregamento e descarregamento dos registros da CPU manualmente, saberemos quais dados permanecem intactos. Mas C (felizmente) abstrai esse detalhe.
Como dois ponteiros podem apontar para o mesmo local na memória, isso pode resultar em código complexo que lida com possíveis colisões .
Esse código extra é lento e prejudica o desempenho, pois realiza operações extras de leitura / gravação de memória, que são mais lentas e (possivelmente) desnecessárias.
A regra de aliasing estrita nos permite evitar código de máquina redundante nos casos em que deve ser seguro assumir que dois ponteiros não apontam para o mesmo bloco de memória (consulte também o
restrict
palavra chave).O aliasing estrito afirma que é seguro assumir que ponteiros para tipos diferentes apontam para locais diferentes na memória.
Se um compilador perceber que dois ponteiros apontam para tipos diferentes (por exemplo, an
int *
e afloat *
), ele assumirá que o endereço de memória é diferente e não protegerá contra colisões de endereços de memória, resultando em um código de máquina mais rápido.Por exemplo :
Vamos assumir a seguinte função:
Para lidar com o caso em que
a == b
(ambos os ponteiros apontam para a mesma memória), precisamos solicitar e testar a maneira como carregamos dados da memória nos registros da CPU, para que o código possa acabar assim:carregar
a
eb
da memória.adicionar
a
ab
.salve
b
e recarreguea
.(salve do registro da CPU na memória e carregue da memória no registro da CPU).
adicionar
b
aa
.salve
a
(do registro da CPU) na memória.A etapa 3 é muito lenta porque precisa acessar a memória física. No entanto, é necessário proteger contra instâncias em que
a
eb
apontar para o mesmo endereço de memória.O aliasing estrito nos permitiria evitar isso, informando ao compilador que esses endereços de memória são distintamente diferentes (o que, nesse caso, permitirá uma otimização ainda maior que não pode ser realizada se os ponteiros compartilharem um endereço de memória).
Isso pode ser informado ao compilador de duas maneiras, usando tipos diferentes para apontar. ou seja:
Usando a
restrict
palavra - chave ou seja:Agora, satisfazendo a regra Strict Aliasing, a etapa 3 pode ser evitada e o código será executado significativamente mais rápido.
De fato, adicionando a
restrict
palavra - chave, toda a função pode ser otimizada para:carregar
a
eb
da memória.adicionar
a
ab
.salvar o resultado para
a
e parab
.Essa otimização não poderia ter sido feita antes, devido à possível colisão (onde
a
eb
seria triplicada em vez de dobrada).fonte
b
(não recarregando) e recarregandoa
. Espero que esteja mais claro agora.restrict
, mas eu acho que o último seria, na maioria das circunstâncias, mais eficaz, e o relaxamento de algumas restriçõesregister
permitiria preencher alguns dos casos emrestrict
que não ajudaria. Não tenho certeza de que tenha sido "importante" tratar o Padrão como uma descrição completa de todos os casos em que os programadores devem esperar que os compiladores reconheçam evidências de alias, em vez de apenas descrever locais onde os compiladores devem presumir alias, mesmo quando não exista nenhuma evidência específica .restrict
palavra - chave minimiza não apenas a velocidade das operações, mas também o número delas, o que pode ser significativo ... quero dizer, afinal, a operação mais rápida não é nenhuma operação :)O aliasing estrito não está permitindo diferentes tipos de ponteiros para os mesmos dados.
Este artigo deve ajudar você a entender o problema em detalhes.
fonte
int
uma estrutura que contém umint
).Tecnicamente, em C ++, a regra estrita de aliasing provavelmente nunca é aplicável.
Observe a definição de indireção ( * operador ):
Também da definição de glvalue
Portanto, em qualquer rastreamento de programa bem definido, um glvalue se refere a um objeto. Portanto, a chamada regra estrita de aliasing nunca se aplica. Pode não ser o que os designers queriam.
fonte
int foo;
, o que é acessado pela expressão lvalue*(char*)&foo
? Isso é um objeto do tipochar
? Esse objeto passa a existir ao mesmo tempo quefoo
? Escrever parafoo
alterar o valor armazenado desse objeto do tipo acima mencionadochar
? Em caso afirmativo, existe alguma regra que permita que o valor armazenado de um objeto do tipochar
seja acessado usando um lvalue do tipoint
?int i;
cria quatro objetos de cada tipo de caracterein addition to one of type
int? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` ei
. Finalmente, não há nada no Standard que permita que mesmo umvolatile
ponteiro qualificado acesse registros de hardware que não atendem à definição de "objeto".