UTF-8字符串解析器内存泄漏与初始化异常Bug排查
初始版本内存异常根因
你遇到的偶发初始化失败、length字段异常为0的核心原因是3个明确的内存错误:
- 结构体堆分配大小完全错误:
utf8_string_t *str = (utf8_string_t*)malloc(sizeof(utf8_string_t*));这行只分配了指针本身的长度(32位系统4字节、64位系统8字节),远小于utf8_string_t结构体的实际内存大小,后续给str->raw/str->length/str->chars赋值时会直接越界写堆内存,破坏堆块元数据或相邻内存的内容,这就是偶发崩溃、字段值随机错乱的根本原因。 - 原始字符串缓冲区分配长度不足:
str->raw = (uint8_t*)malloc(strlen(s) * sizeof(uint8_t));没有为C字符串终止符\0预留1字节空间,后续如果按C字符串规则访问str->raw会触发越界读。 - 错误分支内存泄漏:raw、chars分配失败的分支中,之前已经分配成功的内存没有被释放就直接返回NULL,会持续泄漏堆内存。
修复版本仍存在的未定义行为隐患
你改后的版本修掉了最致命的结构体分配大小、raw缓冲区少1字节的问题,但还存在以下可能触发异常的点:
- 冗余且逻辑错误的free操作:
malloc返回NULL时代表分配失败,对应指针本身是空值,不需要再free:比如if (!str)分支里的free(str)、if (!str->raw)分支里的free(str->raw)、if (!str->chars)分支里的free(str->chars)都是无意义操作,虽然C标准允许free空指针不会触发异常,但属于逻辑冗余。 - 循环计数器类型不匹配:循环里用来给
chars数组计数的j是int类型,但str->length是uint64_t无符号64位类型,一旦解析的字符串长度超过int的取值范围(通常是2^31-1),j会发生整数溢出变成负数,后续写入str->chars[j]会直接写到完全错误的内存地址,触发越界写。 - 完全缺失UTF-8合法性校验:代码里直接用
utf8len(*p)的返回值作为多字节字符长度,没有校验返回值是否在14的合法范围内,也没有校验后续续字节是否符合UTF-8编码规则(必须是0x800xBF开头)。如果传入的字符串包含非法UTF-8字节,要么mb.bytes数组(固定4字节)写越界,要么遍历指针p会跳过末尾的\0一直往后读内存,触发越界访问。 - 码点计数和解析逻辑不一致风险:
str->length是提前调用utf8_strlen(s)计算出来的,如果utf8_strlen的计数规则和后续实际解析循环的规则不一致(比如对非法字节的处理逻辑不同),会出现两种异常:要么j超过str->length,写爆chars缓冲区触发越界;要么j小于str->length,chars缓冲区里残留未初始化的垃圾值。 - 冗余指针强转:C语言中
void*类型可以隐式转换为任意其他指针类型,不需要对malloc的返回值做强转,多余的强转反而可能掩盖忘记包含stdlib.h头文件导致的隐式声明错误。 - 错误日志丢失:所有分配失败的分支都去掉了
perror打印,后续线上出问题时没法快速定位失败原因。
最终修复建议
- 堆分配时永远用
sizeof(结构体类型)计算大小,不要用sizeof(结构体指针),避免出现分配长度不足的低级错误。 - 错误分支释放内存时,只释放已经确认分配成功的指针,不要对malloc失败返回的空指针做无意义free。
- 把循环计数器
j的类型改成和length一致的uint64_t,避免整数溢出。 - 解析循环中增加UTF-8合法性校验:首字节返回的字符长度必须在1~4之间,后续每个续字节都要符合UTF-8编码规范,遇到非法字节直接返回错误,不要继续遍历。
- 可以先遍历一次输入字符串,同时完成合法性校验、码点计数,再分配
chars缓冲区,避免提前计数和实际解析逻辑不一致导致的缓冲区大小错误。 - 保留分配失败时的错误日志打印,方便排查问题。
内容的提问来源于stack exchange,提问作者Moxy
相关产品推荐
相关产品推荐

