stringtranslate.com

Висячий указатель

Висячий указатель

Висячие указатели и дикие указатели в программировании — это указатели , которые не указывают на допустимый объект соответствующего типа. Это особые случаи нарушения безопасности памяти . В более общем смысле, висячие ссылки и дикие ссылки — это ссылки , которые не разрешаются в допустимое место назначения.

Висячие указатели возникают во время уничтожения объекта , когда объект, имеющий входящую ссылку, удаляется или освобождается без изменения значения указателя, так что указатель по-прежнему указывает на ячейку памяти освобожденной памяти. Система может перераспределить ранее освобожденную память, и если программа затем разыменует (теперь) висячий указатель, может возникнуть непредсказуемое поведение , так как память теперь может содержать совершенно другие данные. Если программа записывает в память, на которую ссылается висячий указатель, может произойти скрытое повреждение несвязанных данных, что приводит к тонким ошибкам , которые может быть чрезвычайно трудно обнаружить. Если память была перераспределена другому процессу, то попытка разыменовать висячий указатель может вызвать ошибки сегментации (UNIX, Linux) или общие ошибки защиты (Windows). Если у программы достаточно привилегий, чтобы позволить ей перезаписать данные учета, используемые распределителем памяти ядра, повреждение может вызвать нестабильность системы. В объектно-ориентированных языках со сборкой мусора зависшие ссылки предотвращаются только путем уничтожения недостижимых объектов, то есть не имеющих входящих указателей; это обеспечивается либо трассировкой, либо подсчетом ссылок . Однако финализатор может создавать новые ссылки на объект, требуя восстановления объекта для предотвращения зависшей ссылки.

Дикие указатели, также называемые неинициализированными указателями, возникают, когда указатель используется до инициализации в некоторое известное состояние, что возможно в некоторых языках программирования. Они показывают такое же непредсказуемое поведение, как и висячие указатели, хотя они с меньшей вероятностью останутся незамеченными, поскольку многие компиляторы выдадут предупреждение во время компиляции, если к объявленным переменным будет произведен доступ до их инициализации. [1]

Причина висящих указателей

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

Ниже показан простой пример:

{ char * dp = NULL ; /* ... */ { char c ; dp = & c ; } /* c выходит из области видимости */ /* dp теперь является висячим указателем */ }               

Если операционная система способна обнаружить ссылки времени выполнения на нулевые указатели , решением вышеизложенного является назначение 0 (null) для dp непосредственно перед выходом из внутреннего блока. Другим решением было бы как-то гарантировать, что dp не будет использоваться снова без дальнейшей инициализации.

Другим частым источником зависших указателей является беспорядочная комбинация malloc()и free()библиотечных вызовов: указатель становится зависшим, когда блок памяти, на который он указывает, освобождается. Как и в предыдущем примере, один из способов избежать этого — убедиться, что указатель сброшен на ноль после освобождения его ссылки, как показано ниже.

#include <stdlib.h> void func () { char * dp = malloc ( A_CONST ); /* ... */ free ( dp ); /* dp теперь становится висячим указателем */ dp = NULL ; /* dp больше не является висячим указателем */ /* ... */ }             

Слишком распространенной ошибкой является возврат адресов локальной переменной, размещенной в стеке: как только вызванная функция возвращает значение, пространство для этих переменных освобождается, и технически они имеют «мусорные значения».

int * func ( void ) { int num = 1234 ; /* ... */ return & num ; }        

Попытки прочитать указатель могут по-прежнему возвращать правильное значение (1234) некоторое время после вызова func, но любые функции, вызванные после этого, могут перезаписать выделенное для стека хранилище numдругими значениями, и указатель больше не будет работать правильно. Если указатель на numдолжен быть возвращен, numон должен иметь область действия за пределами функции — он может быть объявлен как static.

Ручное освобождение без висячих ссылок

Антони Кречмар  [pl] (1945–1996) создал полную систему управления объектами, свободную от феномена висячих ссылок. [2] Похожий подход был предложен Фишером и Лебланом [3] под названием «Замки и ключи» .

Причина диких указателей

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

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

int f ( int i ) { char * dp ; /* dp — это дикий указатель */ static char * scp ; /* scp не является диким указателем:  * статические переменные инициализируются 0  * при запуске и сохраняют свои значения  * с последнего вызова впоследствии.  * Использование этой функции может считаться  * плохим стилем, если не прокомментировано */ }         

Дыры в системе безопасности, связанные с висящими указателями

Как и ошибки переполнения буфера , ошибки висячих/диких указателей часто становятся дырами в безопасности. Например, если указатель используется для вызова виртуальной функции , может быть вызван другой адрес (возможно, указывающий на код эксплойта) из-за перезаписи указателя vtable . С другой стороны, если указатель используется для записи в память, может быть повреждена какая-то другая структура данных. Даже если память считывается только после того, как указатель становится висячим, это может привести к утечкам информации (если интересные данные помещаются в следующую структуру, выделенную там) или к повышению привилегий (если теперь недействительная память используется в проверках безопасности). Когда висячий указатель используется после того, как он был освобожден без выделения ему нового фрагмента памяти, это становится известно как уязвимость «использования после освобождения». [4] Например, CVE - 2014-1776 - это уязвимость использования памяти после освобождения в Microsoft Internet Explorer 6–11 [5], используемая атаками нулевого дня со стороны усовершенствованной постоянной угрозы . [6]

Предотвращение ошибок зависания указателя

В языке C простейший метод — реализовать альтернативную версию функции free()(или аналогичной), которая гарантирует сброс указателя. Однако этот метод не очистит другие переменные указателя, которые могут содержать копию указателя.

#include <assert.h> #include <stdlib.h>  /* Альтернативная версия для 'free()' */ static void safefree ( void ** pp ) { /* в режиме отладки, прерывание, если pp равен NULL */ assert ( pp ); /* free(NULL) работает правильно, поэтому не требуется никаких проверок, кроме assert в режиме отладки */ free ( * pp ); /* освободить блок, обратите внимание, что free(NULL) допустим */ * pp = NULL ; /* сбросить исходный указатель */ }            int f ( int i ) { char * p = NULL , * p2 ; p = malloc ( 1000 ); /* получить фрагмент */ p2 = p ; /* скопировать указатель */ /* использовать фрагмент здесь */ safefree (( void ** ) & p ); /* безопасное освобождение; не влияет на переменную p2 */ safefree (( void ** ) & p ); /* этот второй вызов не завершится ошибкой, так как p сбрасывается в NULL */ char c = * p2 ; /* p2 все еще является висячим указателем, поэтому это неопределенное поведение. */ return i + c ; }                               

Альтернативную версию можно использовать даже для гарантии действительности пустого указателя перед вызовом malloc():

safefree ( & p ); /* Я не уверен, был ли освобожден кусок */ p = malloc ( 1000 ); /* выделить сейчас */    

Эти применения могут быть замаскированы с помощью #defineдиректив для создания полезных макросов (распространенным из которых является #define XFREE(ptr) safefree((void **)&(ptr))), создавая что-то вроде метаязыка, или могут быть встроены в отдельную библиотеку инструментов. В каждом случае программисты, использующие эту технику, должны использовать безопасные версии в каждом случае, где free()они будут использоваться; невыполнение этого требования снова приводит к проблеме. Кроме того, это решение ограничено областью действия одной программы или проекта и должно быть надлежащим образом документировано.

Среди более структурированных решений, популярным методом избегания висячих указателей в C++ является использование интеллектуальных указателей . Интеллектуальный указатель обычно использует подсчет ссылок для освобождения объектов. Некоторые другие методы включают метод надгробий и метод замков и ключей . [3]

Другой подход заключается в использовании сборщика мусора Boehm , консервативного сборщика мусора , который заменяет стандартные функции выделения памяти в C и C++ на сборщик мусора. Этот подход полностью устраняет ошибки висячих указателей, отключая освобождения и возвращая объекты сборщиком мусора.

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

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

Обнаружение висящего указателя

Чтобы выявить ошибки висячих указателей, одним из распространенных методов программирования является установка указателей на нулевой указатель или на недопустимый адрес после освобождения хранилища, на которое они указывают. Когда нулевой указатель разыменовывается (в большинстве языков), программа немедленно завершается — нет никакой возможности повреждения данных или непредсказуемого поведения. Это упрощает поиск и устранение базовой ошибки программирования. Этот метод не помогает, когда есть несколько копий указателя.

Некоторые отладчики автоматически перезаписывают и уничтожают данные, которые были освобождены, обычно по определенному шаблону, например 0xDEADBEEF(например, отладчик Visual C/C++ от Microsoft использует 0xCC, 0xCDили 0xDDв зависимости от того, что было освобождено [7] ). Обычно это предотвращает повторное использование данных, делая их бесполезными и очень заметными (шаблон служит для того, чтобы показать программисту, что память уже освобождена).

Такие инструменты, как Polyspace , TotalView , Valgrind , Mudflap, [8] AddressSanitizer или инструменты на основе LLVM [9], также можно использовать для обнаружения использования висячих указателей.

Другие инструменты (SoftBound, Insure++ и CheckPointer) инструментируют исходный код для сбора и отслеживания допустимых значений указателей («метаданных») и проверяют каждый доступ указателя на соответствие метаданным на предмет действительности.

Другая стратегия, когда подозревается небольшой набор классов, заключается в том, чтобы временно сделать все их функции-члены виртуальными : после того, как экземпляр класса будет уничтожен/освобожден, его указатель на таблицу виртуальных методов будет установлен в NULL, и любой вызов функции-члена приведет к сбою программы, и она покажет виновный код в отладчике.

Смотрите также

Ссылки

  1. ^ «Параметры предупреждения — использование коллекции компиляторов GNU (GCC)».
  2. ^ Джанна Чиони, Антони Кречмар, Программируемое освобождение без висячих ссылок , Information Processing Letters , т. 18, 1984 , стр. 179–185
  3. ^ ab CN Fisher, RJ Leblanc, Реализация диагностики во время выполнения в Pascal , IEEE Transactions on Software Engineering , 6(4):313–319, 1980.
  4. ^ Dalci, Eric; анонимный автор; CWE Content Team (11 мая 2012 г.). "CWE-416: Use After Free". Перечисление распространенных слабостей . Mitre Corporation . Получено 28 апреля 2014 г. . {{cite web}}: |author2=имеет общее название ( помощь )
  5. ^ "CVE-2014-1776". Распространенные уязвимости и риски (CVE) . 2014-01-29. Архивировано из оригинала 2017-04-30 . Получено 2017-05-16 .
  6. ^ Чен, Сяобо; Кэселден, Дэн; Скотт, Майк (26 апреля 2014 г.). «Новый эксплойт нулевого дня, нацеленный на версии Internet Explorer 9–11, выявлен в целевых атаках». Блог FireEye . FireEye . Получено 28 апреля 2014 г. .
  7. ^ Шаблоны заполнения памяти Visual C++ 6.0
  8. ^ Отладка указателя Mudflap
  9. ^ Дхурджати, Д. и Адве, В. Эффективное обнаружение всех случаев использования висячих указателей на производственных серверах