在以下代码中,我将数组大小设置为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)
答案 0 :(得分:4)
问题是结构中存在填充,以使int a
在内存中始终对齐4,即使在struct record
s数组中也是如此。现在,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
也是如此。
P.S。挤满了struct
的道路铺天盖地,您想要避免它们像通用代码中的瘟疫一样。
P.P.S。 ferror(file)
几乎肯定不会在fopen
之后才为真-在正常故障中,fopen
将返回NULL
,而ferror(NULL)
可能会导致崩溃。>
答案 1 :(得分:3)
[编辑]
我的回答与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结果(而不是打开文件)很有用。
答案 2 :(得分:0)
我最初误解了valgrind输出,因此@chux应该被接受。我会尽力将最好的答案组合在一起。
第一个错误(我没有立即考虑过)是用fopen(3)
检查ferror(3)
返回的值。 fopen(3)
调用会在错误时返回NULL
(并设置errno
),因此用NULL
检查ferror(3)
是错误的。
通过初始化,您可以写出结构的所有字段,但是并没有初始化它覆盖的所有内存。例如,您的编译器可能在结构中保留了一些填充,以便在访问数据时获得更好的性能。当您在文件上写入整个结构时,实际上是在将未初始化的数据传递到fwrite(3)
函数。
通过更改数组的大小,可以更改Valgrind的行为。可能是由于编译器更改了内存中结构的布局,并且使用了不同的填充。
尝试用rec
擦拭memset(&rec, 0, sizeof(rec));
变量,Valgrind应该停止抱怨。不过,这只会解决症状:由于您要序列化二进制数据,因此应将struct record
标记为__attribute__((packed))
。
您的原始初始化很好。
初始化数据的另一种方法是使用strncpy(3)
。 Strncpy将接受指向要写入的目标的指针,指向源内存块(应从中获取数据的指针)和可用写入大小作为参数。
通过使用strncpy(&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
是为了获取结果。