Безопасное управление памятью

Под безопасным управлением памятью подразумеваются средства языка программирования и его системы времени выполнения, которые гарантируют защиту от ошибок при работе с динамической памятью. Безопасное управление памятью значительно повышает надёжность программной системы, исключая самые распространённые ошибки, которые могут возникать из-за обычной опечатки и в то же время требовать длительного поиска, а часто - остающиеся вообще неисправленными. Кроме того, безопасное управление памятью становится абсолютно необходимым при компонентном программировании.

Формальный анализ проблемы управления памятью

Статья является попыткой строгим языком описать суть проблемы управления памятью, требования к безопасному управлению памятью и условия, при которых гарантируется выполнение этих требований.

Автор: И.Е. Ермаков

1 Требования к динамической памяти в компонентных системах

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

Можно выделить два основных требования к динамической памяти в таких системах:

1) Защита от возможного взаимного повреждения, при сохранении единого адресного пространства.
Это означает, что никакие операции с памятью, выполняемые одним компонентом, не должны приводить к повреждению областей памяти, используемых другим компонентом.

2) Корректное и своевременное удаление динамических объектов, разделяемых компонентами.
Это означает, что а) никакой объект не должен удаляться, пока он используется хотя бы одним компонентом; б) если объект не используется никаким компонентом, он должен быть удалён за некоторое конечное время.

Формализуем требования по этим пунктам, формулируя инварианты, которые должны гарантироваться языком и системой времени выполнения, расчитанными на поддержку компонентного программирования.

2 Инварианты безопасного управления памятью

* Инвариант 1 (И1)

 для любого присутствующего в состоянии системы значения P, имеющего указательный тип
    [ (P указывает на недоступную область памяти)
      OR
     (P указывает на существующий объект) &
     & (тип указуемого объекта соответсвует типу P) & 
     & (на всём протяжении своего существования P указывает на один и тот же объект) ]

* Инвариант 2 (И2)

 [ для любого присутствующего в состоянии системы значения P, имеющего указательный тип
          ( (P = NIL)
             OR
            (P указывает на существующий объект)
          )
 ]
   &
 [ существует конечное T > 0 такое, что
       для любого существующего в системе динамического объекта D
            некоторое время назад, меньшее T,
                 в состоянии системы существовало значение P указательного типа, указывающее на D
 ]

3 Средства обеспечения И1

Первоначально наложим ограничение:

1) в системе отсутствует возможность удаления динамических объектов.

Тогда в не-объектно-ориентированном языке И1 может быть обеспечен языковыми средствами на этапе компиляции. Для этого необходимо соответствие языка следующим требованиям:

2) неотъемлемой частью указательного типа является тип указуемого значения;

3) значения указательных типов порождаются либо одновременно с созданием объектов соответствующих типов, либо копированием уже существующих значений того же типа - и никаким другим образом (отсутствует операция взятия адреса произвольных объектов);

4) указательные типы являются герметичными: т.е. к ним применимы операции =, #, разыменование - и никакие другие;

5) операция разыменовывания имеет тип результата, строго соответствующий типу указателя.

6) не инициализированные явно указатели указывают на недоступную область памяти (имеют спец. значение NIL).

Объектно-ориентированный язык (допускающий расширение типов) добавляет полиморфизм, т.е. происходит «раздвоение» типа переменных на статический и динамический (являющийся произвольным расширением статического). Для указательных типов вводится операция проверки динамического типа и приведения базового типа к расширенному.

Чтобы обеспечить соблюдение И1, требуется способ во время выполнения определять фактический тип указуемого значения и проверять допустимость операции преобразования. Таким образом, обязательной частью каждого динамического объекта должен быть уникальный идентификатор типа. К перечисленным выше требованиям добавляется ещё одно:

7) система времени выполнения должна поддерживать теги типов и их проверку при операциях преобразования типов.

Соблюдение совокупности требований 1-7 гарантирует истинность И1 на протяжении всей работы программной системы, независимо от ошибок, допущенных в исходном коде разработчиками её компонентов.

4 Удаление динамических объектов и обеспечение И2

Очевидно, что система, позволяющая создавать, но не позволяющая удалять динамические объекты, имеет весьма ограниченные возможности применения. Включение операции удаления динамических объектов несёт с собой две хорошо известных проблемы: а) «повисшие указатели» - при удалении объекта, на который ещё существуют указатели; б) «утечки памяти» - наличие объектов, на которые уже не осталось указателей и которые уже никогда не будут удалены. Фактически, эти две проблемы являются нарушениями соответствующих частей И2.

Утечки памяти сами по себе являются достаточно серьёзными, но при этом не вызывают нарушения И1. Образование висячих ссылок с высокой вероятностью может вести к нарушению И1. В наилучшем из возможных случаев память по адресу бывшего динамического объекта станет недоступной для чтения, и для «повисших указателей» будет соблюдена первая альтернатива И1 («x указывает на недоступную область памяти»), т.е. при попытке обращения по таким указателям другие динамические объекты повреждены не будут, а произойдёт ошибка времени выполнения, аналогичная обращению по нулевому указателю. Однако диспетчер памяти при удалении динамического объекта может не освобождать страницу памяти, в которой он был размещён. Тогда ошибка времени выполнения генерироваться не будет. И наконец, в худшем случае (возможно, спустя продолжительное время), по этому же адресу будет размещён новый объект, что приведёт к нарушению И1 - т.е. повреждению компонентом, имеющим висячие ссылки, нового объекта.

При начилии в языке операции явного удаления динамического объекта гарантировать средствами языка или системы времени выполнения отсутствие «повисших указателей» и «утечек памяти» невозможно. Единственным путём избежать этих проблем является применение автоматического управления памятью, при котором освобождение объекта выполняет система времени выполнения и только в тот момент, когда объект гарантированно не используется. Автоматическое управление памятью может опираться на различные механизмы (напимер, на сборку мусора или на подсчёт ссылок).

Таким образом, единственным cредством обеспечения И2 является отказ от ручного освобождения памяти и использование тех или иных механизмов автоматического освобождения.

Автор: Ермаков. И.Е.

© 2005-2024 OberonCore и коллектив авторов.