我一直在使用 IDE 调用编译器,而无需进行太多配置,但从选项中我可以看到,我的项目似乎设置为使用 gnu99 作为 C 语言标准,使用 gnu++11 作为 C++ 语言标准,同时使用 gcc 版本 4.2.1。

该项目正在物联网的嵌入式设备上运行,我需要模拟它的某些部分,而我懒惰地使用了 onlinegdb。在那里,我注意到我可能正在不安全地构建字符串,我担心该项目可能会受到损害并需要紧急更新。我很确定我设计的字符串大小永远不会被我传递给它们的数据溢出,但前提是我正确实现了它,而我恐怕没有这样做。

这是一个可运行的示例:

#include <stdio.h>
#include <string.h>

int main()
{
    // example 1 works as expected initially - I think it is unsafe
    char testString1[6];
    sprintf(testString1, "123");
    sprintf(testString1, "%s456", testString1);
    printf("%s\n", testString1); // 123456
    
    // example 2 doesn't work as I expected - I think it is intrinsically safe though
    char testString2[6];
    snprintf(testString2, 6, "123");
    snprintf(testString2, 6, "%s456", testString2);
    printf("%s\n", testString2); // 456
    
    // example 3 what I found works and I think is safe (EDIT: I understand now it isn't after reading comments)
    char testString3[6];
    snprintf(testString3, 6, "123");
    snprintf(testString3 + strlen(testString3), 6, "456");
    printf("%s\n", testString3); // 123456

    // example 4 modified example3 for safety
    char testString4[6];
    snprintf(testString4, 6, "123");
    snprintf(testString4 + strlen(testString4), sizeof(testString4) - strlen(testString4), "4567");
    printf("%s\n", testString4); // 12345

    return 0;
}

这是输出:

main.c: In function ‘main’:
main.c:9:29: warning: ‘456’ directive writing 3 bytes into a region of size between 1 and 6 [-Wformat-overflow=]
    9 |     sprintf(testString1, "%s456", testString1);
      |                             ^~~
main.c:9:5: note: ‘sprintf’ output between 4 and 9 bytes into a destination of size 6
    9 |     sprintf(testString1, "%s456", testString1);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.c:15:33: warning: ‘456’ directive output may be truncated writing 3 bytes into a region of size between 1 and 6 [-Wformat-truncation=]
   15 |     snprintf(testString2, 6, "%s456", testString2);
      |                                 ^~~
main.c:15:5: note: ‘snprintf’ output between 4 and 9 bytes into a destination of size 6
   15 |     snprintf(testString2, 6, "%s456", testString2);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
123456
456
123456
12345

示例 1 是我到目前为止构建字符串的方式。不幸的是,我的 IDE/编译器没有警告我这一点,但 onlinegdb 却警告了我,可能是因为它使用了不同的编译器或语言标准。

示例 2 是我在注意到 onlinegdb 上的警告后第一次尝试使其更安全。我用 snprintf 替换了 sprintf,给了它一个大小限制,并继续用与示例 1 相同的模式填充它。这不起作用,只用新数据填充它。事实上,如果我删除示例 2 中额外的“456”字符,testString2 最终会被打印为空白。

示例 3 是我如何发现它非常有效并且我认为它是安全的。

我想问一下:

  • 示例 1 安全吗?我认为这不是由于示例 1 和 2 的警告造成的。
  • 为什么示例 2 没有按我的预期工作?
  • 例3真的安全吗?我可以安全地开始构建这样的字符串吗?

编辑1:添加示例4。根据我的理解,snprintf第二个参数是写入字符串的最大字符数,而不是该字符串的最大字符数,因此为了更安全,我减去了字符串的长度(我假设它是分配给字符串的总大小中放置空字符的索引,现在它限制了正在写入的字符数,即使我尝试写入超出适合的字符数。

7

  • 4
    请记住 C 中终止字符串的 0 字节,它需要声明长度中的一个字节。


    – 

  • 6
    snprintf(testString2, 6, "%s456", testString2);C 标准未定义的行为,因为 C 2018 7.21.6.5 2 表示“……如果在重叠的对象之间进行复制,则行为未定义。”您不应在sprintfor的输出和输入中使用相同的数组snprintf


    – 

  • 2
    您可以在您的 ide 中启用其他警告。添加-Wall -Wextra到您的编译命令行。


    – 


  • 2
    示例3不安全。它将把空终止符写入越界。


    – 

  • 1
    我认为第一步应该是安装一个还没有过时二十年的编译器。然后调高警告并在严格模式下编译。你的代码是非常错误的。


    – 



1 个回答
1

由于相同的原因,您的前两个示例是错误的:输出字符串与输入之一重叠。这样做会触发

第 7.21.6.6p2 节中关于该函数明确提到了这一点sprintf

sprintf函数等效于fprintf,不同之处在于输出被写入数组(由参数指定)而不是流中。写入的字符末尾写入空字符;它不计入返回值的一部分。如果复制发生在重叠的对象之间,则行为未定义。

关于该功能的第 7.21.6.5p2 节中也有相同的语言snprintf

您的第三个示例不会遇到此问题,但是目标位置不够大,无法存储所需的字符串。它需要空间容纳您要输入的 6 个字符,以及终止空字节。

示例 4 之所以有效,是因为第二次调用snprintf限制了附加到数组剩余长度的内容。

你实际上不需要sprintfsnprintf做你想做的事。您可以简单地使用strcpystrcat

char testString[7];
strcpy(testString, "123");
strcat(testString, "456");

您还可以使用strncat它来snprintf指定大小限制(在本snprintf例中,应该是可用空间,而不是总空间)。还有该strncpy函数,但不建议使用该函数,因为它不会在所有情况下自动以 null 终止目标字符串。

7

  • 刚刚添加了示例 4,对示例 3 进行了修改,以限制添加到字符串中的字符数。这是有效的吗?我可以使用 strncat 并以任何方式指定限制,以确保它不会写入字符串之外吗?


    – 

  • 我明确建议不要使用 ,strncpy因为每个初学者都认为它是 的更安全版本strcpy,就像strncat的更安全版本一样strcat


    – 

  • 2
    @rmarques:使用strncat()更有可能导致错误而不是解决问题。躲开它。也避免使用strncpy(),尽管它比 更容易正确使用strncat()。注意:如果比字符串短,即使是空字符串,也会strncat(dst, sizeof(dst), “long string”)越界。dstdst


    – 

  • @JonathanLeffler 至少strncat()保证放置一个 nul 终止符,但strncpy()没有这样的保证。令人惊讶的是。


    – 

  • @WeatherVane — 但与 不同的是,strncpy()当大小指定为 时,不会超出范围。两者都不令人满意或真正安全。两者都是危险的。但我拒绝使用,而我偶尔会使用,通常会在目标字符串的最后一个字节中随后分配一个空字节。sizeof(target)strncpy()strncat()strncpy()


    –