最近我们切换到了较新的 GCC,它在优化大小时优化了整个函数并将其替换为“空指针访问”陷阱代码。查看 godbolt,在使用 进行优化时,GCC 11.1 出现了此问题。使用和-Os
进行优化效果很好,甚至使用 也是如此。-O2
-O3
-fstrict-aliasing
简化后的代码如下所示(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));
}
并导致以下组装:
bug:
xor eax, eax
mov DWORD PTR ds:0, eax
ud2
什么优化会让 GCC 认为initialize_this
变量为 NULL?我唯一想到的就是违反严格的别名规则。但是,将双指针从 转换为uint32_t **
真的uint8_t **
会成为这里的问题并导致如此严重的后果吗?
*element = bounds->start;
违反严格别名:element
有类型uint8_t **
,*element
有类型uint8_t *
,所以这是存储到*element
具有类型的uint8_t *
。element
的地址为initialize_this
,其声明类型为 ,因此有效类型为uint32_t *
。uint32_t *
类型为的auint8_t *
不符合 C 2018 6.5 7 中的任何别名规则。-fstrict-aliasing
通过 启用-Os
。uint32_t *
在之后成为别名的类型的写入initialize_this = NULL;
,而该写入initialize_this
与空指针没有变化。此外,代码中没有控制来确保的值
bounds->start
是正确对齐的地址uint32_t
。这些问题可以得到修复:
reserve_space
还应该传递一个对齐要求,该要求可以通过使用调用来计算_Alignof (uint32_t *)
(并且,根据即将推出的 C 标准,_Alignof (typeof (initialize_this))
)。reserve_space
应根据需要添加填充字节,以使起始地址成为此要求的倍数。uint8_t **
,而reserve_space
应返回void *
指向保留空间的 。然后调用例程可以将其分配给initialize_this
,赋值的隐式转换将自动将其转换为正确的类型。代码也没有显示 的起源
buffer
。如果它是动态分配的空间,那么一旦initialize_this
如上所述正确设置 ,*initialize_this
就可以用作普通的uint32_t
。特别是,无需使用memcpy
将值复制到其中;它可以使用 来设置*initialize_this = 1234;
。但是,如果buffer
以其他方式创建,例如 声明的 数组uint8_t
,则可能仍存在别名问题。