【问题标题】:Determining the proper predefined array size in C?在 C 中确定正确的预定义数组大小?
【发布时间】:2019-03-31 22:02:50
【问题描述】:

在下面的代码中,我将数组大小设置为 20。在 Valgrind 中,代码测试干净。但是,一旦我将大小更改为 30,就会出现错误(如下所示)。让我困惑的部分是我可以将值更改为 40 并且错误消失了。改成50,又报错了。然后60个测试干净等等。一直这样下去。所以我希望有人能够向我解释这一点。因为尽管我尽了最大的努力来解决它,但它对我来说并不是很清楚。这些错误很难查明,因为代码看起来都是有效的。

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

struct record {
    int number;
    char text[30];
};

int main(int argc, char *argv[])
{
    FILE *file = fopen("testfile.bin", "w+");
    if (ferror(file)) {
        printf("%d: Failed to open file.", ferror(file));
    }

    struct record rec = { 69, "Some testing" };

    fwrite(&rec, sizeof(struct record), 1, file);
    if (ferror(file)) {
        fprintf(stdout,"Error writing file.");
    }

    fflush(file);
    fclose(file);
}

Valgrind 错误:

valgrind --leak-check=full --show-leak-kinds=all\
                --track-origins=yes ./fileio
==6675== Memcheck, a memory error detector
==6675== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==6675== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==6675== Command: ./fileio
==6675== 
==6675== Syscall param write(buf) points to uninitialised byte(s)
==6675==    at 0x496A818: write (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FA85C: _IO_file_write@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48F9BBE: new_do_write (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FB9D8: _IO_do_write@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48F9A67: _IO_file_sync@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48EEDB0: fflush (in /usr/lib/libc-2.28.so)
==6675==    by 0x109288: main (fileio.c:24)
==6675==  Address 0x4a452d2 is 34 bytes inside a block of size 4,096 alloc'd
==6675==    at 0x483777F: malloc (vg_replace_malloc.c:299)
==6675==    by 0x48EE790: _IO_file_doallocate (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FCBBF: _IO_doallocbuf (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FBE47: _IO_file_overflow@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FAF36: _IO_file_xsputn@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48EFBFB: fwrite (in /usr/lib/libc-2.28.so)
==6675==    by 0x10924C: main (fileio.c:19)
==6675==  Uninitialised value was created by a stack allocation
==6675==    at 0x109199: main (fileio.c:11)
==6675== 
==6675== 
==6675== HEAP SUMMARY:
==6675==     in use at exit: 0 bytes in 0 blocks
==6675==   total heap usage: 2 allocs, 2 frees, 4,648 bytes allocated
==6675== 
==6675== All heap blocks were freed -- no leaks are possible
==6675== 
==6675== For counts of detected and suppressed errors, rerun with: -v
==6675== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

【问题讨论】:

    标签: c


    【解决方案1】:

    问题在于结构中有填充以使int a在内存中始终对齐4,即使在struct records 的数组中也是如此。现在,20+4 可以被 4 整除,40+4 和 60+4 也是如此。但 30+4 和 50+4 不是。因此需要添加 2 个填充字节以使 sizeof (struct record) 可被 4 整除。

    当您运行数组大小为 34 的代码时,sizeof (struct record) == 36,并且字节 35 和 36 包含不确定的值 - 即使 struct record 已完全初始化。更糟糕的是,写入不确定值的代码可能会泄露敏感信息——Heartbleed bug 就是一个很好的例子。

    解决方案实际上是使用fwrite编写结构。而是单独编写成员 - 这也提高了可移植性。也没有太大的性能差异,fwrite 缓冲 写入,fread 也是如此。


    附:通往地狱的道路上铺满了structs,你想像泛型代码中的瘟疫一样避免它们。


    附言ferror(file) 几乎肯定不会在 fopen 之后为真 - 在正常失败中 fopen 将返回 NULLferror(NULL) 可能会导致崩溃。

    【讨论】:

    • 我认为你是正确的,因为 4 的整除率随 OP 错误而变化,但无论 rec 的大小如何,fwrite(&amp;rec, sizeof(struct record), 1, file); 本身都很好。我怀疑if (ferror(NULL)) 的 UB 以各种方式简单地表现出来 - 或者 OP 没有向我们展示一些重要的东西。
    • @chux 我很确定 ferror(NULL) 会导致 SIGSEGV - 并且 OP 从来没有碰到过这个。
    • 诚然,SIGSEGV,一个非 C 标准事件/信号,对 UB 的期望是合理的。
    • 我同意 Valgrind 的抱怨可能来自于结尾的填充。 “write(buf) 指向未初始化的字节”
    【解决方案2】:

    [编辑]

    我的回答与 OP 代码中的一个弱点有关,但 Valgrind write(buf) points to uninitialized byte(s) 是由于其他人回答的其他原因。


    当打开失败时,ferror(file)未定义的行为(UB)。

    if (ferror(file)) 不是确定开放成功的正确测试。

    FILE *file = fopen("testfile.bin", "w+");
    // if (ferror(file)) {
    //    printf("%d: Failed to open file.", ferror(file));
    // }
    if (file == NULL) {
        printf("Failed to open file.");
        return -1;  // exit code, do not continue
    }
    

    我没有看到其他明显的错误。


    ferror(file) 用于测试 I/O 的结果,而不是打开文件。

    【讨论】:

    • @Dacav 也许。然而if (ferror(NULL))未定义的行为。如果没有很好地检查file,其余代码都是有问题的。
    • 谢谢,我以后会记住这一点,一定要更仔细地阅读有关返回值/等的信息。
    • 原来 OP 是一个巨大的巨魔,呵呵 @chux :D
    • 我已更改我的代码以包含此修复程序。我已经意识到这是一个很好的建议,只是注意到这不是我试图笨拙地绕开我的头的问题的答案或解决方案。
    【解决方案3】:

    我最初误解了 valgrind 的输出,所以 @chux 值得接受。不过,我会尽力整理出最好的答案。

    检查错误

    第一个错误(我没有立即考虑的那个)是检查fopen(3)ferror(3) 返回的值。 fopen(3) 调用在错误时返回 NULL(并设置 errno),因此检查 NULLferror(3) 是错误的。

    序列化文件结构。

    通过初始化,您可以编写结构的所有字段,但不会初始化它所覆盖的所有内存。例如,您的编译器可能会在结构中保留一些填充,以便在访问数据时获得更好的性能。当您在文件中写入整个结构时,实际上是在将未初始化的数据传递给 fwrite(3) 函数。

    通过改变数组的大小可以改变 Valgrind 的行为。这可能是因为编译器改变了结构在内存中的布局,并且使用了不同的填充。

    尝试用memset(&amp;rec, 0, sizeof(rec)); 擦除rec 变量,Valgrind 应该停止抱怨。不过,这只会解决症状:由于您正在序列化二进制数据,因此您应该将struct record 标记为__attribute__((packed))

    初始化内存

    你原来的初始化是好的。

    另一种初始化数据的方法是使用strncpy(3)。 Strncpy 将接受指向要写入的目标的指针、指向源内存块(应从中获取数据)的指针和可用的写入大小作为参数。

    通过使用strncpy(&amp;rec.text, "hello world", sizeof(rec.text),您可以在rec.text 缓冲区上写入“hello world”。但是要注意字符串的终止:strncpy 不会写超出给定的大小,如果源字符串比那个长,就不会有任何字符串终止符。

    strncpy可以安全使用如下

    strncpy(&rec.text, "hello world", sizeof(rec.text) - 1);
    rec.text[sizeof(rec.text) - 1] = '\0';
    

    第一行将“hello world”复制到目标字符串。 sizeof(rec.text) - 1 作为大小传递,因此我们为 \0 终止符留出了空间,它被明确写为最后一个字符,以涵盖 sizeof(rec.text) 比“hello world”短的情况。

    挑剔

    最后,错误通知应该发送到stderr,而stdout 用于结果。

    【讨论】:

    • 但请注意,我可以将 30 更改为 40,突然它不再发送初始化字节了。 50 码可以,60 码不可以。
    • struct record rec = { 69, "Some testing" }; 在我看来像是已初始化。
    • 不要专注于此。未初始化内存时会发生未定义的行为。
    • "通过初始化,您不会写入文本字段的所有字节。"和“您实际上是在处理未初始化的数据。”不正确。初始化是全部或全部,不是 C 中的部分。struct record rec = { 69, "Some testing" }; 初始化所有 rec。 OP 的错误是每隔一个构建,fileNULL
    • @chux,是的,我应该改写一下。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-06
    • 1970-01-01
    • 2020-04-14
    • 1970-01-01
    • 2013-06-16
    • 1970-01-01
    相关资源
    最近更新 更多