调试数组越界问题
为了理解 Git 的工作机制,近期尝试用 C 语言编写 Git 基本功能的简单实现。一如继往 ,在调试 SHA-1 哈希码与对象文件生成的过程中也遇到了一些“怪异”的错误。最终发现是数组越界导致的。由于编译器给出的错误只是现象而不是原因,而且是站在编译的角度来看的,因此当数组存在越界时,报出的错误往往非常“怪异”,显示的出错位置也一般不是错误的发源处。例如,今天在调试 git-in-c 时,程序在退出时会报 OpenSSL CTX 的 free(str)
出现问题。若怀疑是 OpenSSL 的问题,则根本无从下手解决。又使用了 valgrind 检测内存是否有错误,结果报告中的一个错误说是:
==391667== Use of uninitialised value of size 8
==391667== at 0x4EF0A42: _itoa_word (_itoa.c:178)
==391667== by 0x4EFBE58: __vfprintf_internal (vfprintf-process-arg.c:164)
==391667== by 0x4F172F6: __vsprintf_internal (iovsprintf.c:96)
==391667== by 0x4EF7D60: sprintf (sprintf.c:30)
==391667== by 0x499EB6C: generate_object_path_components_from_sha1_hash (cgit.c:609)
==391667== by 0x499EC2F: write_blob_object_file (cgit.c:632)
==391667== by 0x1093FB: main (main.c:64)
==391667== Uninitialised value was created by a stack allocation
==391667== at 0x109219: main (main.c:11)
但实际上,我对于传入 sprintf
的动态数组,都已经正确分配了内存,而是否对其初始化并不必要。之后,即便我增加了 memset
对这段内存初始化,也还是同样的问题。
最终发现,下面这段代码出现了数组越界:
for (size_t i = 1; i <= SHA1_BYTE_LENGTH; i++)
sprintf((char *)(path_components[2] + (i - 1) * 2), "%02x", sha1_hash[i]);
正确的应为:
for (size_t i = 1; i < SHA1_BYTE_LENGTH; i++)
sprintf((char *)(path_components[2] + (i - 1) * 2), "%02x", sha1_hash[i]);
这样一来,一切就说得通了:
- 数组越界后,访问的内存区间超出了我的程序通过
malloc
申请的范围,所以valgrind
提示说有 8 个字节的内存未经初始化。 - 因为数组越界,也影响到了 OpenSSL 使用到的内存,从而产生了意想不到的效果。
Lessons learned
C/C++ 程序编译与运行过程中出现“怪异”现象时,若编译器给出的错误提示与代码位置以及程序在异常退出时产生的 coredump 堆栈信息不符合我们的直观理解,就不能紧盯着错误提示、代码位置、堆栈函数不放,而要在出错位置的周围找原因。这是由于编译器与程序给出的信息只是现象而不是原因,而且是站在编译器与程序代码的角度来看的。这就要求我们理解编译器的“潜台词”(connotation),透过表象抓住背后的本质。