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

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

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

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

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

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

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

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

{
   char *dp = NULL;
   /* ... */
   {
       char c;
       dp = &c;
   } 
     /* c falls out of scope */
     /* dp is now a dangling pointer */
}

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

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

#include <stdlib.h>

void func()
{
    char *dp = malloc(A_CONST);
    /* ... */
    free(dp);         /* dp now becomes a dangling pointer */
    dp = NULL;        /* dp is no longer dangling */
    /* ... */
}

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

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

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

Освобождение вручную без зависшей ссылки

Антони Кречмар  [ pl ] (1945–1996) создал полную систему управления объектами, свободную от феномена висячих ссылок, см.

Схема аксиом операции kill
Пусть x 1 , ..., x n - переменные, n> 0, 1≤i≤n. Каждая формула следующей схемы является теоремой о виртуальной машине, построенной Кречмаром.
читается как : если объект o является значением n переменных, то после выполнения инструкции kill (x i ) общее значение этих переменных равно нулю (это означает, что с этого момента объект o недоступен и, следовательно, часть память, занимаемая им, может быть переработана с помощью той же операции kill без какого-либо вреда).

Как следствие:

  • нет необходимости повторять операцию kill (x 1 ), kill (x 2 ), ...
  • нет феномена висячих ссылок ,
  • любая попытка доступа к удаленному объекту будет обнаружена и обозначена как исключение « ссылка ни на что ».

Примечание: стоимость убийства постоянна .

Похожий подход был предложен Фишером и ЛеБланом под названием « Замки и ключи» .

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

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

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

int f(int i)
{
    char *dp;    /* dp is a wild pointer */
    static char *scp;  /* scp is not a wild pointer:
                        * static variables are initialized to 0
                        * at start and retain their values from
                        * the last call afterwards.
                        * Using this feature may be considered bad
                        * style if not commented */
}

Бреши в безопасности, связанные с висячими указателями

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

Как избежать ошибок висячих указателей

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

#include <assert.h>
#include <stdlib.h>

/* Alternative version for 'free()' */
static void safefree(void **pp)
{
    /* in debug mode, abort if pp is NULL */
    assert(pp);
    /* free(NULL) works properly, so no check is required besides the assert in debug mode */
    free(*pp);                  /* deallocate chunk, note that free(NULL) is valid */
    *pp = NULL;                 /* reset original pointer */
}

int f(int i)
{
    char *p = NULL, *p2;
    p = malloc(1000);    /* get a chunk */
    p2 = p;              /* copy the pointer */
    /* use the chunk here */
    safefree((void **)&p);       /* safety freeing; does not affect p2 variable */
    safefree((void **)&p);       /* this second call won't fail as p is reset to NULL */
    char c = *p2;       /* p2 is still a dangling pointer, so this is undefined behavior. */
    return i + c;
}

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

    safefree(&p);        /* i'm not sure if chunk has been released */
    p = malloc(1000);    /* allocate now */

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

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

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

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

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

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

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

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

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

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

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

Другое использование

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

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

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