含指针的自定义结构体使用memcpy为何引发堆缓冲区溢出?
你代码里的核心bug出在dynamic_array_create函数的malloc调用上:
dynamic_array.data = malloc(sizeof(DYNAMIC_STARTING_SIZE * element_size));
这里误用了sizeof运算符——你实际想分配的是DYNAMIC_STARTING_SIZE * element_size字节的内存,但加了sizeof之后,计算的是这个乘积值的类型大小(64位机器上size_t是8字节),而不是乘积本身的数值。
比如你的struct point大小是16字节(两个int各4字节+指针8字节),DYNAMIC_STARTING_SIZE * element_size的结果是16,但sizeof(16)等于sizeof(size_t)也就是8字节。这就导致你只分配了8字节内存,却要写入16字节的结构体数据,必然触发堆缓冲区溢出。
当结构体没有指针时,总大小是8字节(两个int),刚好和sizeof(size_t)一致,所以不会触发溢出,这就是为什么去掉指针字段问题消失的原因。
把sizeof去掉,直接用乘积作为malloc的参数:
dynamic_array.data = malloc(DYNAMIC_STARTING_SIZE * element_size);
同时还要注意两个额外问题:
- 必须检查malloc的返回值,如果分配失败会返回NULL,需要处理错误(比如断言或返回错误状态)
DYNAMIC_GROWTH_AMOUNT是浮点数1.5,用ceil计算扩容后的容量时,要引入<math.h>头文件,并且编译时链接数学库(比如gcc加-lm参数),否则会出现未定义行为。
修复后的dynamic_array_create函数:
#include <assert.h> #include <math.h> struct dynamic_array dynamic_array_create(size_t element_size) { struct dynamic_array dynamic_array; dynamic_array.data = malloc(DYNAMIC_STARTING_SIZE * element_size); assert(dynamic_array.data != NULL); // 简单错误检查,实际项目可替换为更健壮的处理逻辑 dynamic_array.amount = 0; dynamic_array.capacity = DYNAMIC_STARTING_SIZE; dynamic_array.element_size = element_size; return dynamic_array; }
原dynamic_array_push里先计算目标地址再扩容的逻辑有潜在风险:如果realloc移动了内存块位置,之前计算的destination会指向旧的无效内存地址。正确的顺序应该是先判断扩容、调整内存,再计算目标地址:
void dynamic_array_push(struct dynamic_array* dynamic_array, const void* element) { if (dynamic_array->amount >= dynamic_array->capacity) { dynamic_array->capacity = (size_t)ceil(dynamic_array->capacity * DYNAMIC_GROWTH_AMOUNT); void* new_data = realloc(dynamic_array->data, dynamic_array->capacity * dynamic_array->element_size); assert(new_data != NULL); dynamic_array->data = new_data; } unsigned char* destination = (unsigned char*)dynamic_array->data + dynamic_array->amount * dynamic_array->element_size; memcpy(destination, element, dynamic_array->element_size); dynamic_array->amount++; }
修复后,初始分配的内存大小就是1*16=16字节,刚好能容纳一个struct point实例,memcpy时不会再溢出,地址 sanitizer也不会报错。
内容的提问来源于stack exchange,提问作者Jagger Harris

