C - Comportamento de conversão entre dois ponteiros
Atualização 2020-12-11: Obrigado @ "Some programmer dude" pela sugestão no comentário. Meu problema subjacente é que nossa equipe está implementando um mecanismo de armazenamento de tipo dinâmico. Alocamos vários buffers de array char [PAGE_SIZE] com 16-alinhados para armazenar tipos dinâmicos de dados (não há estrutura fixa). Por razões de eficiência, não podemos realizar a codificação de bytes ou alocar espaço adicional para uso memcpy.
Uma vez que o alinhamento foi determinado (ou seja, 16), o resto é usar o elenco de ponteiro para acessar objetos do tipo especificado, por exemplo:
int main() {
// simulate our 16-aligned malloc
_Alignas(16) char buf[4096];
// store some dynamic data:
*((unsigned long *) buf) = 0xff07;
*(((double *) buf) + 2) = 1.618;
}
Mas nossa equipe contesta se essa operação é um comportamento indefinido.
Eu li muitas perguntas semelhantes, como
- Por que -Wcast-align não avisa sobre a conversão de char * para int * em x86?
- Como lançar matriz char para int na posição não alinhada?
- C comportamento indefinido. Regra de aliasing estrita ou alinhamento incorreto?
- SEI CERT C CS EXP36-C
Mas isso é diferente da minha interpretação do padrão C, quero saber se é meu mal-entendido.
A principal confusão é sobre a seção 6.3.2.3 # 7 de C11:
Um ponteiro para um tipo de objeto pode ser convertido em um ponteiro para um tipo de objeto diferente. Se o ponteiro resultante não estiver alinhado corretamente 68) para o tipo referenciado, o comportamento é indefinido.
68) Em geral, o conceito '' alinhado corretamente '' é transitivo: se um ponteiro para o tipo A está alinhado corretamente para um ponteiro para o tipo B, que por sua vez está alinhado corretamente para um ponteiro para o tipo C, então um ponteiro para o tipo A está alinhado corretamente para um ponteiro do tipo C.
O ponteiro resultante aqui se refere a Objeto de ponteiro ou Valor de ponteiro ?
Na minha opinião, acho que a resposta é o Objeto Ponteiro , mas mais respostas parecem indicar o Valor do Ponteiro .
Interpretação A: Objeto de Ponteiro
Meus pensamentos são os seguintes: O próprio ponteiro é um objeto. De acordo com 6.2.5 # 28 , diferentes ponteiros podem ter diferentes requisitos de representação e alinhamento. Portanto, de acordo com 6.3.2.3 # 7 , desde que dois ponteiros tenham o mesmo alinhamento, eles podem ser convertidos com segurança sem comportamento indefinido, mas não há garantia de que possam ser desreferenciados. Expresse essa ideia em um programa:
#include <stdio.h>
int main() {
char buf[4096];
char *pc = buf;
if (_Alignof(char *) == _Alignof(int *)) {
// cast safely, because they have the same alignment requirement?
int *pi = (int *) pc;
printf("pi: %p\n", pi);
} else {
printf("char * and int * don't have the same alignment.\n");
}
}
Interpretação B: Valor do Indicador
No entanto, se o padrão C11 estiver falando sobre Pointer Value para o tipo referenciado em vez de Pointer Object . A verificação de alinhamento do código acima não faz sentido. Expresse essa ideia em um programa:
#include <stdio.h>
int main() {
char buf[4096];
char *pc = buf;
/*
* undefined behavior, because:
* align of char is 1
* align of int is 4
*
* and we don't know whether the `value` of pc is 4-aligned.
*/
int *pi = (int *) pc;
printf("pi: %p\n", pi);
}
Qual interpretação é correta?
Respostas
A interpretação B está correta. O padrão fala sobre um ponteiro para um objeto, não o próprio objeto. "Ponteiro resultante" se refere ao resultado da conversão, e uma conversão não produz um lvalue, portanto, está se referindo ao valor do ponteiro após a conversão.
Tomando o código em seu exemplo, suponha que um intdeve ser alinhado em um limite de 4 bytes, ou seja, seu endereço deve ser um múltiplo de 4. Se o endereço de bufestá 0x1001convertendo esse endereço para int *é inválido porque o valor do ponteiro não está alinhado corretamente. Se o endereço de bufestá 0x1000convertendo para int *é válido.
Atualizar:
O código que você adicionou aborda o problema de alinhamento, portanto, está tudo bem a esse respeito. No entanto, ele tem um problema diferente: ele viola o aliasing estrito.
A matriz que você definiu contém objetos do tipo char. Ao converter o endereço para um tipo diferente e, subsequentemente, desreferenciar o tipo de tipo convertido, você está acessando objetos de um tipo como objetos de outro tipo. Isso não é permitido pelo padrão C.
Embora o termo "aliasing estrito" não seja usado na norma, o conceito é descrito na seção 6.5, parágrafos 6 e 7:
6 O tipo efetivo de um objeto para acesso ao seu valor armazenado é o tipo declarado do objeto, se houver. 87) Se um valor é armazenado em um objeto sem tipo declarado por meio de um lvalue tendo um tipo que não é um tipo de caractere, então o tipo do lvalue torna-se o tipo efetivo do objeto para esse acesso e para acessos subsequentes que não modificar o valor armazenado. Se um valor é copiado para um objeto sem tipo declarado usando
memcpyoumemmove, ou é copiado como uma matriz de tipo de caractere, então o tipo efetivo do objeto modificado para esse acesso e para acessos subsequentes que não modificam o valor é o tipo efetivo do objeto do qual o valor é copiado, se houver. Para todos os outros acessos a um objeto sem tipo declarado, o tipo efetivo do objeto é simplesmente o tipo do lvalue usado para o acesso.7 Um objeto deve ter seu valor armazenado acessado apenas por uma expressão lvalue que possui um dos seguintes tipos: 88)
- um tipo compatível com o tipo efetivo do objeto,
- uma versão qualificada de um tipo compatível com o tipo efetivo do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do objeto,
- um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada do tipo efetivo do objeto,
- um tipo de agregado ou união que inclui um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de um subagregado ou união contida), ou
- um tipo de personagem.
...
87) Objetos alocados não têm tipo declarado.
88) A intenção desta lista é especificar aquelas circunstâncias nas quais um objeto pode ou não ter um alias.
No seu exemplo, você está escrevendo um unsigned longe um doubleno topo dos charobjetos. Nenhum destes tipos satisfaz as condições do parágrafo 7.
Além disso, a aritmética do ponteiro aqui não é válida:
*(((double *) buf) + 2) = 1.618;
Como você está tratando bufcomo uma série de doublequando não é. No mínimo, você precisaria realizar a aritmética necessária bufdiretamente e lançar o resultado no final.
Então, por que isso é um problema para uma charmatriz e não para um buffer retornado por malloc? Porque a memória retornada de nãomalloc tem tipo efetivo até que você armazene algo nela, que é o que o parágrafo 6 e a nota de rodapé 87 descrevem.
Portanto, de um ponto de vista estrito do padrão, o que você está fazendo é um comportamento indefinido. Mas dependendo do seu compilador, você pode desativar o aliasing estrito para que funcione. Se você estiver usando o gcc, você vai querer passar a -fno-strict-aliasingbandeira
O padrão não exige que as implementações considerem a possibilidade de que o código algum dia observe um valor em a T*que não está alinhado para o tipo T. No clang, por exemplo, ao direcionar plataformas cujas instruções de carga / armazenamento "maiores" não suportam acesso desalinhado, converter um ponteiro em um tipo cujo alinhamento ele não satisfaz e, em seguida, usá memcpy-lo pode resultar na geração de código do compilador que falhará se o ponteiro não estiver alinhado, embora memcpyele mesmo não imponha quaisquer requisitos de alinhamento.
Ao direcionar um ARM Cortex-M0 ou Cortex-M3, por exemplo, dado:
void test1(long long *dest, long long *src)
{
memcpy(dest, src, sizeof (long long));
}
void test2(char *dest, char *src)
{
memcpy(dest, src, sizeof (long long));
}
void test3(long long *dest, long long *src)
{
*dest = *src;
}
clang irá gerar tanto para test1 e test3 código que iria falhar se srcou destnão estavam alinhados, mas para test2isso vai gerar código que é maior e mais lento, mas que irá apoiar o alinhamento arbitrário dos operandos origem e destino.
Para ter certeza, mesmo com clang, o ato de converter um ponteiro não alinhado em um long long*geralmente não fará com que nada de estranho aconteça por si só, mas é o fato de que tal conversão produziria UB que isenta o compilador de qualquer responsabilidade de lidar com o caso de ponteiro não alinhado em test1.