【问题标题】:How To Avoid Abort Trap 6 Error at Runtime Using strncat()?如何使用 strncat() 在运行时避免 Abort Trap 6 错误?
【发布时间】:2019-01-05 17:29:08
【问题描述】:

中止陷阱 6 问题源于调用 extra_info() 方法,在该方法中它多次使用 strncat()。删除此功能不会在运行时产生错误。

据我了解:

Abort trap: 6 是由使用引起的 指向不存在的内存位置的无效索引 Abort trap: 6 in C Program。 它也可能在需要释放变量内存时发生。避免 在这种情况下,您可以使用多个变量或释放单个变量 每次重复使用时都会发生变化。但我感觉解决方案要简单得多。

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

char line[1001]; // The line supports up to a 1000 characters
char lines[11][1001]; // An array of lines (up to 10 lines where each line is a 1000 characters max)
char info[100]; // Holds extra info provided by user

char * extra_info(
        char string_1[],
        char string_2[],
        char string_3[],
        char string_4[],
        char string_5[]
    );

int main(){

    int 
    i, // Line number
    j; // Length of the line
    char result[100], text[100];
    FILE *file;

    strcpy(text, "String No."); // The default text

    file = fopen("test.txt", "w+"); // Open the file for reading and writing

    for(i = 0; i < 10; i++){ // Loop to create a line.

        if(i != 9){ // If the line is NOT at the 10th string

            sprintf(result, "%s%d, ", text, i); // Format the text and store it in result

        }
        else{

            sprintf(result, "%s%d ", text, i); // Format the text and store it in result            

        }

        extra_info(
            "st",
            "nd",
            "rd",
            "th",
            "th"
        );

        strncat(line, info, 100); // Append the extra info at the end of each line        

        printf("%s", result); // Display the result variable to the screen

        strncat(line, result, 15); // Concatenate all strings in one line

    }

    strncat(line, "\n\n", 2); // Add a new-line character at the end of each line

    for(j = 0; j < 10; j++){ // Now loop to change the line

        strcpy(lines[i], line); // Copy the line of text into each line of the array

        fputs(lines[i], file); // Put each line into the file        

    }

    fclose(file);  

}

char * extra_info( // Append user defined and predefined info at the end of a line
        char string_1[],
        char string_2[],
        char string_3[],
        char string_4[],
        char string_5[]
    ){
        char text[100]; // A variable to hold the text

        /* Append a default text into each strings 
        and concatenate them into one line */

        sprintf(text, " 1%s", string_1);
        strncat(line, text, 100);

        sprintf(text, ", 2%s", string_2);
        strncat(line, text, 100);

        sprintf(text, ", 3%s", string_3);
        strncat(line, text, 100);

        sprintf(text, ", 4%s", string_4);
        strncat(line, text, 100);

        sprintf(text, ", 5%s.", string_5);
        strncat(line, text, 100);

        strcpy(info, line); // Copies the line into the info global variable

        return line;

}

这段代码使用 GCC 可以很好地编译,但我偶然发现了代码运行良好的情况,但可能会因为这个错误而破坏某些功能。这与 strncat() 以这种方式被多次调用有关,这使我认为会有内存分配问题,但是在尝试了其他示例之后,解决方案似乎要简单得多。对此的任何帮助将不胜感激。提前致谢。

【问题讨论】:

  • 先修复警告怎么样?你可以这样编译:gcc prog.c -Wall -Wextra
  • 另外,当您遇到崩溃时,在调试器中运行以定位崩溃发生的时间和地点可能是一个好主意。
  • @gsamaras 使用没有标志的 GCC 编译这个没有警告。
  • @Someprogrammerdude 实际上我已经找到了这个问题,因此相应地编辑了帖子。查看第一段。
  • 如果 target 的长度为 100 字节,即使它包含空字符串,您也不能使用 strncat(target, source, 100)。您遇到了缓冲区溢出。 strncat() 函数的接口是邪恶的和卑鄙的,你永远不应该使用它。如果您有足够的信息可以安全使用它,您可以改用memmove()

标签: c


【解决方案1】:

我在 2018 年 3 月编写了随附的代码,以使自己对 strncat() 的情况感到满意,该问题在我提交答案之前已被删除。这只是重新定位该代码。

strncat() 函数(正如我在comment 中所说)邪恶而卑鄙。它也与strncpy() 界面不一致——并且与您在其他任何地方会遇到的任何东西都不同。阅读本文后,您将决定(幸运地)永远不要使用strncat()

TL;DR — 永远不要使用 strncat()

C 标准定义了strncat()(POSIX 也同意——strncat()

C11 §7.24.3.2 strncat 函数

概要

#include <string.h>
char *strncat(char * restrict s1, const char * restrict s2, size_t n);

说明

strncat 函数从s2 指向的数组中追加不超过n 个字符(不追加空字符和后面的字符)到s1 指向的字符串末尾. s2 的初始字符覆盖s1 末尾的空字符。终止的空字符始终附加到结果中。309) 如果复制发生在重叠的对象之间,则行为未定义。

退货

strncat 函数返回s1 的值。

309) 因此,s1 指向的数组中可以包含的最大字符数为strlen(s1)+n+1

脚注用strncat()标识了最大的陷阱——你不能安全地使用:

char *source = …;

char target[100] = "";

strncat(target, source, sizeof(target));

这与在 C 代码中采用数组大小​​参数 1 的大多数其他函数的情况相反。

要安全使用strncat(),你应该知道:

  • target
  • sizeof(target) — 或者,对于动态分配的空间,分配的长度
  • strlen(target) — 您必须知道目标字符串中已有内容的长度
  • source
  • strlen(source) — 如果您担心源字符串是否被截断;如果您不在乎,则不需要

有了这些信息,您可以使用:

strncat(target, source, sizeof(target) - strlen(target) - 1);

但是,这样做会有点傻;如果你知道strlen(target),你可以避免让strncat()再次找到它,使用:

strncat(target + strlen(target), source, sizeof(target) - strlen(target) - 1);

请注意,strncat() 保证空终止,这与 strncpy() 不同。这意味着您可以使用:

size_t t_size = sizeof(target);
size_t t_length = strlen(target);
strncpy(target + t_length, source, t_size - t_length - 1);
target[t_size - 1] = '\0';

如果源字符串太长而无法附加到目标,则可以保证得到相同的结果。

演示代码

说明strncat() 方面的多个程序。请注意,在 macOS 上,&lt;string.h&gt; 中有一个 strncat() 的宏定义,它调用了一个不同的函数——__builtin___strncat_chk——它验证了strncat() 的使用。为了命令行的紧凑性,我删除了两个我通常使用的警告编译器选项——-Wmissing-prototypes -Wstrict-prototypes——但这不会影响任何编译。

strncat19.c

这演示了strncat() 的一种安全用法:

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

int main(void)
{
    char spare1[16] = "abc";
    char buffer[16] = "";
    char spare2[16] = "xyz";
    strncat(buffer, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", sizeof(buffer) - 1);
    printf("%zu: [%s]\n", strlen(buffer), buffer);
    printf("spare1 [%s]\n", spare1);
    printf("spare2 [%s]\n", spare2);
    return 0;
}

它编译干净(使用来自 XCode 10.1 (Apple LLVM version 10.0.0 (clang-1000.11.45.5)) 和 GCC 8.2.0 的 Apple 的 clang,即使设置了严格的警告:

$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror strncat19.c -o strncat19
$ ./strncat19
15: [ABCDEFGHIJKLMNO]
spare1 [abc]
spare2 [xyz]
$

strncat29.c

这类似于strncat19.c,但 (a) 允许您指定要在命令行上复制的字符串,并且 (b) 错误地使用 sizeof(buffer) 而不是 sizeof(buffer) - 1 作为长度。

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

int main(int argc, char **argv)
{
    const char *data = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    if (argc == 2)
        data = argv[1];
    char spare1[16] = "abc";
    char buffer[16] = "";
    char spare2[16] = "xyz";
    strncat(buffer, data, sizeof(buffer));
    printf("%zu: [%s]\n", strlen(buffer), buffer);
    printf("spare1 [%s]\n", spare1);
    printf("spare2 [%s]\n", spare2);
    return 0;
}

此代码无法使用严格的警告选项进行编译:

$ clang -O3 -g -std=c11 -Wall -Wextra -Werror strncat29.c -o strncat29  
strncat29.c:12:27: error: the value of the size argument in 'strncat' is too large, might lead to a buffer
      overflow [-Werror,-Wstrncat-size]
    strncat(buffer, data, sizeof(buffer));
                          ^~~~~~~~~~~~~~
strncat29.c:12:27: note: change the argument to be the free space in the destination buffer minus the terminating null byte
    strncat(buffer, data, sizeof(buffer));
                          ^~~~~~~~~~~~~~
                          sizeof(buffer) - strlen(buffer) - 1
1 error generated.
$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror strncat29.c -o strncat29  
In file included from /usr/include/string.h:190,
                 from strncat29.c:2:
strncat29.c: In function ‘main’:
strncat29.c:12:5: error: ‘__builtin___strncat_chk’ specified bound 16 equals destination size [-Werror=stringop-overflow=]
     strncat(buffer, data, sizeof(buffer));
     ^~~~~~~
cc1: all warnings being treated as errors
$

即使没有请求警告,警告也是由 GCC 给出的,但由于没有 -Werror 选项,它会生成一个可执行文件:

$ gcc -o strncat29 strncat29.c
In file included from /usr/include/string.h:190,
                 from strncat29.c:2:
strncat29.c: In function ‘main’:
strncat29.c:12:5: warning: ‘__builtin___strncat_chk’ specified bound 16 equals destination size [-Wstringop-overflow=]
     strncat(buffer, data, sizeof(buffer));
     ^~~~~~~
$ ./strncat29
Abort trap: 6
$ ./strncat29 ZYXWVUTSRQPONMK
15: [ZYXWVUTSRQPONMK]
spare1 [abc]
spare2 [xyz]
$ ./strncat29 ZYXWVUTSRQPONMKL
Abort trap: 6
$

这就是 __builtin__strncat_chk 的作用。

strncat97.c

这段代码也有一个可选的字符串参数;它还关注命令行是否有其他参数,如果有,则直接调用strncat()函数,而不是让宏先检查:

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

/*
** Demonstrating that strncat() should not be given sizeof(buffer) as
** the size, even if the string is empty to start with.  The use of
** (strncat) inhibits the macro expansion on macOS; the code behaves
** differently when the __strncat_chk function (on High Sierra or
** earlier - it's __builtin__strncat_chk on Mojave) is called instead.
** You get an abort 6 (but no other useful message) when the buffer
** length is too long.
*/

int main(int argc, char **argv)
{
    const char *data = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    if (argc >= 2)
        data = argv[1];
    char spare1[16] = "abc";
    char buffer[16] = "";
    char spare2[16] = "xyz";
    size_t len = (argc == 2) ? sizeof(buffer) : sizeof(buffer) - 1;
    if (argc < 3)
        strncat(buffer, data, len);
    else
        (strncat)(buffer, data, len);
    printf("buffer %2zu: [%s]\n", strlen(buffer), buffer);
    printf("spare1 %2zu: [%s]\n", strlen(spare1), spare1);
    printf("spare2 %2zu: [%s]\n", strlen(spare2), spare2);
    return 0;
}

现在编译器会产生不同的结果:

$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror strncat97.c -o strncat97  
strncat97.c: In function ‘main’:
strncat97.c:26:9: error: ‘strncat’ output truncated copying 15 bytes from a string of length 26 [-Werror=stringop-truncation]
         (strncat)(buffer, data, len);
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors
$ clang -O3 -g -std=c11 -Wall -Wextra -Werror strncat97.c -o strncat97  
$

这展示了使用多个编译器的优势——不同的编译器有时会检测到不同的问题。尝试使用不同数量的选项来做多项事情时,这段代码很混乱。足以表明:

$ ./strncat97
0x7ffee7506420: buffer 15: [ABCDEFGHIJKLMNO]
0x7ffee7506430: spare1  3: [abc]
0x7ffee7506410: spare2  3: [xyz]
$ ./strncat97 ABCDEFGHIJKLMNOP
Abort trap: 6
$ ./strncat97 ABCDEFGHIJKLMNO
0x7ffeea141410: buffer 15: [ABCDEFGHIJKLMNO]
0x7ffeea141420: spare1  3: [abc]
0x7ffeea141400: spare2  3: [xyz]
$

strncat37.c

这是上述程序的全唱歌、全跳舞版本,通过getopt() 处理选项。它还使用我的错误报告程序;它们的代码可以在我在 GitHub 上的 SOQ(堆栈溢出问题)存储库中以文件 stderr.cstderr.h 的形式在 src/libsoq 子目录中找到。

#include "stderr.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>

/*
** Demonstrating that strncat() should not be given sizeof(buffer) as
** the size, even if the string is empty to start with.  The use of
** (strncat) inhibits the macro expansion on macOS; the code behaves
** differently when the __strncat_chk function (on High Sierra or
** earlier - it's __builtin__strncat_chk on Mojave) is called instead.
** You get an abort 6 (but no other useful message) when the buffer
** length is too long.
*/

static const char optstr[] = "fhlmsV";
static const char usestr[] = "[-fhlmsV] [string]";
static const char hlpstr[] =
    "  -f  Function is called directly\n"
    "  -h  Print this help message and exit\n"
    "  -l  Long buffer length -- sizeof(buffer)\n"
    "  -m  Macro cover for the function is used (default)\n"
    "  -s  Short buffer length -- sizeof(buffer)-1 (default)\n"
    "  -V  Print version information and exit\n"
    ;

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);

    int f_flag = 0;
    int l_flag = 0;
    int opt;

    while ((opt = getopt(argc, argv, optstr)) != -1)
    {
        switch (opt)
        {
        case 'f':
            f_flag = 1;
            break;
        case 'h':
            err_help(usestr, hlpstr);
            /*NOTREACHED*/
        case 'l':
            l_flag = 1;
            break;
        case 'm':
            f_flag = 0;
            break;
        case 's':
            l_flag = 0;
            break;
        case 'V':
            err_version(err_getarg0(), &"@(#)$Revision$ ($Date$)"[4]);
            /*NOTREACHED*/
        default:
            err_usage(usestr);
            /*NOTREACHED*/
        }
    }

    if (optind < argc - 1)
        err_usage(usestr);

    const char *data = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    if (optind != argc)
        data = argv[optind];
    char spare1[16] = "abc";
    char buffer[16] = "";
    char spare2[16] = "xyz";
    size_t len = l_flag ? sizeof(buffer) : sizeof(buffer) - 1;

    printf("Specified length: %zu\n", len);
    printf("Copied string: [%s]\n", data);
    printf("Copied %s\n", f_flag ? "using strncat() function directly"
                                 : "using strncat() macro");

    if (f_flag)
        (strncat)(buffer, data, len);
    else
        strncat(buffer, data, len);

    printf("%p: buffer %2zu: [%s]\n", (void *)buffer, strlen(buffer), buffer);
    printf("%p: spare1 %2zu: [%s]\n", (void *)spare1, strlen(spare1), spare1);
    printf("%p: spare2 %2zu: [%s]\n", (void *)spare2, strlen(spare2), spare2);
    return 0;
}

和以前一样,Clang 和 GCC 对代码的可接受性有不同的看法(-Werror 表示来自 GCC 的警告被视为错误):

$ clang -O3 -g -I./inc -std=c11 -Wall -Wextra -Werror strncat37.c -o strncat37 -L./lib  -lsoq 
$ gcc -O3 -g -I./inc -std=c11 -Wall -Wextra -Werror strncat37.c -o strncat37 -L./lib  -lsoq 
strncat37.c: In function ‘main’:
strncat37.c:80:9: error: ‘strncat’ output may be truncated copying between 15 and 16 bytes from a string of length 26 [-Werror=stringop-truncation]
         (strncat)(buffer, data, len);
         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
cc1: all warnings being treated as errors
$

运行时:

$ ./strncat37 -h
Usage: strncat37 [-fhlmsV] [string]
  -f  Function is called directly
  -h  Print this help message and exit
  -l  Long buffer length -- sizeof(buffer)
  -m  Macro cover for the function is used (default)
  -s  Short buffer length -- sizeof(buffer)-1 (default)
  -V  Print version information and exit

$ ./strncat37
Specified length: 15
Copied string: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
Copied using strncat() macro
0x7ffedff4e400: buffer 15: [ABCDEFGHIJKLMNO]
0x7ffedff4e410: spare1  3: [abc]
0x7ffedff4e3f0: spare2  3: [xyz]
$ ./strncat37 -m -s
Specified length: 15
Copied string: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
Copied using strncat() macro
0x7ffeeaf043f0: buffer 15: [ABCDEFGHIJKLMNO]
0x7ffeeaf04400: spare1  3: [abc]
0x7ffeeaf043e0: spare2  3: [xyz]
$ ./strncat37 -m -l
Specified length: 16
Copied string: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
Copied using strncat() macro
Abort trap: 6
$ ./strncat37 -f -s
Specified length: 15
Copied string: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
Copied using strncat() function directly
0x7ffeef0913f0: buffer 15: [ABCDEFGHIJKLMNO]
0x7ffeef091400: spare1  3: [abc]
0x7ffeef0913e0: spare2  3: [xyz]
$ ./strncat37 -f -l
Specified length: 16
Copied string: [ABCDEFGHIJKLMNOPQRSTUVWXYZ]
Copied using strncat() function directly
0x7ffeed8d33f0: buffer 16: [ABCDEFGHIJKLMNOP]
0x7ffeed8d3400: spare1  0: []
0x7ffeed8d33e0: spare2  3: [xyz]
$

默认行为也是正确行为;该程序不会崩溃,也不会产生意想不到的副作用。当使用宏运行并且指定的长度过长 (-m -l) 时,程序会崩溃。当使用该函数运行且长度过长 (-f -l) 时,程序会覆盖数组 spare1 的第一个字节,并在 buffer 末尾添加空值,并显示 16 个字节的数据而不是 15 个。


1 当您使用%31s 或类似名称时,scanf() 中的一个例外情况;指定的数字是可以存储在字符串中的非空字符数;它会在读取 31 个其他字符后添加一个空字节。同样,可以安全使用的最大大小是sizeof(string) - 1

您可以在我的SOQ(堆栈溢出问题)存储库中的src/so-5405-4423 子目录中找到strncatXX.c 的代码。


问题代码分析

从问题中获取代码并将int main(){ 更改为int main(void){,因为我的默认编译选项会为非原型main() 生成错误(如果我不使用-Werror,这将是一个警告) ,并在main() 的末尾添加return 0;,剩下的就是在运行macOS 10.14.2 Mojave 的Mac 上使用GCC 8.2.0 编译时出现这些错误:

$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes so-5405-4423-v1.c -o so-5405-4423-v1 
In file included from /opt/gcc/v8.2.0/lib/gcc/x86_64-apple-darwin17.7.0/8.2.0/include-fixed/stdio.h:425,
                 from so-5405-4423-v1.c:1:
so-5405-4423-v1.c: In function ‘main’:
so-5405-4423-v1.c:32:29: error: ‘%d’ directive writing between 1 and 2 bytes into a region of size between 1 and 100 [-Werror=format-overflow=]
             sprintf(result, "%s%d, ", text, i); // Format the text and store it in result
                             ^~~~~~~~
so-5405-4423-v1.c:32:29: note: directive argument in the range [0, 10]
so-5405-4423-v1.c:32:13: note: ‘__builtin___sprintf_chk’ output between 4 and 104 bytes into a destination of size 100
             sprintf(result, "%s%d, ", text, i); // Format the text and store it in result
             ^~~~~~~
so-5405-4423-v1.c:37:29: error: ‘ ’ directive writing 1 byte into a region of size between 0 and 99 [-Werror=format-overflow=]
             sprintf(result, "%s%d ", text, i); // Format the text and store it in result
                             ^~~~~~~
so-5405-4423-v1.c:37:13: note: ‘__builtin___sprintf_chk’ output between 3 and 102 bytes into a destination of size 100
             sprintf(result, "%s%d ", text, i); // Format the text and store it in result
             ^~~~~~~
cc1: all warnings being treated as errors
$

编译器注意到 text 是一个可以包含 0 到 99 个字符的字符串,因此理论上它在与数字和 ", "(或 " " 进行一次迭代)连接时可能会导致溢出。它被初始化为 "String No." 的事实意味着不存在溢出风险,但您可以通过为 text 使用更短的长度来缓解这种情况——比如 20 而不是 100

我承认这个在 GCC 中相对较新的警告并不总是那么有用(这是代码正常但警告仍然出现的情况)。我通常修复这个问题,只是因为它当前显示了我的默认选项并且代码没有编译并带有-Werror的任何警告,而且我还没有准备好没有那个级别保护。我不使用 clang-Weverything 选项 raw;它会产生绝对适得其反的警告(至少 AFAIAC)。但是,我反对对我不起作用的“一切”选项。如果 -Wall-Wextra 选项太痛苦,出于某种原因,我会取消它,但要谨慎。我会检查疼痛程度,并针对任何症状进行处理。

你也有循环:

for(j = 0; j < 10; j++){ // Now loop to change the line

    strcpy(lines[i], line); // Copy the line of text into each line of the array

    fputs(lines[i], file); // Put each line into the file        

}   

不幸的是,当这个循环运行时,i 等于10,它超出了数组lines 的范围。这可能导致崩溃。据推测,索引应该是j 而不是i

这是您的代码的检测版本 (so-5405-4423-v2.c):

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

char line[1001];
char lines[11][1001];
char info[100];

char *extra_info(char string_1[], char string_2[], char string_3[],
                 char string_4[], char string_5[]);

int main(void)
{
    char result[100], text[20];
    const char filename[] = "test.txt";
    FILE *file;

    strcpy(text, "String No.");

    file = fopen(filename, "w+");
    if (file == NULL)
    {
        fprintf(stderr, "Failed to open file '%s' for writing/update\n", filename);
        return 1;
    }

    for (int i = 0; i < 10; i++)
    {
        if (i != 9)
            sprintf(result, "%s%d, ", text, i);
        else
            sprintf(result, "%s%d ", text, i);

        fprintf(stderr, "Iteration %d:\n", i);
        fprintf(stderr, "1 result (%4zu): [%s]\n", strlen(result), result);
        fprintf(stderr, "1 line   (%4zu): [%s]\n", strlen(line), line);
        extra_info("st", "nd", "rd", "th", "th");
        fprintf(stderr, "2 line   (%4zu): [%s]\n", strlen(line), line);
        fprintf(stderr, "1 info   (%4zu): [%s]\n", strlen(info), info);
        strncat(line, info, 100);
        fprintf(stderr, "3 line   (%4zu): [%s]\n", strlen(line), line);
        printf("%s", result);
        strncat(line, result, 15);
        fprintf(stderr, "3 line   (%4zu): [%s]\n", strlen(line), line);
    }

    fprintf(stderr, "4 line   (%4zu): [%s]\n", strlen(line), line);
    strncat(line, "\n\n", 2);

    for (int j = 0; j < 10; j++)
    {
        strcpy(lines[j], line);
        fputs(lines[j], file);
    }

    fclose(file);

    return 0;
}

char *extra_info(char string_1[], char string_2[], char string_3[],
                 char string_4[], char string_5[])
{
    char text[100];

    sprintf(text, " 1%s", string_1);
    fprintf(stderr, "EI 1: add (%zu) [%s] to (%zu) [%s]\n", strlen(string_1), string_1, strlen(line), line);
    strncat(line, text, 100);

    sprintf(text, ", 2%s", string_2);
    fprintf(stderr, "EI 2: add (%zu) [%s] to (%zu) [%s]\n", strlen(string_2), string_2, strlen(line), line);
    strncat(line, text, 100);

    sprintf(text, ", 3%s", string_3);
    fprintf(stderr, "EI 3: add (%zu) [%s] to (%zu) [%s]\n", strlen(string_3), string_3, strlen(line), line);
    strncat(line, text, 100);

    sprintf(text, ", 4%s", string_4);
    fprintf(stderr, "EI 4: add (%zu) [%s] to (%zu) [%s]\n", strlen(string_4), string_4, strlen(line), line);
    strncat(line, text, 100);

    sprintf(text, ", 5%s.", string_5);
    fprintf(stderr, "EI 5: add (%zu) [%s] to (%zu) [%s]\n", strlen(string_5), string_5, strlen(line), line);
    strncat(line, text, 100);

    fprintf(stderr, "EI 6: copy (%zu) [%s] to info\n", strlen(line), line);
    strcpy(info, line);

    return line;
}

运行时,它会产生类似于以下内容的输出:

Iteration 0:
1 result (  13): [String No.0, ]
1 line   (   0): []
EI 1: add (2) [st] to (0) []
EI 2: add (2) [nd] to (4) [ 1st]
EI 3: add (2) [rd] to (9) [ 1st, 2nd]
EI 4: add (2) [th] to (14) [ 1st, 2nd, 3rd]
EI 5: add (2) [th] to (19) [ 1st, 2nd, 3rd, 4th]
EI 6: copy (25) [ 1st, 2nd, 3rd, 4th, 5th.] to info
2 line   (  25): [ 1st, 2nd, 3rd, 4th, 5th.]
1 info   (  25): [ 1st, 2nd, 3rd, 4th, 5th.]
3 line   (  50): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.]
3 line   (  63): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0, ]
Iteration 1:
1 result (  13): [String No.1, ]
1 line   (  63): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0, ]
EI 1: add (2) [st] to (63) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0, ]
EI 2: add (2) [nd] to (67) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st]
EI 3: add (2) [rd] to (72) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd]
EI 4: add (2) [th] to (77) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd]
EI 5: add (2) [th] to (82) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th]
EI 6: copy (88) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.] to info
2 line   (  88): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.]
1 info   (  88): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.]
3 line   ( 176): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.]
3 line   ( 189): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1, ]
Iteration 2:
1 result (  13): [String No.2, ]
1 line   ( 189): [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1, ]
EI 1: add (2) [st] to (189) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1, ]
EI 2: add (2) [nd] to (193) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1,  1st]
EI 3: add (2) [rd] to (198) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1,  1st, 2nd]
EI 4: add (2) [th] to (203) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1,  1st, 2nd, 3rd]
EI 5: add (2) [th] to (208) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1,  1st, 2nd, 3rd, 4th]
EI 6: copy (214) [ 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th. 1st, 2nd, 3rd, 4th, 5th.String No.0,  1st, 2nd, 3rd, 4th, 5th.String No.1,  1st, 2nd, 3rd, 4th, 5th.] to info
String No.0, String No.1, Abort trap: 6

当您观察到 214 个字节从 line(足以容纳该字符串)复制到 info(不是 - 它只有 100 个字节长)时,随之而来的崩溃并不令人惊讶。尚不完全清楚期望的行为是什么。

在我的 Mac 上,lldb 调试器在__strcpy_chk 中报告崩溃; AFAICT,它在extra_info() 末尾突出显示的行中:

frame #6: 0x00007fff681bbe84 libsystem_c.dylib`__strcpy_chk + 83
frame #7: 0x00000001000017cc so-5405-4423-v2`extra_info(string_1=<unavailable>, string_2=<unavailable>, string_3="rd", string_4="th", string_5="th") at so-5405-4423-v2.c:86

因此,虽然显然不是 strncat() 导致了这里的崩溃,但 strncat() 的使用方式显然并不正确 - IMO,它不正确,但观点可能不同.我仍然坚持我的基本结论:不要使用strncat()

【讨论】:

  • 好吧,感谢所有的花里胡哨。我假设您在这些示例中的意思是 sizeof(string) + 1 。每当我需要优化此部分时,我都会牢记这一点。这些是重构的重要说明。从 strcat() 切换是一个简单的解决方法,但不是罪魁祸首。
  • 我不确定你认为我在哪里可能指的是 sizeof(string) + 1 并且没有使用它——尽管如果你指的是你的调试负载版本中的 fprintf() 语句代码,那么它是“程序员的选择”;我选择报告字符串长度,尽管报告缓冲区长度是可行的。 strncat() 可能不是罪魁祸首,但实际上并没有提供它想象中提供的任何保护。并且正确使用 strncat() 已经够难了,最好不要使用它——我支持我的 TL;DR 总结。
  • 您的默认编译选项会触发所有这些警告。单独使用 GCC 编译就足以满足这个 sn-p 的要求,如果您输入更多标志,最终会出现更多警告。诸如文件末尾没有换行符之类的东西,这并不重要
  • 我的经验表明,使用这些选项进行编译可以确保代码比不使用更接近无错误。当然,你可以随心所欲。但是,如果打开警告显示未以其他方式显示的问题(通常是这种情况),那么使用它是明智的。我将继续将它们用于我自己的工作;对于我的一些代码,我使用了更详细的警告。我很少接受会产生任何警告的代码,即使在规则更加宽松的办公室也是如此。不过,我这样做是咬牙切齿的。严格的原型是一个非常痛点,但代码是古老的。
  • 提供编译器警告是有原因的。提高警告级别允许专门的程序员使用编译器的帮助来发现代码中可能存在的问题,即使代码可以编译。有些程序员不考虑在条件表达式中使用符号不同的值,而那些希望避免代码失败的程序员则不这样做。如果您的代码在使用-Wall -Wextra -pedantic 时无法在没有警告的情况下编译,则需要修复您的代码。
【解决方案2】:

解决方案很简单,就像我感觉到的那样,在 C 语言中根本没有邪恶、卑鄙或愤世嫉俗的东西。第一个 strcpy() 不必发生,第二个 extra_info() 被放错了位置,第三个即使我要使用 strcpy() 参数也将被交换。因此出现错误Abort trap 6

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

char line[1001]; // The line supports up to a 1000 characters
char lines[11][1001]; // An array of lines (up to 10 lines where each line is a 1000 characters max)
char info[100]; // Holds extra info provided by user

char * extra_info(
        char string_1[],
        char string_2[],
        char string_3[],
        char string_4[],
        char string_5[]
    );

int main(){

    int 
    i, // Line number
    j; // Length of the line
    char result[100], text[100];
    FILE *file;

    strcpy(text, "String No."); // The default text

    file = fopen("test.txt", "w+"); // Open the file for reading and writing

    for(i = 0; i < 10; i++){ // Loop to create a line.

        if(i != 9){ // If the line is NOT at the 10th string

            sprintf(result, "%s%d, ", text, i); // Format the text and store it in result

        }
        else{

            sprintf(result, "%s%d ", text, i); // Format the text and store it in result            

        }

        strncat(line, info, 100); // Append the extra info at the end of each line        

        strncat(line, result, 15); // Concatenate all strings in one line

    }

    extra_info(
        "st",
        "nd",
        "rd",
        "th",
        "th"
    );    

    strncat(line, "\n\n", 2); // Add a new-line character at the end of each line

    for(j = 0; j < 10; j++){ // Now loop to change the line

        strcpy(lines[i], line); // Copy the line of text into each line of the array

        fputs(lines[i], file); // Put each line into the file        

    }

    fclose(file);  

}

char * extra_info( // Append user defined and predefined info at the end of a line
        char string_1[],
        char string_2[],
        char string_3[],
        char string_4[],
        char string_5[]
    ){
        char text[100]; // A variable to hold the text

        /* Append a default text into each strings 
        and concatenate them into one line */

        sprintf(text, " 1%s", string_1);
        strncat(line, text, 100);

        sprintf(text, ", 2%s", string_2);
        strncat(line, text, 100);

        sprintf(text, ", 3%s", string_3);
        strncat(line, text, 100);

        sprintf(text, ", 4%s", string_4);
        strncat(line, text, 100);

        sprintf(text, ", 5%s.", string_5);
        strncat(line, text, 100);

        return line;

}

【讨论】:

  • for (j = 0; j &lt; 10; j++)循环中,您使用的是索引i,但i在上一个循环结束时设置为10,因此您访问的是lines[10],这不是数组的一部分,这是未定义的行为。如果您在第一个循环中使用了for (int i = 0; i &lt; 10; i++),在第二个循环中使用了for (int j = 0; j &lt; 10; j++),那么编译器会告诉您这个错误。而且,尽管您断言代码中没有任何恶意,但您使用 strncat() 的事实会使您容易出错,因为您使用它的方式非常反常。
猜你喜欢
  • 1970-01-01
  • 2021-07-22
  • 1970-01-01
  • 1970-01-01
  • 2014-04-02
  • 1970-01-01
  • 2020-08-03
  • 2021-12-23
  • 1970-01-01
相关资源
最近更新 更多