Как устранить ошибки Valgrind для неинициализированных значений и неверных операций чтения в программе сканирования памяти Linux

В настоящее время я разрабатываю программу сканирования памяти для лабораторного задания, цель которой — прочитать и проанализировать карту памяти самого процесса, в частности, путем чтения из /proc/self/maps. Программа предназначена для анализа этого файла, определения читаемых областей памяти, а затем подсчета вхождений символа «А» в эти области. Эта функциональность реализована и работает корректно при обычном выполнении.

Однако когда я запускаю свою программу под Valgrind с параметрами --leak-check=full --track-origins=yes --error-exitcode=1 --show-leak-kinds=all --read-var-info=yes --malloc-fill=0xAA --free-fill=0xFF, она сообщает о множественных ошибках, связанных с неинициализированными значениями и недопустимыми чтениями памяти. В частности, Valgrind выводит предупреждения о том, что «Условный переход или перемещение зависит от неинициализированных значений» и «Недопустимое чтение размера 1», указывая на то, что моя программа может пытаться прочитать память за пределами выделенных областей или использовать неинициализированную память для принятия решений.

Интересно, что Valgrind также сообщает об отсутствии утечек памяти, что говорит о том, что проблема не в том, что память не освобождается, а в том, как осуществляется доступ к памяти и ее использование во время выполнения программы. Это несоответствие между нормальной работой программы без видимых проблем и сообщением Valgrind о серьезных ошибках вызывает недоумение.

Соответствующая часть вывода Valgrind включает предупреждения о необработанных кодах DW_OP_, которые могут указывать на более глубокую проблему, связанную с отладочной информацией или способом интерпретации адресов памяти. Кроме того, сообщение об ошибке «Адрес 0x4037000 составляет 0 байт после ограничения сегмента данных brk 0x4037000» предполагает попытку чтения памяти прямо на границе разрешенного значения, что соответствует недопустимым ошибкам чтения.

Учитывая эти подробности, я ищу рекомендации о том, как диагностировать и устранять ошибки, о которых сообщает Valgrind, в моей программе сканирования памяти. Мне особенно интересно понять, почему возникают эти неинициализированные значения и недопустимые чтения, и как изменить мою программу, чтобы устранить эти проблемы, сохранив при этом ее функциональность.

Вот мой код


/// @file    main.c

#include "parser.h"
#include "scanner.h"
#include <stdio.h>
#include <stdlib.h>

int main() {
    MemoryRegion *regions = NULL;
    int count = 0;
    if (parse_maps(&regions, &count) != 0) {
        fprintf(stderr, "Error parsing /proc/self/maps\n");
        return 1;
    }
    scan_memory(regions, count);
    free(regions); // Clean up
    return 0;
}


/// @file parser.c

#include "parser.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int parse_maps(MemoryRegion **regions, int *count) {
    assert(regions != NULL);
    FILE *file = fopen("/proc/self/maps", "r");
    if (!file) {
        perror("Failed to open /proc/self/maps");
        return -1;
    }

    char line[496]; // buffer for reading lines
    *count = 0;
    int capacity = 12; // initial capacity
    *regions = calloc(capacity, sizeof(MemoryRegion));
    if (!*regions) {
        perror("Failed to allocate memory for regions");
        fclose(file);
        return -1;
    }
    // loop through each line in the file
    while (fgets(line, sizeof(line), file)) {
        MemoryRegion region; // temporary region to store the current line
        char *ptr = line;
        char *endptr;

        region.start_addr = strtoul(ptr, &endptr, 16); // parse the start address
        if (ptr == endptr) {
            continue;
        }
        ptr = endptr + 1; // skip the '-'

        region.end_addr = strtoul(ptr, &endptr, 16); // parse the end address
        if (ptr == endptr) {
            continue;
        }
        ptr = endptr + 1; // skip the space

        if (sscanf(ptr, "%4s", region.permissions) != 1) {
            continue;
        }

        if (strstr(line, "[vvar]")  || region.permissions[0] != 'r') {
            continue;
        }
        // check if needed to reallocate memory
        if (*count == capacity) {
            capacity *= 2;
            *regions = realloc(*regions, capacity * sizeof(MemoryRegion));
            if (!*regions) {
                perror("Failed to reallocate memory for regions");
                fclose(file);
                return -1;
            }
        }
        (*regions)[*count] = region; // store the region
        (*count)++;
    }
        fclose(file);
        return 0;
}

/// @file parser.h

#ifndef MEMSCAN_PARSER_H
#define MEMSCAN_PARSER_H
#include <stdio.h>

/// @brief MemoryRegion - struct to store memory region information
typedef struct {
    unsigned long start_addr;
    unsigned long end_addr;
    char permissions[5];
} MemoryRegion;

/// @brief parse_maps - parses /proc/self/maps and stores the memory regions
/// @param regions - pointer to an array of MemoryRegion structs
/// @param count - pointer to an integer to store the number of memory regions
/// @return - 0 on success, -1 on failure
int parse_maps(MemoryRegion **regions, int *count);








/// @file scanner.c

#include "scanner.h"
#include "parser.h"
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>


int count_a_in_region(unsigned long start, unsigned long end) {
    int count = 0;
    // Ensure we do not attempt to read the end address
    for (char *ptr = (char *)(uintptr_t)start;
         ptr < (char *)(uintptr_t)end - 1; ptr++) {
        if (*ptr == 'A') {
            count++;
        }
    }
    return count;
}

void scan_memory(MemoryRegion *regions, int count) {
    assert(regions != NULL);
    for (int i = 0; i < count; i++) {
        MemoryRegion region = regions[i];
        assert(region.end_addr > region.start_addr); // ensure end is greater than start
        size_t size = region.end_addr - region.start_addr;
        int countA = 0;
        if (region.permissions[0] == 'r') {
            countA = count_a_in_region(region.start_addr, region.end_addr);
            if (countA == -1) {
                fprintf(stderr, "Error counting 'A' in region %d\n", i);
                continue;
            }
        }
        printf("%d: 0x%lx - 0x%lx %s Number of bytes read [%zu] Number of 'A' is [%d]\n", i, region.start_addr, region.end_addr, region.permissions, size, countA);
    }
}



/// @file scanner.h

#ifndef SCANNER_H
#define SCANNER_H
#include "parser.h"

/// @brief count_a_in_region - counts the number of 'A' characters in the specified memory region
/// @param start - the start address of the memory region
/// @param end - the end address of the memory region
/// @return - the number of 'A' characters in the region, or -1 on error
int count_a_in_region(unsigned long start, unsigned long end);

/// @brief scan_memory - scans the memory regions for the specified pattern
/// @param regions - pointer to an array of MemoryRegion structs
/// @param count - the number of memory regions
void scan_memory(MemoryRegion *regions, int count);

#endif //SCANNER_H

Я попробовал -1 в функции подсчета, когда я избавляюсь от подсчета букв, никаких ошибок valgrind

Просто дикая догадка, потому что я точно не знаю, как работает valgrind. Но вы читаете память на основе адресов отображений виртуального адресного пространства. Это не обязательно означает, что за этими адресами действительно выделена память. Я предполагаю, что valgrind жалуется на чтение памяти с адресов, которые никогда не были выделены.

UniversE 18.02.2024 14:13

@UniversE, мне неясно, что именно вы подразумеваете под «выделенным», но отображение (виртуальной) памяти в адресное пространство процесса означает, что система обещает, что процесс может получить доступ к этой памяти. Система может лгать (например, Linux известен чрезмерным использованием памяти), но если система не может выполнить свои обещания на практике, то можно ожидать, что попытки доступа к этой памяти на самом деле потерпят неудачу, а не просто вызовут предупреждение от Valgrind.

John Bollinger 18.02.2024 15:47

@JohnBollinger, возможно, моя терминология не на 100% точна. Но под «выделенным» я подразумевал, что память принадлежит живому объекту, «выделенному» вызовом malloc. Valgrind, скорее всего, отслеживает эти выделения и сообщает о «недопустимом» чтении, если вы пытаетесь прочитать из памяти, которая не принадлежит такому объекту. Если я не ошибаюсь, это также - каким-то образом - похоже на то, куда движется ваш ответ.

UniversE 18.02.2024 15:53

Вы правы, @UniversE, то, что вы описываете, соответствует моему ответу. Ваш первоначальный комментарий показался мне запутанным, потому что он очень предварительный. Большая часть памяти, отображенной в адресное пространство данной программы, определенно не соответствует объектам, выделенным через распределитель библиотеки C.

John Bollinger 18.02.2024 15:59
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
129
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Учитывая эти подробности, я ищу рекомендации о том, как диагностировать и устранять ошибки, о которых сообщает Valgrind, в моей программе сканирования памяти.

Легко: не делай этого.

Мне особенно интересно понять, почему возникают эти неинициализированные значения и недопустимые чтения, и как изменить мою программу, чтобы устранить эти проблемы, сохранив при этом ее функциональность.

Вы, вероятно, не можете.

Интересно, что Valgrind также сообщает об отсутствии утечек памяти, что говорит о том, что проблема не в том, что память не освобождается, а в том, как осуществляется доступ к памяти и ее использование во время выполнения программы. Это несоответствие между нормальной работой программы без видимых проблем и сообщением Valgrind о серьезных ошибках вызывает недоумение.

Не совсем. Необходимо понимать, что диагностику Valgrind следует интерпретировать с точки зрения семантики исходного языка(ов) анализируемой программы. Особенно, если исходным языком является C или C++, к которым Valgrind особенно настроен. Если Valgrind отлично справляется со своей задачей при анализе программы на языке C, то

  • каждая попытка разыменовать указатель, который определенно не указывает на живой объект, согласно спецификации языка C, должна генерировать диагностику. Ваша диагностика «неверное чтение» находится в этой категории.

  • каждая попытка прочитать объект, которому не было присвоено положительное значение, согласно спецификации языка C, должна генерировать диагностику. Ваш «Условный прыжок или ход зависит от неинициализированных значений» находится в этой категории.

Ваша программа действительно содержит серьезные ошибки, если судить исключительно по спецификации языка. Он свирепствует с поведением, которое не определено языком C. И это неудивительно, потому что ISO C просто не определяет способ делать то, что вы хотите. Это не означает, что вы не можете выполнить эту работу с помощью программы на языке C. Ваша конкретная реализация C вполне может поддерживать это. Многие так и делают. Это означает, что некоторые вещи, которые должна делать такая программа, относятся к числу тех, которые Valgrind призван обнаруживать и сообщать о них.

Конечно, у Valgrind есть параметры, влияющие на то, о каких проблемах он будет сообщать, но мне трудно представить, что вы ожидаете от Valgrind, чтобы обнаружить, если вы отключите всю диагностику, связанную с преднамеренным поведением вашей программы. Valgrind просто не подходит для анализа программы, которая намеренно делает то же, что и ваша. Возможно, то, что вы надеялись сказать вам Валгриндом, можно определить просто по тому, доставлен ли в программу SIGBUS или SIGSEGV.

Если говорить от имени главного участника Valgrind за последние несколько лет, то это совершенно неверно. Я не понимаю, как вы можете отредактировать это, чтобы сделать его правильным или полезным для ОП.

Paul Floyd 20.02.2024 11:32
Ответ принят как подходящий

По совпадению, я сейчас работаю над кодом в Valgrind, который выполняет синтаксический анализ mmap. Это используется для двух целей: получения сопоставления памяти самого инструмента Valgrind (Valgrind не может перехватить mmap для собственного сопоставления, поскольку оно не запущено), а также для проверки mmap, если вы запускаете с --sanity-level=3 или выше.

предупреждения о необработанных кодах DW_OP_

Вероятно, старая версия Валгринда. Попробуйте взять что-нибудь более свежее.

--leak-check=full --track-origins=yes --error-exitcode=1 --show-leak-kinds=all --read-var-info=yes

Вам не нужны ни один из этих вариантов для того, что вы делаете. Они просто сделают ваш бег медленнее. Я рекомендую вам не использовать их или, по крайней мере, использовать их только после того, как увидите ошибку.

Вы действительно ищете «A» (0x41) или 0xAA? Я не уверен, что вы используете --malloc-fill=0xAA.

Итак, почему вы видите проблемы? Memcheck знает обо всей выделенной памяти (как в вашем коде, так и в коде запуска перед main()). Он также знает о стеке. malloc() не просто выделяет один бит памяти каждый раз, когда вы его вызываете. Скорее, он использует пулы различных размеров степени двойки и распределяет ресурсы из этих пулов. При больших выделениях памяти для получения памяти будет использоваться анонимный mmap — см. справочную страницу malloc. Это означает две вещи.

  1. Когда вы читаете из памяти в куче, которая была выделена с помощью malloc (но не записана), и сравниваете ее с «A», вы получаете Conditional jump or move depends on uninitialised value(s) из «if (*ptr == 'A')»
  2. Когда вы читаете из памяти, которая никогда не была выделена, вы получите Invalid read of size 1.

Попробуйте что-нибудь вроде этого. Это скорее грубая сила.

#include <memcheck.h>

int count_a_in_region(unsigned long start, unsigned long end) {
    int count = 0;
    VALGRIND_DISABLE_ADDR_ERROR_REPORTING_IN_RANGE(start, end - start);
    // Ensure we do not attempt to read the end address
    for (char *ptr = (char *)(uintptr_t)start;
         ptr < (char *)(uintptr_t)end - 1; ptr++) {
        if (*ptr == 'A') {
            count++;
        }
    }
    VALGRIND_ENABLE_ADDR_ERROR_REPORTING_IN_RANGE(start, end - start);
    return count;
}

Если вышеперечисленное не работает или вы хотите иметь больше контроля над происходящим, попробуйте использовать VALGRIND_CHECK_MEM_IS_ADDRESSABLE. Это вернет 0, если диапазон адресный. Если он не полностью адресуемый, он вернет первый неадресуемый адрес. Вероятно, вам придется использовать его несколько раз, чтобы найти все адресные диапазоны в регионе mmap.

Если вам нужно отключить ошибки «Условный переход», попробуйте VALGRIND_MAKE_MEM_DEFINED_IF_ADDRESSABLE или VALGRIND_CHECK_MEM_IS_DEFINED.

Спасибо! избавился от всех ошибок, которые у меня были!

Lyon 20.02.2024 00:13

Я мог бы добавить VALGRIND_CHECK_MEM_IS_UNADDRESSABLE, который может быть полезен для поиска неадресуемых диапазонов.

Paul Floyd 20.02.2024 11:35

Другие вопросы по теме