Recentemente, mudamos para um GCC mais recente e ele otimizou uma função inteira e a substituiu pelo código de armadilha "acesso ao ponteiro nulo" ao otimizar o tamanho. Olhando para godbolt, o problema apareceu com o GCC 11.1 ao otimizar com -Os
. Otimizar com -O2
e -O3
funciona bem, mesmo com -fstrict-aliasing
.
Simplificado, o código fica assim ( link godbolt ):
#include <inttypes.h>
#include <stddef.h>
#include <string.h>
typedef struct bounds_s {
uint8_t *start;
uint8_t *end;
} bounds_s_t;
static void reserve_space(bounds_s_t *bounds, size_t len, uint8_t **element)
{
if (bounds->start + len > bounds->end) {
return;
}
*element = bounds->start;
bounds->start += len;
}
void bug(uint8_t *buffer, size_t size)
{
bounds_s_t bounds;
uint32_t *initialize_this;
initialize_this = NULL;
bounds.start = buffer;
bounds.end = buffer + size;
reserve_space(&bounds, sizeof(*initialize_this), (uint8_t **)&initialize_this);
uint32_t value = 1234;
memcpy(initialize_this, &value, sizeof(*initialize_this));
}
E leva à seguinte montagem:
bug:
xor eax, eax
mov DWORD PTR ds:0, eax
ud2
Que otimização faz o GCC pensar que a initialize_this
variável é NULL? A única coisa que me vem à mente é quebrar regras rígidas de alias. Mas será que a tipificação de ponteiros duplos pode uint32_t **
realmente uint8_t **
ser o problema aqui e levar a consequências tão pesadas?
*element = bounds->start;
viola o alias estrito:element
tem typeuint8_t **
,*element
tem typeuint8_t *
, então isso é armazenado*element
com typeuint8_t *
.element
tem o endereço deinitialize_this
, que tem o tipo declarado e, portanto, o tipo efetivo,uint32_t *
.uint32_t *
com um tipo deuint8_t *
não está em conformidade com nenhuma das regras de alias em C 2018 6.5 7.-fstrict-aliasing
é habilitado por-Os
.uint32_t *
depois deinitialize_this = NULL;
, queinitialize_this
permanece inalterado em relação a um ponteiro nulo.Além disso, não há controle no código para garantir que o valor de
bounds->start
seja um endereço alinhado corretamente para auint32_t
.Esses problemas podem ser corrigidos:
reserve_space
também deve passar por um requisito de alinhamento, que pode ser calculado pela chamada usando_Alignof (uint32_t *)
(e, com o futuro padrão C esperado,_Alignof (typeof (initialize_this))
).reserve_space
deve adicionar bytes de preenchimento conforme necessário para tornar o endereço inicial um múltiplo deste requisito.uint8_t **
,reserve_space
deveria retornar umvoid *
apontando para o espaço reservado. A rotina de chamada pode então atribuir isso ainitialize_this
, e a conversão implícita da atribuição irá convertê-la automaticamente para o tipo correto.O código também não mostra as origens do
buffer
. Se for um espaço alocado dinamicamente, então, uma vezinitialize_this
configurado corretamente conforme descrito acima,*initialize_this
pode ser usado como um arquivouint32_t
. Em particular, não há necessidade dememcpy
copiar um valor nele; pode ser definido com*initialize_this = 1234;
. No entanto, sebuffer
for criado de alguma outra forma, como uma matriz declarada deuint8_t
, poderão permanecer problemas de alias.