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

Было моим наблюдением, что если free( ptr ) вызывается там, где ptr не является допустимым указателем на системно-распределенную память, происходит нарушение прав доступа. Скажем, что я называю free следующим образом:

LPVOID ptr = (LPVOID)0x12345678;
free( ptr );

Это наиболее определенно приведет к нарушению доступа. Есть ли способ проверить, что ячейка памяти, на которую указывает ptr, является допустимой системной памятью?

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

Отчет о ситуации

Хотя знание того, действительно ли указатель памяти действительно может быть полезным во многих сценариях, это моя особая ситуация:

Я пишу драйвер для нового оборудования, которое заменит существующее аппаратное обеспечение, которое подключается к ПК через USB. Мой мандат заключается в том, чтобы написать новый драйвер, чтобы вызовы существующего API для текущего драйвера продолжали работать в приложениях ПК, в которых он используется. Таким образом, только необходимые изменения в существующих приложениях - это загрузка соответствующих DLL драйверов при запуске. Проблема заключается в том, что существующий драйвер использует обратный вызов для отправки полученных последовательных сообщений в приложение; указатель на выделенную память, содержащую сообщение, передается из драйвера в приложение через обратный вызов. В этом случае приложение должно вызвать другой API-интерфейс драйвера, чтобы освободить память, передав тот же указатель из приложения в драйвер. В этом случае второй API не имеет возможности определить, действительно ли приложение передало указатель на действительную память.

Ответ 1

Чтобы решить вашу конкретную проблему, я не думаю, что вам нужно беспокоиться о проверке указателя. Если приложение передает вашу DLL недействительный адрес, это представляет проблему управления памятью в приложении. Независимо от того, как вы кодируете свой драйвер, вы не можете исправить реальную ошибку.

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

#define DATA_MAGIC 0x12345678
struct data {
    int foo;    /* The actual object data. */
    int magic;  /* Magic number for memory debugging. */
};

struct data *api_recv_data() {
    struct data *d = malloc(sizeof(*d));
    d->foo = whatever;
    d->magic = DATA_MAGIC;
    return d;
}

void api_free_data(struct data *d) {
    if (d->magic == DATA_MAGIC) {
        d->magic = 0;
        free(d);
    } else {
        fprintf(stderr, "api_free_data() asked to free invalid data %p\n", d);
    }
}

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

Ответ 2

На самом деле есть некоторые функции, называемые IsBadReadPtr(), IsBadWritePtr(), IsBadStringPtr() и IsBadCodePtr() что может выполнить эту работу, но не использовать его. Я упоминаю об этом только для того, чтобы вы знали, что эти параметры не для продолжения.

Вам гораздо лучше убедиться, что вы установите все ваши указатели на NULL или 0, когда он ничего не указывает и не проверяет это.

Например:

// Set ptr to zero right after deleting the pointee.
delete ptr; // It okay to call delete on zero pointers, but it
            // certainly doesn't hurt to check.

Примечание. Это может быть проблемой производительности некоторых компиляторов (см. раздел "Размер кода" на этой странице), поэтому на самом деле это может быть стоит сначала выполнить самопроверку против нуля.

ptr = 0;

// Set ptr to zero right after freeing the pointee.
if(ptr != 0)
{
    free(ptr); // According to Matteo Italia (see comments)
               // it also okay to pass a zero pointer, but
               // again it doesn't hurt.
    ptr = 0;
}

// Initialize to zero right away if this won't take on a value for now.
void* ptr = 0;

Еще лучше использовать какой-то вариант RAII и никогда не иметь дело с указателями напрямую:

class Resource
{
public:
    // You can also use a factory pattern and make this constructor
    // private.
    Resource() : ptr(0)
    {
        ptr = malloc(42); // Or new[] or AcquiteArray() or something
        // Fill ptr buffer with some valid values
    }

    // Allow users to work directly with the resource, if applicable
    void* GetPtr() const { return ptr; }

    ~Resource()
    {
        if(ptr != 0)
        {
            free(ptr); // Or delete[] or ReleaseArray() or something

            // Assignment not actually necessary in this case since
            // the destructor is always the last thing that is called
            // on an object before it dies.
            ptr = 0;            
        }
    }

private:
    void* ptr;
};

Или используйте стандартные контейнеры, если это применимо (это действительно приложение RAII):

std::vector<char> arrayOfChars;

Ответ 3

Короткий ответ: Нет.

В Windows есть функция, в которой предположительно сообщает, указывает ли указатель на реальную память (IsBadreadPtr() и он ilk), но он не работает, и вы никогда не должны его использовать!

Истинное решение вашей проблемы состоит в том, чтобы всегда инициализировать указатели на NULL и reset их до NULL, когда вы их delete d.

EDIT на основе ваших изменений:

Вы действительно намекаете на гораздо больший вопрос: как вы можете быть уверены, что ваш код продолжает функционировать должным образом перед лицом клиентского кода, который затягивается?

Это действительно должен быть вопрос сам по себе. Нет простых ответов. Но это зависит от того, что вы подразумеваете под "продолжать правильно функционировать".

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

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

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

Ответ 4

Нет, вы должны знать, указывают ли ваши указатели на правильно распределенную память.

Ответ 5

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

Кроме того, вы вызываете Undefined Поведение, пытаясь free указать недопустимый указатель, чтобы он мог вообще сработать или сделать что-либо вообще.

Кроме того, free является функцией стандартной библиотеки С++, унаследованной от C, а не функцией WinAPI.

Ответ 6

Прежде всего, в стандарте нет ничего, что гарантировало бы такую ​​вещь (free с указателем не malloc ed undefined).

Во всяком случае, передача free - это всего лишь скрученный путь к простому доступу к этой памяти; если вы хотите проверить, читается ли указатель на указателе в Windows, вы должны просто попробовать и быть готовым к устранению исключения SEH; это фактически то, что делают функции IsBadxxxPtr, переведя такое исключение в свой код возврата.

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

Ответ 7

Я не собираюсь повторять то, что все уже сказали, просто чтобы добавить к этим ответам, вот почему существуют интеллектуальные указатели - используйте их!

В любое время, когда вам приходится сталкиваться с сбоями из-за ошибок в памяти - сделайте шаг назад, большой вздох и устраните основную проблему - опасно пытаться обойти их!

ИЗМЕНИТЬ на основе вашего обновления:

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

  • Клиентское приложение предоставляет буфер, в который вы помещаете сообщение, то есть ваш API не беспокоится об управлении этой памятью - для этого требуются изменения в вашем интерфейсе и клиентском коде.
  • Вы изменяете семантику интерфейса и заставляете клиентов интерфейса не беспокоиться об управлении памятью (т.е. вы вызываете указатель на то, что действует только в контексте обратного вызова - если клиент требует, они делают их собственная копия данных). Это не меняет ваш интерфейс - вы все равно можете обратный вызов с указателем, однако ваши клиенты должны будут проверить, не используют ли они буфер за пределами этого контекста - возможно, если они это сделают, это, вероятно, не то, что вы хотите, и поэтому оно может быть хорошо, что они исправляют (?)

Лично я бы пошел за последним, пока вы можете быть уверены, что буфер не используется вне обратного вызова. Если это так, тогда вам придется использовать хакерство (например, было предложено с помощью магического номера), хотя это не всегда гарантируется, например, пусть говорят, что была какая-то форма переполнения буфера из предыдущего блока, и вы как-то переписать магическое число с дерьмом - что там происходит?)

Ответ 8

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

Лучшее предложение, которое я могу вам дать: научиться правильно управлять своей памятью.

Ответ 9

Не без доступа к внутренним компонентам реализации malloc.

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

Ответ 10

Помимо очевидного мнения других о том, что это очень плохая практика, я вижу другую проблему.

Просто потому, что конкретный адрес не вызывает free() для генерации нарушения доступа, не означает, что он безопасен для освобождения этой памяти. Адрес может фактически быть адресом внутри кучи, чтобы не было нарушения доступа, и освобождение его приведет к повреждению кучи. Или это может быть даже действительный адрес, чтобы освободить, и в этом случае вы освободили блок памяти, который все еще может быть использован!

Вы действительно не объяснили, почему такой плохой подход даже следует учитывать.

Ответ 11

По-видимому, вы определили, что вы закончили с объектом, в котором у вас есть указатель, и если этот объект был malloc ed, вы хотите free его. Это не похоже на необоснованную идею, но тот факт, что у вас есть указатель на объект, ничего не говорит о том, как был выделен этот объект (с malloc, с new, с new[], on стек, как разделяемая память, в виде файла с отображением памяти в виде пула памяти APR, используя сборщик мусора Boehm-Demers-Weiser и т.д.), поэтому нет способа определить правильный способ освобождения объекта (или если вообще требуется освобождение, у вас может быть указатель на объект в стеке). Это ответ на ваш реальный вопрос.

Но иногда лучше ответить на вопрос, который должен был быть задан. И этот вопрос: "Как я могу управлять памятью на С++, если я не всегда могу сказать такие вещи, как" как был выделен этот объект и как он должен быть освобожден "?" Это сложный вопрос, и, хотя это нелегко, можно управлять памятью, если вы следуете нескольким политикам. Всякий раз, когда вы слышите, как люди жалуются на правильное соединение каждого malloc с free, каждый new с delete и каждый new[] с delete[] и т.д., Вы знаете, что они делают жизнь более сложной, чем это необходимо не следуя дисциплинированному режиму управления памятью.

Я собираюсь предположить, что вы передаете указатели на функцию, и когда функция выполняется, вы хотите очистить указатели. Эта политика, как правило, невозможна. Вместо этого я бы рекомендовал следовать политике: (1) если функция получает указатель от кого-то другого, тогда ожидается, что "кто-то еще" очистится (в конце концов, "кто-то еще" знает, как была выделена память) и ( 2) если функция выделяет объект, тогда эта документация функции укажет, какой метод следует использовать для освобождения объекта. Во-вторых, я настоятельно рекомендую умные указатели и подобные классы.

Совет Stroustrup:

Если я создаю 10 000 объектов и имею указатели на них, мне нужно удалить эти 10 000 объектов, а не 9999, а не 10,001. Я не знаю, как это сделать. Если мне придется обрабатывать 10 000 объектов напрямую, я собираюсь испортить.... Итак, довольно давно я подумал: "Ну, но я могу справиться с небольшим количеством объектов правильно". Если у меня есть сотня объектов для работы, я могу быть уверен, что правильно обработал 100, а не 99. Если я смогу получить число до 10 объектов, я начну получать удовольствие. Я знаю, как убедиться, что я правильно обработал 10, а не только 9.

Например, вам нужен такой код:

#include <cstdlib>
#include <iostream>
#include "boost/shared_ptr.hpp"

namespace {
    // as a side note, there is no reason for this sample function to take int*s
    // instead of ints; except that I need a simple function that uses pointers
    int foo(int* bar, int* baz)
    {
        // note that since bar and baz come from outside the function, somebody
        // else is responsible for cleaning them up
        return *bar + *baz;
    }
}

int main()
{
    boost::shared_ptr<int> quux(new int(2));

    // note, I would not recommend using malloc with shared_ptr in general
    // because the syntax sucks and you have to initialize things yourself
    boost::shared_ptr<int> quuz(reinterpret_cast<int*>(std::malloc(sizeof(int))), std::free);
    *quuz = 3;

    std::cout << foo(quux.get(), quuz.get()) << '\n';
}

Ответ 12

Почему 0x12345678 обязательно будет недействительным? Если ваша программа использует много памяти, там можно выделить что-то. Действительно, есть только одно значение указателя, которое вы должны абсолютно полагаться на недопустимое распределение: NULL.

Ответ 13

С++ не использует "malloc", а "новый", который обычно имеет другую реализацию; поэтому "delete" и "free" не могут быть смешаны - ни "delete" , ни "delete" [] (его массив-версия) не могут быть удалены.

DLL имеют собственную область памяти и не могут быть смешаны с системой управления памятью области памяти, отличной от DLL.

Каждый API и язык имеет собственное управление памятью для своего типа объектов памяти. I.e.: Вы не открываете ('' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '') '. То же самое относится к очень другому API, даже если этот тип является указателем на память вместо дескриптора.