É o comportamento “struct hack” tecnicamente indefinido?


111

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 charespecificado.

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.


33
Esta parece uma questão bastante clara, razoável e, acima de tudo, passível de resposta. Não vendo o motivo da votação fechada.
cHao

2
Se você introduziu um compilador "ansi c" que não suportava o hack de struct, a maioria dos programadores de c que conheço não aceitaria que seu compilador "funcionasse direito". Não obstante, aceitariam uma leitura estrita da norma. O comitê simplesmente perdeu um sobre isso.
dmckee --- ex-moderador gatinho

4
@james O hack funciona malocalizando um objeto grande o suficiente para o array que você quer dizer, apesar de ter declarado um array mínimo. Portanto, você está acessando a memória alocada fora da definição estrita da estrutura. Escrever além de sua alocação é um erro indiscutível, mas isso é diferente de escrever em sua alocação, mas fora da "estrutura".
dmckee --- ex-moderador gatinho

2
@James: O malloc superdimensionado é fundamental aqui. Ele garante que haja memória --- memória com endereço legal e 'pertencente' à estrutura (ou seja, é ilegal para qualquer outra entidade usá-la) --- além do final nominal da estrutura. Observe que isso significa que você não pode usar o hack de struct em variáveis ​​automáticas: elas devem ser alocadas dinamicamente.
dmckee --- ex-moderador gatinho

5
@detly: É mais simples alocar / desalocar uma coisa do que alocar / desalocar duas coisas, especialmente porque a última tem duas maneiras de falhar com as quais você precisa lidar. Isso é mais importante para mim do que a economia marginal de custo / velocidade.
Jamesdlin

Respostas:


52

Como diz o FAQ C :

Não está claro se é legal ou portátil, mas é bastante popular.

e:

... uma interpretação oficial considerou que não está estritamente em conformidade com o Padrão C, embora pareça funcionar em todas as implementações conhecidas. (Compiladores que verificam os limites da matriz cuidadosamente podem emitir avisos.)

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:

  • Um subscrito de array está fora do intervalo, mesmo se um objeto estiver aparentemente acessível com o subscrito fornecido (como na expressão lvalue a[1][7]dada a declaração int a[4][5]) (6.5.6).

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:

Se o operando de ponteiro e o resultado apontam para elementos do mesmo objeto de array, ou um após o último elemento do objeto de array, a avaliação não deve produzir um estouro; caso contrário, o comportamento é indefinido.


1
No código do OP, p->snunca é usado como array. É passado para e strcpy, nesse caso, decai para um plano char *, que passa a apontar para um objeto que pode ser interpretado legalmente como estando char [100];dentro do objeto alocado.
R .. GitHub PARAR DE AJUDAR O ICE

3
Talvez outra maneira de ver isso é que a linguagem poderia restringir concebivelmente como você acessa variáveis ​​de array reais , conforme descrito em J.2, mas não há como fazer tais restrições para um objeto alocado por malloc, quando você simplesmente converteu o void *para um ponteiro para [uma estrutura contendo] uma matriz. Ainda é válido acessar qualquer parte do objeto alocado usando um ponteiro para char(ou de preferência unsigned char).
R .. GitHub PARAR DE AJUDAR O ICE

@R. - Eu posso ver como J2 pode não cobrir isso, mas também não é coberto pelo 6.5.6?
detly

1
Claro que sim! As informações de tipo e tamanho podem ser embutidas em cada ponteiro, e qualquer aritmética de ponteiro errônea pode ser feita para trap - ver, por exemplo, CCured . Em um nível mais filosófico, não importa se nenhuma implementação possível poderia pegá-lo, ainda é um comportamento indefinido (há, iirc, casos de comportamento indefinido que exigiriam um oráculo para o Problema de Interrupção acertar - que é exatamente o porquê eles são indefinidos).
zwol de

4
O objeto não é um objeto de matriz, portanto 6.5.6 é irrelevante. O objeto é o bloco de memória alocado por malloc. Procure "objeto" no padrão antes de jorrar bs.
R .. GitHub PARAR DE AJUDAR O ICE

34

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.


2
Também posso imaginar um compilador que pode decidir que se um array tiver o tamanho 1, então arr[x] = y;pode ser reescrito como arr[0] = y;; para um array de tamanho 2, arr[i] = 4;pode ser reescrito como i ? 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.
supercat

Se o padrão define o acesso à matriz fora dos limites da matriz como comportamento indefinido, o hack da estrutura também o é. Se, no entanto, o padrão define o acesso à matriz como um açúcar sintático para aritmética de ponteiro ( 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.
yyny

13

Sim, é um comportamento indefinido.

O Relatório de defeito de linguagem C # 051 dá uma resposta definitiva a esta pergunta:

O idioma, embora comum, não é estritamente conforme

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_051.html

No documento de justificativa C99, o Comitê C adiciona:

A validade desse construto sempre foi questionável. Em resposta a um Relatório de Defeito, o Comitê decidiu que era um comportamento indefinido porque a matriz p-> itens contém apenas um item, independentemente da existência de espaço.


2
1 para encontrar isso, mas ainda afirmo que é contraditório. Dois ponteiros para o mesmo objeto (neste caso, o byte fornecido) são iguais, e um ponteiro para ele (o ponteiro para a matriz de representação de todo o objeto obtido por 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.
R .. GitHub PARAR DE AJUDAR O GELO

É uma pena que os compiladores C tenham começado a proibir a declaração de matrizes de comprimento zero; se não fosse por essa proibição, muitos compiladores não teriam que fazer nenhum tratamento especial para fazê-los funcionar como "deveriam", mas ainda seriam capazes de codificar em caso especial para matrizes de elemento único (por exemplo, se *foocontém um matriz de elemento único boz, a expressão foo->boz[biz()*391]=9;pode ser simplificada como biz(),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.
supercat

11

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 de char).


Para ser pedante, esse não é o hack da estrutura. O hack de struct usa uma matriz com tamanho fixo, não um membro de matriz flexível. O hack da estrutura é o que foi questionado e é UB. Os membros da matriz flexível parecem apenas uma tentativa de apaziguar o tipo de gente vista neste tópico reclamando desse fato.
underscore_d

7

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 é um charponteiro 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 como charobjetos dentro do objeto alocado. O fato de que o ponteiro foi derivado usando em ->vez de adicionar explicitamente o deslocamento ao ponteiro retornado por malloc, convertido para char *, é irrelevante.

Tecnicamente, p->s[0]é o único elemento da charmatriz dentro da estrutura, os próximos elementos (por exemplo, p->s[1]através p->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 (e charnã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 1in [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 usar s[sizeof struct that_other_struct];para o array. Então, p->s[i]é claramente definido como um elemento da matriz na estrutura para i<sizeof struct that_other_structe como um objeto char em um endereço após o final da estrutura para i>=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 Tmas com uma matriz final maior, fosse declarada, o elemento s[0]teria que coincidir com o elemento s[0]em struct 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 para struct T.


4
Você está certo ao dizer que a natureza da aritmética do ponteiro é irrelevante, mas está errado sobre o acesso além do tamanho declarado do array. Consulte N1494 (último rascunho C1x público) seção 6.5.6 parágrafo 8 - você nem mesmo tem permissão para fazer a adição que leva um ponteiro mais de um elemento além do tamanho declarado da matriz, e você não pode desreferenciá-lo, mesmo que é apenas um elemento passado.
zwol

1
@Zack: isso é verdade se o objeto for um array. Não é verdade se o objeto é um objeto alocado pelo mallocqual 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.
R .. GitHub PARAR DE AJUDAR O ICE

6
+1 Se mallocnão aloca um intervalo de memória que pode ser acessado com aritmética de ponteiros, de que adianta? E se p->s[1]é definido pelo padrão como açúcar sintático para aritmética de ponteiros, então esta resposta apenas reafirma que mallocé útil. O que resta para discutir? :)
Daniel Earwicker,

3
Você pode argumentar que está bem definido tanto quanto você quiser, mas isso não muda o fato de que não é. O padrão é muito claro sobre o acesso além dos limites de um array, e o limite desse array é 1. É tão simples quanto isso.
Lightness Races in Orbit

3
@R .., eu acho, sua suposição de que dois ponteiros comparando iguais devem se comportar da mesma forma está errada. Considere int m[1]; int n[1]; if(m+1 == n) m[1] = 0;supor que a iframificação foi inserida. Este é UB (e não há garantia de inicialização n) 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.) [...]
mafso

7

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ém p->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ém p->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").


1
Em (2), 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 a charno objeto alocado.
R .. GitHub PARAR DE AJUDAR O ICE

Por outro lado, tenho quase certeza de que (3) é um comportamento indefinido. Sempre que você executa qualquer operação que dependa de tal estrutura residindo naquele endereço, o compilador está livre para gerar código de máquina que lê de qualquer parte da estrutura. Pode ser inútil ou pode ser um recurso de segurança para verificação estrita de alocação, mas não há motivo para uma implementação não poder fazer isso.
R .. GitHub PARAR DE AJUDAR O ICE

R: Se uma matriz foi declarada como tendo um tamanho (não é apenas o 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.
zwol

1
@Zack, você está errado em várias coisas. 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.
R .. GitHub PARAR DE AJUDAR O ICE

6
O problema é que alguns membros do comitê desejam desesperadamente que esse "hack" seja UB, porque eles imaginam algum país das fadas onde uma implementação em C poderia impor limites de ponteiro. Para melhor ou pior, no entanto, fazer isso entraria em conflito com outras partes do padrão - coisas como a capacidade de comparar ponteiros para igualdade (se os limites foram codificados no próprio ponteiro) ou o requisito de que qualquer objeto seja acessível por meio de uma 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.
R .. GitHub PARAR DE AJUDAR O ICE

3

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


Você pode dar um exemplo?
Tal de

1

Se um compilador aceita algo como

typedef struct {
  int len;
  char dat [];
};

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:

typedef struct {
  int tanto faz;
  char dat [1];
} MY_STRUCT;

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:

#define LARGEST_DAT_SIZE 0xF000
typedef struct {
  int tanto faz;
  char dat [LARGEST_DAT_SIZE];
} MY_STRUCT;

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 .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.