Определение правильного предопределенного размера массива в C? - PullRequest
2 голосов
/ 01 апреля 2019

В следующем коде у меня установлен размер массива 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)

Ответы [ 3 ]

4 голосов
/ 01 апреля 2019

Проблема в том, что в структуре есть заполнение для , чтобы int a всегда выравнивалось на 4 в памяти, даже в массиве struct record с.Теперь 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 является ярким примером.

Решение на самом деле , а не , написать структуру, используяfwrite.Вместо этого пишите членам индивидуально - это также улучшает переносимость.Также нет большой разницы в производительности, так как fwrite буферизует записи и так же fread.


PS дорога в ад вымощена упакованным structs, вы хотите избежать их, как чумы в общем коде.


PPS ferror(file) почти наверняка никогда не будет верным сразу после fopen - и при обычных сбоях fopen вернет NULLи ferror(NULL), вероятно, приведет к падению.

3 голосов
/ 01 апреля 2019

[править]

Мой ответ касается слабости кода 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) полезен для проверки результата ввода-вывода, а не открытия файла.

0 голосов
/ 01 апреля 2019

Я изначально неверно истолковал вывод 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), вы пишете "hello world"msgstr "над буфером rec.text.Но вы должны обратить внимание на завершение строки: 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".

Nitpicks

Наконец, уведомления об ошибках должны идти на stderr, тогда как stdout для результатов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...