O que estou perguntando é o conhecido truque "último membro de uma estrutura tem comprimento variável". É mais ou menos assim:
struct T {
int len;
char s[1];
};
struct T *p = malloc(sizeof(struct T) + 100);
p->len = 100;
strcpy(p->s, "hello world");
Por causa da forma como a estrutura é disposta na memória, podemos sobrepor a estrutura sobre um bloco maior do que o necessário e tratar o último membro como se fosse maior do que o 1 char
especificado.
Portanto, a questão é: essa técnica é um comportamento tecnicamente indefinido? . Eu esperava que sim, mas estava curioso para saber o que a norma diz sobre isso.
PS: Estou ciente da abordagem C99 para isso, gostaria que as respostas se prendessem especificamente à versão do truque, conforme listado acima.
c
undefined-behavior
c89
Evan Teran
fonte
fonte
Respostas:
Como diz o FAQ C :
e:
A justificativa por trás do bit de 'conformidade estrita' está na especificação, seção J.2 Comportamento indefinido , que inclui na lista de comportamento indefinido:
O parágrafo 8 da Seção 6.5.6 Operadores aditivos menciona outra vez que o acesso além dos limites definidos da matriz é indefinido:
fonte
p->s
nunca é usado como array. É passado para estrcpy
, nesse caso, decai para um planochar *
, que passa a apontar para um objeto que pode ser interpretado legalmente como estandochar [100];
dentro do objeto alocado.malloc
, quando você simplesmente converteu ovoid *
para um ponteiro para [uma estrutura contendo] uma matriz. Ainda é válido acessar qualquer parte do objeto alocado usando um ponteiro parachar
(ou de preferênciaunsigned char
).malloc
. Procure "objeto" no padrão antes de jorrar bs.Acredito que tecnicamente seja um comportamento indefinido. O padrão (discutivelmente) não o aborda diretamente, então ele se enquadra no "ou pela omissão de qualquer definição explícita de comportamento." cláusula (§4 / 2 de C99, §3.16 / 2 de C89) que diz que é comportamento indefinido.
O "indiscutivelmente" acima depende da definição do operador de subscrito da matriz. Especificamente, ele diz: "Uma expressão pós-fixada seguida por uma expressão entre colchetes [] é uma designação subscrita de um objeto de matriz." (C89, §6.3.2.1 / 2).
Você pode argumentar que o "de um objeto de matriz" está sendo violado aqui (já que você está subscrevendo fora do intervalo definido do objeto de matriz), caso em que o comportamento é (um pouco mais) explicitamente indefinido, em vez de apenas indefinido cortesia de nada defini-lo completamente.
Em teoria, posso imaginar um compilador que faz a verificação de limites de array e (por exemplo) abortaria o programa quando / se você tentasse usar um subscrito fora do intervalo. Na verdade, não sei da existência de tal coisa, e dada a popularidade deste estilo de código, mesmo se um compilador tentasse impor subscritos em algumas circunstâncias, é difícil imaginar que alguém toleraria isso em essa situação.
fonte
arr[x] = y;
pode ser reescrito comoarr[0] = y;
; para um array de tamanho 2,arr[i] = 4;
pode ser reescrito comoi ? arr[1] = 4 : arr[0] = 4;
Embora eu nunca tenha visto um compilador realizar tais otimizações, em alguns sistemas embarcados elas podem ser muito produtivas. Em um PIC18x, usando tipos de dados de 8 bits, o código para a primeira instrução seria dezesseis bytes, o segundo, dois ou quatro e o terceiro, oito ou doze. Não é uma má otimização, se legal.a[2] == a + 2
), ele não o faz. Se eu estiver correto, todos os padrões C definem o acesso ao array como aritmático de ponteiro.Sim, é um comportamento indefinido.
O Relatório de defeito de linguagem C # 051 dá uma resposta definitiva a esta pergunta:
http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html
No documento de justificativa C99, o Comitê C adiciona:
fonte
malloc
) é válido na adição, então como pode o ponteiro idêntico, obtido por outra rota, ser inválido na adição? Mesmo que eles queiram alegar que é UB, isso não faz sentido, porque não há como uma implementação computacionalmente distinguir entre o uso bem definido e o uso supostamente indefinido.*foo
contém um matriz de elemento únicoboz
, a expressãofoo->boz[biz()*391]=9;
pode ser simplificada comobiz(),foo->boz[0]=9;
). Infelizmente, a rejeição de arrays de elemento zero dos compiladores significa que muitos códigos usam arrays de elemento único, e seriam quebrados por essa otimização.Essa maneira particular de fazer isso não está explicitamente definida em nenhum padrão C, mas C99 inclui o "hack de estrutura" como parte da linguagem. Em C99, o último membro de uma estrutura pode ser um "membro de matriz flexível", declarado como
char foo[]
(com qualquer tipo que você deseja no lugar dechar
).fonte
Não é um comportamento indefinido , independentemente do que alguém, oficial ou não , diga, porque é definido pela norma.
p->s
, exceto quando usado como um lvalue, é avaliado como um ponteiro idêntico a(char *)p + offsetof(struct T, s)
. Em particular, este é umchar
ponteiro válido dentro do objeto malloc'd, e há 100 (ou mais, dependendo das considerações de alinhamento) endereços sucessivos imediatamente após ele, que também são válidos comochar
objetos dentro do objeto alocado. O fato de que o ponteiro foi derivado usando em->
vez de adicionar explicitamente o deslocamento ao ponteiro retornado pormalloc
, convertido parachar *
, é irrelevante.Tecnicamente,
p->s[0]
é o único elemento dachar
matriz dentro da estrutura, os próximos elementos (por exemplo,p->s[1]
atravésp->s[3]
) são provavelmente bytes de preenchimento dentro da estrutura, que podem ser corrompidos se você executar a atribuição à estrutura como um todo, mas não se você simplesmente acessar um indivíduo membros e o resto dos elementos são espaço adicional no objeto alocado que você pode usar como quiser, desde que obedeça aos requisitos de alinhamento (echar
não tenha requisitos de alinhamento).Se você está preocupado com a possibilidade de sobreposição com bytes de preenchimento na estrutura, de alguma forma, invocar demônios nasais, pode evitar isso substituindo o
1
in[1]
por um valor que garante que não haja preenchimento no final da estrutura. Uma maneira simples, mas inútil de fazer isso, seria fazer uma estrutura com membros idênticos, exceto nenhum array no final, e usars[sizeof struct that_other_struct];
para o array. Então,p->s[i]
é claramente definido como um elemento da matriz na estrutura parai<sizeof struct that_other_struct
e como um objeto char em um endereço após o final da estrutura parai>=sizeof struct that_other_struct
.Edit: Na verdade, no truque acima para obter o tamanho certo, você também pode precisar colocar uma união contendo cada tipo simples antes da matriz, para garantir que a própria matriz comece com o alinhamento máximo, em vez de no meio do preenchimento de algum outro elemento . Novamente, eu não acredito que nada disso seja necessário, mas estou oferecendo isso para o mais paranóico dos advogados de línguas por aí.
Edição 2: A sobreposição com bytes de preenchimento definitivamente não é um problema, devido a outra parte do padrão. C requer que, se duas estruturas concordam em uma subsequência inicial de seus elementos, os elementos iniciais comuns podem ser acessados por meio de um ponteiro para qualquer tipo. Como consequência, se uma estrutura idêntica a,
struct T
mas com uma matriz final maior, fosse declarada, o elementos[0]
teria que coincidir com o elementos[0]
emstruct T
, e a presença desses elementos adicionais não poderia afetar ou ser afetada pelo acesso a elementos comuns da estrutura maior usando um ponteiro parastruct T
.fonte
malloc
qual está sendo acessado como uma matriz ou se é uma estrutura maior que está sendo acessada por meio de um ponteiro para uma estrutura menor cujos elementos são um subconjunto inicial dos elementos da estrutura maior, entre outros casos.malloc
não aloca um intervalo de memória que pode ser acessado com aritmética de ponteiros, de que adianta? E sep->s[1]
é definido pelo padrão como açúcar sintático para aritmética de ponteiros, então esta resposta apenas reafirma quemalloc
é útil. O que resta para discutir? :)1
. É tão simples quanto isso.int m[1]; int n[1]; if(m+1 == n) m[1] = 0;
supor que aif
ramificação foi inserida. Este é UB (e não há garantia de inicializaçãon
) de acordo com 6.5.6 p8 (última frase), conforme eu li. Relacionado: 6.5.9 p6 com nota de rodapé 109. (As referências são para C11 n1570.) [...]Sim, é um comportamento tecnicamente indefinido.
Observe que existem pelo menos três maneiras de implementar o "hack de estrutura":
(1) Declarando a matriz final com tamanho 0 (a forma mais "popular" em código legado). Isso é obviamente UB, uma vez que as declarações de array de tamanho zero são sempre ilegais em C. Mesmo que seja compilado, a linguagem não oferece garantias sobre o comportamento de qualquer código que viole as restrições.
(2) Declarando a matriz com tamanho mínimo legal - 1 (seu caso). Nesse caso, qualquer tentativa de pegar o ponteiro para
p->s[0]
e usá-lo para aritmética de ponteiro que vai alémp->s[1]
é um comportamento indefinido. Por exemplo, uma implementação de depuração tem permissão para produzir um ponteiro especial com informações de intervalo incorporadas, que será interceptado toda vez que você tentar criar um ponteiro alémp->s[1]
.(3) Declarar a matriz com tamanho "muito grande" como 10000, por exemplo. A ideia é que o tamanho declarado seja maior do que qualquer coisa que você possa precisar na prática. Este método não contém UB em relação ao intervalo de acesso à matriz. No entanto, na prática, é claro, sempre alocaremos uma quantidade menor de memória (apenas a quantidade realmente necessária). Não tenho certeza sobre a legalidade disso, ou seja, gostaria de saber o quão legal é alocar menos memória para o objeto do que o tamanho declarado do objeto (assumindo que nunca acessamos os membros "não alocados").
fonte
s[1]
não é um comportamento indefinido. É o mesmo que*(s+1)
, que é o mesmo que*((char *)p + offsetof(struct T, s) + 1)
, que é um ponteiro válido para achar
no objeto alocado.foo[]
açúcar sintático para*foo
), então qualquer acesso além do menor de seu tamanho declarado e seu tamanho alocado é UB, independentemente de como a aritmética do ponteiro foi feita.foo[]
em uma estrutura não é açúcar sintático para*foo
; é um membro de matriz flexível C99. Para o resto, veja minha resposta e comentários sobre outras respostas.unsigned char [sizeof object]
matriz sobreposta imaginária . Eu mantenho minha afirmação de que o membro flexível da matriz "hack" para pré-C99 tem um comportamento bem definido.O padrão é bastante claro que você não pode acessar coisas além do final de um array. (e passar por ponteiros não ajuda, já que você não tem permissão nem mesmo para incrementar ponteiros após o fim do array).
E para "trabalhar na prática". Eu vi o otimizador gcc / g ++ usando essa parte do padrão, gerando código errado ao atender a este C. inválido
fonte
Se um compilador aceita algo como
Acho que está bem claro que deve estar pronto para aceitar um subscrito em 'dat' além de seu comprimento. Por outro lado, se alguém codifica algo como:
e depois acessa somestruct-> dat [x]; Eu não acho que o compilador tem qualquer obrigação de usar código de computação de endereço que funcionará com grandes valores de x. Acho que se alguém quisesse estar realmente seguro, o paradigma adequado seria mais como:
e então faça um malloc de (sizeof (MYSTRUCT) -LARGEST_DAT_SIZE + desejado_array_length) bytes (tendo em mente que se o comprimento_de_matriz_desejado for maior que LARGEST_DAT_SIZE, os resultados podem ser indefinidos).
A propósito, acho que a decisão de proibir matrizes de comprimento zero foi infeliz (alguns dialetos mais antigos, como o Turbo C, suportam isso), uma vez que uma matriz de comprimento zero pode ser considerada um sinal de que o compilador deve gerar código que funcione com índices maiores .
fonte