
2.2.3 空字符结尾错误
另一个常见的问题是字符串没有正确地以空字符结尾。一个字符串正确地以空字符结尾,是指在数组最后一个元素处或在它之前存在一个空终结符。如果一个字符串没有以空字符结尾,程序可能会被欺骗,导致在数组边界之外读取或写入数据。
字符串必须在数组的最后一个元素的地址处或在它之前包含一个空终止字符,才可以安全地作为标准字符串处理函数如strcpy()函数或strlen()函数的参数被传递。空终止字符之所以是必要的,是因为前面这些函数以及其他由C标准定义的字符串处理函数,都依赖于它的存在来标记字符串的结尾。同样,如果程序对一个字符数组迭代循环的终止条件取决于为字符串分配的内存内是否存在一个空终止字符,字符串也必须以空字符结尾。
1 size_t i; 2 char ntbs[16]; 3 /* ... */ 4 for (i = 0; i < sizeof(ntbs); ++i) { 5 if (ntbs[i] == '\0') break; 6 /* ... */ 7 }
下面的程序在微软Visual C++2010中能通过编译,但在警告级别/W3下会对使用strncpy()和strcpy()发出警告。如果_FORTIFY_SOURCE宏被定义为一个非零值,它在Linux下还会(在运行时)由GCC诊断。
1 int main(void) { 2 char a[16]; 3 char b[16]; 4 char c[16]; 5 strncpy(a, "0123456789abcdef", sizeof(a)); 6 strncpy(b, "0123456789abcdef", sizeof(b)); 7 strcpy(c, a); 8 /* ... */ 9 }
在这个程序中,三个字符数组(a[]、b[]和c[])被声明为16个字节。虽然strncpy()到a仅限于写sizeof(a)(16个字节),但由于strncpy()函数的历史和标准的行为,导致结果字符串不是以空字符结尾的。
根据C标准,strncpy()函数从源数组复制不超过n个字符(空字符后的字符不会被复制)到目标数组。因此,如本例所示,如果源数组中的前n个字符中不存在空字符,那么其结果字符串将不会是以空字符结尾的。
strncpy()到b也有类似的结果。这取决于编译器如何分配存储空间,a[]后的存储空间可能碰巧存在一个空字符,但是这是编译器未指定的,并在本例中是不太可能的,尤其是存储空间是紧密堆积的时候。其结果是strcpy()到c可能写得远远超出了数组界限,因为a[]中存储的字符串不是正确地以空字符结尾的。
《C安全编码标准》[Seacord 2008]包括“STR 32-C.按要求提供空字节结尾的字符串”。请注意,该规则并不排除使用字符数组。例如,即使在调用strncpy()之后,存储在ntbs字符数组中的字符串可能不是正确地以空字符结尾的,下面的程序片段也没有什么错。
1 char ntbs[NTBS_SIZE]; 2 3 strncpy(ntbs, source, sizeof(ntbs)-1); 4 ntbs[sizeof(ntbs)-1] = '\0';
与本章中描述的其他字符串操作错误一样,空字符结尾错误也很难检测,它们会潜伏在部署好的代码中,直至遇到一组特别的输入而导致发生错误。编写代码不能依赖于编译器如何分配内存,因为这在编译器的下个版本中很可能发生改变。