AddressSanitizer для C++: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

AddressSanitizer для C++: полное руководство по обнаружению ошибок памяти

1. Что это и зачем нужна

AddressSanitizer (ASan) — это не библиотека в классическом понимании, а мощный инструмент динамического анализа кода, встроенный в компиляторы Clang и GCC. Он предназначен для обнаружения ошибок работы с памятью на этапе выполнения программы. Разработанный командой Google, ASan стал стандартом де-факто для поиска таких опасных дефектов, как переполнение буфера (buffer overflow), использование памяти после освобождения (use-after-free), двойное освобождение (double-free) и утечки памяти (memory leaks).

Зачем это нужно? Ошибки памяти — одни из самых коварных в C++. Они могут не проявляться годами, вызывая случайные краши, повреждение данных или уязвимости безопасности. Традиционные отладчики часто бессильны, так как программа может падать далеко от места реальной ошибки. ASan решает эту проблему, инструментируя код на этапе компиляции: он добавляет проверки вокруг каждой операции с памятью (malloc, new, free, delete, чтение/запись по указателям). При обнаружении аномалии ASan немедленно останавливает программу и выводит подробный стек-трейс с указанием точной строки кода, типа ошибки и состояния памяти.

Для образовательного сайта PythonLib важно понимать: хотя Python управляет памятью автоматически, знание ASan полезно C++ разработчикам, пишущим высоконагруженные или критичные к безопасности приложения. ASan интегрируется в CI/CD пайплайны, позволяя ловить баги до попадания в продакшн. Он работает на Linux, macOS, Windows и FreeBSD, поддерживает C, C++ и Objective-C.

2. Установка

AddressSanitizer не устанавливается отдельно — он является частью компилятора. Вам нужен Clang (версия 3.1+) или GCC (версия 4.8+).

Установка компилятора (пример для Ubuntu/Debian):

sudo apt update
sudo apt install clang-14  # или g++-11

Для macOS: Xcode Command Line Tools включают Clang с ASan.

xcode-select --install

Для Windows: Используйте Visual Studio 2019+ (включите флаг /fsanitize=address) или установите Clang через LLVM.

Проверка поддержки:

clang++ -fsanitize=address --version
g++ -fsanitize=address --version

3. Быстрый старт — минимальный рабочий пример

Создайте файл example.cpp с намеренной ошибкой (использование после освобождения):

#include <iostream>

int main() {
    int* p = new int(42);
    delete p;
    std::cout << *p << std::endl;  // Ошибка: use-after-free
    return 0;
}

Скомпилируйте и запустите с ASan:

clang++ -fsanitize=address -g -o example example.cpp
./example

Вы увидите подробный отчет с указанием строки, типа ошибки и стека вызовов. Без ASan программа могла бы вывести 42 или упасть с segmentation fault.

4. Основные методы/функции/классы

AddressSanitizer не предоставляет API для вызова из кода — он работает автоматически. Однако существуют вспомогательные макросы и функции для тонкой настройки и интеграции. Рассмотрим 8 ключевых элементов.

4.1. __asan_address_is_poisoned()

Сигнатура: int __asan_address_is_poisoned(void const *addr);

Что делает: Проверяет, помечена ли область памяти как "отравленная" (недоступная). Возвращает 1, если адрес находится в отравленной зоне, иначе 0.

Пример:

#include <sanitizer/asan_interface.h>
#include <iostream>

int main() {
    int* arr = new int[10];
    arr[5] = 100;
    // Проверяем, отравлен ли адрес arr+10 (за пределами массива)
    if (__asan_address_is_poisoned(arr + 10)) {
        std::cout << "Адрес отравлен!" << std::endl;
    }
    delete[] arr;
    return 0;
}

4.2. __asan_poison_memory_region()

Сигнатура: void __asan_poison_memory_region(void const *addr, size_t size);

Что делает: Помечает заданную область памяти как "отравленную". Любое чтение/запись в эту область вызовет ошибку ASan. Полезно для ручного тестирования границ.

Пример:

#include <sanitizer/asan_interface.h>
#include <iostream>

int main() {
    char buffer[100];
    // Помечаем последние 10 байт как недоступные
    __asan_poison_memory_region(buffer + 90, 10);
    buffer[95] = 'X';  // ASan обнаружит ошибку
    return 0;
}

4.3. __asan_unpoison_memory_region()

Сигнатура: void __asan_unpoison_memory_region(void const *addr, size_t size);

Что делает: Снимает "отравление" с области памяти, делая её снова доступной для операций.

Пример:

#include <sanitizer/asan_interface.h>

int main() {
    int data[5];
    __asan_poison_memory_region(data + 2, sizeof(int) * 2);
    __asan_unpoison_memory_region(data + 2, sizeof(int)); // Разрешаем доступ к data[2]
    data[2] = 42; // OK
    data[3] = 10; // Ошибка: data[3] всё ещё отравлен
    return 0;
}

4.4. __lsan_do_leak_check()

Сигнатура: int __lsan_do_leak_check();

Что делает: Принудительно запускает проверку утечек памяти (LeakSanitizer — часть ASan). Возвращает количество обнаруженных утечек. Полезно для точечного контроля в середине программы.

Пример:

#include <sanitizer/lsan_interface.h>
#include <iostream>

int main() {
    int* leak = new int(100); // Утечка: не удаляем
    int leaks = __lsan_do_leak_check();
    std::cout << "Обнаружено утечек: " << leaks << std::endl;
    return 0;
}

4.5. __sanitizer_set_report_path()

Сигнатура: void __sanitizer_set_report_path(const char *path);

Что делает: Устанавливает путь для сохранения отчета об ошибках в файл вместо вывода в stderr. Полезно для автоматизированного тестирования.

Пример:

#include <sanitizer/common_interface_defs.h>

int main() {
    __sanitizer_set_report_path("/tmp/asan_report.log");
    int* p = new int(5);
    delete p;
    delete p; // Двойное освобождение — ошибка
    return 0;
}

4.6. __sanitizer_get_coverage_guards()

Сигнатура: uintptr_t *__sanitizer_get_coverage_guards(uintptr_t *size);

Что делает: Возвращает массив указателей на счетчики покрытия кода (используется совместно с флагом -fsanitize-coverage=trace-pc-guard). Позволяет собирать статистику выполнения.

Пример:

#include <sanitizer/coverage_interface.h>
#include <iostream>

int main() {
    uintptr_t size = 0;
    uintptr_t* guards = __sanitizer_get_coverage_guards(&size);
    std::cout << "Количество точек покрытия: " << size << std::endl;
    return 0;
}

4.7. __asan_describe_address()

Сигнатура: void __asan_describe_address(void *addr);

Что делает: Выводит подробную информацию об адресе: принадлежность к куче/стеку/глобальной области, размер блока, статус (отравлен/свободен). Полезна для отладки.

Пример:

#include <sanitizer/asan_interface.h>

int main() {
    int* arr = new int[20];
    __asan_describe_address(arr + 5); // Информация об адресе
    delete[] arr;
    return 0;
}

4.8. ASAN_OPTIONS (переменная окружения)

Сигнатура: Не функция, а переменная окружения для конфигурации ASan.

Что делает: Позволяет управлять поведением ASan без перекомпиляции. Ключевые опции: detect_leaks=1 (включить поиск утечек), verbosity=1 (подробный вывод), malloc_context_size=30 (глубина стека).

Пример:

export ASAN_OPTIONS="detect_leaks=1:verbosity=1:malloc_context_size=20"
./my_program

5. Реальный практический пример (100-200 строк кода)

Разработаем простой менеджер строк с намеренными ошибками и продемонстрируем, как ASan их находит.

#include <iostream>
#include <cstring>
#include <sanitizer/asan_interface.h>

class StringManager {
private:
    char* buffer;
    size_t capacity;

public:
    StringManager(size_t cap) : capacity(cap) {
        buffer = new char[capacity];
        memset(buffer, 0, capacity);
        std::cout << "[+] Выделено " << capacity << " байт" << std::endl;
    }

    // Копирование строки с переполнением буфера (ошибка!)
    void copyString(const char* src) {
        size_t len = strlen(src);
        if (len > capacity) {
            std::cerr << "Ошибка: строка слишком длинная!" << std::endl;
            return;
        }
        // Намеренная ошибка: не проверяем границы
        for (size_t i = 0; i <= len; ++i) { // <= вместо <
            buffer[i] = src[i];
        }
    }

    // Освобождение с последующим использованием (use-after-free)
    void destroy() {
        delete[] buffer;
        std::cout << "[-] Память освобождена" << std::endl;
    }

    void print() {
        std::cout << "Содержимое: " << buffer << std::endl;
    }

    // Двойное освобождение
    void doubleFree() {
        delete[] buffer;
        std::cout << "[-] Двойное освобождение выполнено" << std::endl;
    }

    ~StringManager() {
        // Деструктор не проверяет, освобождена ли память
    }
};

int main() {
    // Тест 1: Переполнение буфера
    std::cout << "=== Тест 1: Buffer overflow ===" << std::endl;
    StringManager sm1(10);
    sm1.copyString("Hello, World!"); // Строка длиннее 10 символов
    sm1.print();
    sm1.destroy();

    // Тест 2: Use-after-free
    std::cout << "\n=== Тест 2: Use-after-free ===" << std::endl;
    StringManager* sm2 = new StringManager(20);
    sm2->copyString("Test");
    sm2->destroy();
    sm2->print(); // Использование после освобождения

    // Тест 3: Double-free
    std::cout << "\n=== Тест 3: Double-free ===" << std::endl;
    StringManager sm3(5);
    sm3.copyString("AB");
    sm3.doubleFree(); // Двойное освобождение

    // Тест 4: Утечка памяти
    std::cout << "\n=== Тест 4: Memory leak ===" << std::endl;
    new StringManager(100); // Никогда не удаляется

    // Принудительная проверка утечек
    __lsan_do_leak_check();

    return 0;
}

Компиляция и запуск:

clang++ -fsanitize=address -fsanitize=leak -g -o test_string_manager test.cpp
./test_string_manager

ASan обнаружит все четыре ошибки: переполнение буфера (HEAP BUFFER OVERFLOW), use-after-free (HEAP USE AFTER FREE), double-free (DOUBLE FREE) и утечку памяти (LEAK). Каждый отчет будет содержать стек-трейс, размер выделенной памяти и точное место ошибки.

6. Советы по производительности и типичные ошибки

Советы по производительности:

  • Используйте флаги опти

Рекомендаций