strncpy并非总是以null结尾

4

我正在使用以下代码:

char filename[ 255 ];
strncpy( filename, getenv( "HOME" ), 235 );
strncat( filename, "/.config/stationlist.xml", 255 );

收到以下信息:

(warning) Dangerous usage of strncat - 3rd parameter is the maximum number of characters to append.
(error) Dangerous usage of 'filename' (strncpy doesn't always null-terminate it).

7
你的确切问题是什么? - Shamim Hafiz - MSFT
2
只是好奇,是哪个编译器给出了这些消息? - andrew cooke
1
@andrewcooke Google表示它是cppcheck静态分析器。 - ouah
危险使用getenv():它可能会返回NULL,并导致str[n]cpy()的危险使用。 - wildplasser
这回答您的问题吗?为什么strncpy不会自动加上null终止符?(https://dev59.com/eXM_5IYBdhLWcg3wQQzw) - Ayxan Haqverdili
6个回答

8
我通常避免使用str*cpy()str*cat()函数。这些函数需要处理边界条件、晦涩的API定义和意外的性能后果。
相反,您可以使用snprintf()函数。您只需要考虑目标缓冲区的大小。而且,它更加安全,因为它不会溢出,并且总是为您添加NUL终止符。
char filename[255];
const char *home = getenv("HOME");
if (home == 0) home = ".";
int r = snprintf(filename, sizeof(filename), "%s%s", home, "/.config/stationlist.xml");
if (r >= sizeof(filename)) {
    /* need a bigger filename buffer... */
} else if (r < 0) {
    /* handle error... */
}

这是一个有趣的解决方案,但是如何在必须解析格式字符串("%s%s")和可变参数的情况下比直接从A复制字节到B更快呢? - Oliver
@Oliver:当我谈到性能后果时,我是指重复调用str*cat()而不是单个sprintf()调用来完成同样的任务。但你是正确的,在性能敏感的代码路径中频繁调用sprintf()不是一个好主意。我认为对于形成配置文件路径不会有问题。 - jxh

5

您的strncat调用可能会导致filename溢出。

请使用:

strncat(filename, "/.config/stationlist.xml",
        sizeof filename - strlen(filename) - 1);

在使用strncpy函数后,一定要记得在缓冲区末尾添加空字符:

strncpy( filename, getenv( "HOME" ), 235 );
filename[235] = '\0';

由于strncpy如果源字符串的长度大于等于最大复制字符数,它不会在目标缓冲区中添加空字符。


这个方法的每一页文档都需要打印出最后一句话。 - Maury Markowitz

3

man strncpy的说明如下:

Warning: If there is no null byte among the first n bytes
of src, the string placed in dest will not be null terminated.

如果在源字符串中在达到最大长度之前遇到了0字节,它将被复制。但是如果在源字符串中第一个0字节出现之前就达到了最大长度,目标字符串将不会被终止。最好在strncpy()返回后自己确认一下...

2

无论是 strncpy() 还是更甚的 strncat(),它们都有着不明显的行为,最好不要使用它们。

strncpy()

如果目标字符串,比如说长度为 255 字节,strncpy() 总会写入这 255 个字节。如果源字符串短于 255 字节,就会用零填充剩余部分。如果源字符串长于 255 字节,则会在复制了 255 字节后停止,导致目标字符串没有空字符终止符。

strncat()

对于大多数“大小已知”的函数(如 strncpy()memcpy()memmove() 等),大小参数是目标字符串(内存)中的字节数。而对于 strncat(),大小是目标字符串中已存在的字符串结尾之后的空间大小。因此,只有当您知道目标缓冲区的大小(S)以及目标字符串当前的长度(L)时才能安全地使用 strncat()。此时,strncat() 的安全参数为 S-L(我们将在其他时间考虑是否存在偏移一的情况)。但是,既然您知道了 L,就没有必要让 strncat() 跳过前面的 L 个字符;您可以将 target+L 作为开始位置,并直接复制数据。您也可以使用 memmove()memcpy(),或者甚至使用 strcpy()strncpy()。如果您不知道源字符串的长度,则必须有信心将其截断。

问题代码的分析

char filename[255];
strncpy(filename, getenv("HOME"), 235);
strncat(filename, "/.config/stationlist.xml", 255);

第一行是无可非议的,除非大小被认为太小(或者在未设置$HOME环境变量的情况下运行程序),但这超出了本问题的范围。调用strncpy()时没有使用sizeof(filename)来确定大小,而是使用任意小的数字。虽然不至于出大问题,但不能保证变量的最后20个字节是零字节(甚至其中任意一个都可能不是零字节)。在某些情况下(filename是全局变量,以前未使用过),可以保证有零字节。

strncat()调用试图将24个字符附加到filename字符串的末尾,该字符串可能已经长达232-234个字节,或者可以任意长达235个字节以上。不管怎样,这都是一种明显的缓冲区溢出。使用strncat()也直接陷入了有关其大小的陷阱中。你说可以在filename的现有字符串长度之外添加多达255个字符,这是彻头彻尾的错误(除非getenv("HOME")返回的字符串恰好为空)。

更安全的代码:

char filename[255];
static const char config_file[] = "/.config/stationlist.xml";
const char *home = getenv("HOME");
size_t len = strlen(home);
if (len > sizeof(filename) - sizeof(config_file))
    ...error file name will be too long...
else
{
    memmove(filename, home, len);
    memmove(filename+len, config_file, sizeof(config_file));
}

有人会坚称'memcpy()是安全的,因为字符串不会重叠',在某种程度上他们是正确的,但是重叠应该不是一个问题,而使用memmove(),这就不是一个问题了。所以我一直使用memmove()...但我没有进行时间测量来看看它有多大的问题,如果它真的是个问题的话。也许其他人已经做过测量。

总结

  1. 不要使用strncat()
  2. 谨慎使用strncpy()(注意它在非常大的缓冲区上的行为!)。
  3. 计划使用memmove()memcpy()代替;如果您能够安全地进行复制,则知道使其明智的大小。

1

1) 您的strncpy函数不一定会在文件名后添加空字符。事实上,如果getenv("HOME")的长度超过235个字符且getenv("HOME")[234]不是0,则不会添加空字符。 2) 您的strncat()可能会试图将文件名扩展到255个字符以上,因为如它所述,

3rd parameter is the maximum number of characters to append. 

(不是dst的总允许长度)


1
如果文件名是一个自动变量,它将不会被初始化(或者按你所说的“归零”)。 - wildplasser

0

strncpy(Copied_to,Copied_from,sizeof_input)在字符数组(非字符串类型)后输出垃圾值。为了解决这个问题,可以使用for循环遍历字符数组来输出,而不是简单地使用cout<<var;

for(i=0;i<size;i++){cout<<var[i]} 

我无法找到使用minGW编译器在Windows系统上遍历的解决方法。 空终止并不能解决这个问题。在线编译器可以正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接