Перейти к содержанию

Последнее обновление:2026-10-12

Проверка памяти с помощью AddressSanitizer

C и C++ — очень небезопасные и подверженные ошибкам языки программирования. Address Sanitizer — разработанный Google инструмент для обнаружения ошибок доступа к памяти, таких как использование после освобождения (use-after-free) и утечки памяти. Он встроен в GCC версии >= 4.8 и применим к коду на C и C++. Address Sanitizer использует инструментирование на этапе выполнения для отслеживания выделения памяти, а значит, код нужно собрать с AddressSanitizer, чтобы использовать все его возможности.

В этой статье мы рассмотрим инструмент проверки памяти AddressSanitizer на восьми перечисленных выше сценариях.

1. Use after free

Возьмём следующий код. Сначала с помощью ключевого слова new выделяется память под целое число, и её адрес присваивается указателю ptr. Затем память освобождается ключевым словом delete. Однако после освобождения программа всё ещё пытается через указатель ptr обратиться к уже освобождённой памяти и записать в неё значение 10 — это и есть ошибка использования после освобождения.

#include <iostream>

int main() {
  int* ptr = new int;
  delete ptr;
  *ptr = 10;  // Use after free
  return 0;
}
При компиляции добавьте следующую опцию.

-fsanitize=address
Запустите полученный bin-файл — получим следующий результат.

heap use after free

2. heap buffer overflow

Сначала с помощью ключевого слова new в куче выделяется массив из двух целых чисел, и его начальный адрес присваивается указателю arr. Затем программа пытается сохранить значение 10 в третьей позиции массива (индекс 2), что выходит за допустимые границы массива и приводит к ошибке переполнения буфера в куче.

#include <iostream>

int main() {
  int* arr = new int[2];
  arr[2] = 10;  // Heap buffer overflow
  delete[] arr;
  return 0;
}
Так же, как выше:

g++-13 heap_buffer_overflow.cc  -Wall -g -fsanitize=address 
Затем:

./a.out
Получим следующий результат проверки.

heap_buffer_overflow

3. stack buffer overflow

В функции vulnerableFunction определён символьный массив buffer из 8 символов. Затем с помощью std::cin данные со стандартного ввода читаются в buffer без ограничения размера ввода. Если количество введённых символов превышает размер buffer, происходит ошибка переполнения буфера в стеке.

void vulnerableFunction() {
  char buffer[8];
  std::cin >> buffer;  // Stack buffer overflow
  std::cout << "You entered: " << buffer << std::endl;
}
Использование такое же, как выше.

stack_buffer_overflow

4. global buffer overflow

Глобальный буфер (массив buffer) определён в глобальной области видимости программы без достаточного ограничения размера ввода. Если количество введённых символов превышает размер буфера, происходит ошибка переполнения глобального буфера. Как и переполнение буфера в стеке, переполнение глобального буфера может привести к нестабильности и проблемам безопасности программы.

#include <iostream>

char buffer[8];

int main() {
    std::cin >> buffer; // 全局缓冲区溢出错误
    std::cout << "You entered: " << buffer << std::endl;
    return 0;
}
Использование такое же, как выше; получим:

global_buffer_overflow

5. use after return

В функции f возвращается адрес локальной переменной i, а в функции main предпринимается попытка обратиться через возвращённый указатель к уже уничтоженной локальной переменной. В такой ситуации можно проверять bin-файл, установив ASAN_OPTIONS=detect_stack_use_after_return=1.

g++-13 stack_use_after_return.cc -Wall -g -fsanitize=address
ASAN_OPTIONS=detect_stack_use_after_return=1 ./a.out
Код:

#include <iostream>

int *f() {
  int i = 42;
  int *p = &i;
  return p;
}

int main() { return *f(); }
И мы получим:

stack_use_after_return

6. use after scope

Адрес локальной переменной x присваивается указателю p, однако после выхода из внутренней области видимости предпринимается попытка обратиться через указатель p к уже вышедшей из области действия локальной переменной x, что приводит к ошибке использования вне области видимости. Это также неопределённое поведение.

#include <iostream>
int main() {
  int* p = 0;
  {
    int x = 0;
    p = &x;
  }
  *p = 5;
  return 0;
}
Компиляция и запуск:

g++-13 stack_use_after_scope.cc -Wall -g -fsanitize=address
ASAN_OPTIONS=detect_stack_use_after_scope=0  ./a.out 
На практике clang++ справляется, а g++ обнаружить не может.

stack_use_after_scope

7. Initialization order bugs

В двух разных файлах исходного кода присутствует инициализация глобальной переменной a, но порядок инициализации не определён. В файле init_order_b.cc предпринимается попытка использовать значение переменной a, однако из-за несогласованного порядка инициализации возможно неопределённое поведение.

Код ниже:

// init_order_a.cc
int duplicate(int n) { return n * 2; }
auto a = duplicate(2);

// init_order_b.cc
#include <iostream>

extern int a;
auto b = a;  

int main() {
  std::cout << b << std::endl;
  return 0;
}
Когда мы пишем такой код, результат не определён: может быть 4, а может 0 — в зависимости от порядка инициализации.

Например:

clang++ init_order_a.cc init_order_b.cc  -Wall -g -fsanitize=address
Так вывод будет 4, а если сначала скомпилировать b, а потом a — получим 0.

clang++ init_order_b.cc init_order_a.cc  -Wall -g -fsanitize=address
При возникновении такой проблемы мы можем использовать AddressSanitizer для проверки порядка инициализации.

ASAN_OPTIONS=check_initialization_order=true ./a.out 
init_order

8. memory leak

С помощью ключевого слова new динамически выделяется память под целое число, но перед завершением программы она не освобождается ключевым словом delete, что приводит к утечке памяти.

#include <iostream>

int main() {
  int* ptr = new int;
  // Missing delete ptr; // Memory leak
  return 0;
}
Установите ASAN_OPTIONS=detect_leaks=1.

ASAN_OPTIONS=detect_leaks=1 ./a.out
memory_leak

Похожие темы

Комментарии