Содержание статьи
К проблемам безопасности относятся:
- обращение к неверному указателю (в том числе переполнение буфера и разыменование
NULL
); - ошибочное обращение к уже освобожденной памяти (use after free);
- ошибочная попытка освободить ранее освобожденный указатель (double free).
info
Рекомендую также ознакомиться с моей предыдущей статьей, где я рассматривал механизмы сборки мусора и их особенности.
Как мы видели на примере с использованием Boehm GC в C, сборка мусора сама по себе решает только проблему с утечками памяти. Безопасность памяти обеспечивают уже свойства самого языка.
Ассоциация между сборкой мусора и безопасностью памяти возникает от того, что многие популярные прикладные языки не разрешают ручное управление памятью и адресную арифметику вовсе, — в этих условиях у пользователя просто нет возможности выполнить небезопасную операцию. Однако и возможности освободить память тоже нет, поэтому нужен какой‑то механизм автоматического управления, и сборка мусора — самый популярный.
Самый популярный не значит единственный и универсальный. Первая и главная проблема языков с принудительной сборкой мусора — на них можно писать только программы, которые выполняются в пространстве пользователя. Ядру операционной системы или прошивке микроконтроллера не на кого положиться, они вынуждены управлять памятью самостоятельно, а значит, и язык должен поддерживать указатели и адресную арифметику.
Вторая проблема — потеря производительности и предсказуемости времени выполнения. Классические однопоточные сборщики мусора создают паузы в выполнении программы, которые могут быть заметны пользователю. При использовании многопоточных алгоритмов и верной настройке таймеров под задачи конкретного приложения можно свести паузы к минимуму, но свести затраты ресурсов на сборку мусора к нулю невозможно.
Вполне логично, что разработчики языков ищут альтернативные и промежуточные варианты. Давай посмотрим, какими способами разные языки пытаются обеспечить безопасность памяти.
Строгая типизация указателей
Проблемы с безопасностью памяти в C возникают в первую очередь из‑за отсутствия строгой типизации. Функция malloc(
возвращает нетипизированный указатель (void*
), который пользователь может привести к любому типу. Следить за совпадением размера блока памяти с размером данных тоже обязанность пользователя. К примеру, портирование старого кода на 64-битные платформы может принести много веселых минут, если его авторы жестко прописали размер указателя 32 бит.
Ну и самая классическая ошибка, конечно, — случайное обращение к нулевому указателю.
#include <stdio.h>void main(void) { char* str = NULL; printf("%sn", str);}
$
$ ./
Segmentation fault (core dumped)
Более современные языки для системного программирования относятся к этому вопросу более ответственно.
Например, в аде нетипизированные указатели — особый и редкий случай. Обычные указатели всегда типизированные. Вместо malloc(
применяется оператор new
с явным указанием типа. Простого способа освободить память «вообще» там тоже нет, вместо этого есть обобщенная функция (дженерик) Ada.
, которую перед использованием нужно специализировать под конкретный тип данных.
Таким способом запросить или освободить неверный объем памяти гораздо сложнее. Использование после освобождения, впрочем, также будет обнаружено только во время выполнения программы.
info
Указатели в аде называются access types. Например, access
— указатель на целое число.
Для демонстрации сохраним следующий код в файл access_example.
(имя файла должно совпадать с названием основной процедуры).
with Ada.Unchecked_Deallocation;procedure Access_Example is type Int_Ptr is access Integer; -- Специализация дженерика под Int_Ptr procedure Free_Integer is new Ada.Unchecked_Deallocation (Object => Integer, Name => Int_Ptr); P : Int_Ptr; I : Integer;begin -- Запрашиваем память под целое число с помощью new -- и сохраняем туда значение 42 P := new Integer'(42); -- Освобождаем память, теперь P = null Free_Integer(P); -- Пробуем получить значение по указателю I := P.all;end Access_Example;
Теперь скомпилируем с помощью GNAT и запустим.
$
gcc -c -I./ -I- ./access_example.adb
gnatbind -x access_example.ali
gnatlink access_example.ali
$ ./
raised CONSTRAINT_ERROR : access_example.adb:17 access check failed
Как видим, тип указателя access
не защитил нас от обращения к освобожденной памяти. Одно хорошо: хотя бы исключение, а не segmentation fault, как в C, так что наша проблема просто баг, а не потенциальная уязвимость.
Однако начиная с Ada 2005 поддерживается и проверка, что указатель ненулевой. Для этого нужно исправить type
на type
. В этом случае наша программа перестанет компилироваться.
$
gcc -c -I./ -I- ./access_example.adb
access_example.adb:8:35: non null exclusion of actual and formal "Name" do not match
access_example.adb:10:04: warning: (Ada 2005) null-excluding objects must be initialized
access_example.adb:10:04: warning: "Constraint_Error" will be raised at run time
access_example.adb:15:04: warning: freeing "not null" object will raise Constraint_Error
gnatmake: "./access_example.adb" compilation error
На практике от такого типа мало пользы, поскольку его память невозможно освободить. По этой причине опцию not
обычно применяют для подтипов, чтобы предотвратить использование null
в качестве аргумента функции, а для самих значений применяют обычный указатель.
type Int_Ptr is access Integer;subtype Initialized_Int_Ptr is not null Int_Ptr;procedure Some_Proc(Arg: Initialized_Int_Ptr);
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»