Как правильно напечатать адрес переменной, уже освобожденной из кучи?

Я делаю утилиту ведения журнала, которая отслеживает выделения и освобождения внутри созданной мной библиотеки.

Моя программа не рухнула, но я все еще скептически отношусь к своему подходу.

void my_free(struct my_type *heap)
{
    if (!heap)
        logger("Fatal error: %s", "Null pointer parameter");

    // I leave this here on purpose for the application to crash
    // in case heap == NULL
    free(heap->buffer);

    void *address = heap;

    free(heap);

    logger("Heap at %p successfully deallocated", address);
    // since heap->buffer is always allocated/deallocated together with
    // heap I only need to track down the heap address
}
  • Есть ли проблема сделать это таким образом?
  • Могу ли я сохранить адрес в числовом виде? Как в беззнаковом целом? Какой тип по умолчанию?

person LeoVen    schedule 20.06.2019    source источник


Ответы (5)


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

Согласно C 2018 6.2.4 2, «значение указателя становится неопределенным, когда объект, на который он указывает (или только что прошедший), достигает конца своего времени существования». Основная причина этого правила состоит в том, чтобы разрешить реализации C, в которых байты в указателе не содержат прямого адреса памяти, но содержат информацию, которая используется для поиска адресной информации в различных структурах данных. Когда объект освобождается, несмотря на то, что байты в указателе не изменяются, данные в этих структурах могут измениться, и тогда попытка использования указателя может потерпеть неудачу по разным причинам. Примечательно, что попытка напечатать значение может завершиться неудачей, поскольку прежний адрес больше не доступен в структурах данных. Однако, поскольку это правило существует, даже менее экзотические реализации C могут использовать его для оптимизации, поэтому компилятор C может реализовать, например, free(x); printf("%p", (void *) x); эквивалентно free(x); printf("%p", (void *) NULL);.

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

#include <inttypes.h>
#include <stdint.h>
...
uintptr_t ux = (uintptr_t) (void *) x;
free(x);
…
printf("%" PRIxPTR, ux);

Обратите внимание, что для строгого соответствия мы сначала преобразуем указатель в void *, а затем в uintptr_t. Это просто потому, что стандарт C не определяет явно поведение преобразования любого указателя в uintptr_t, но определяет поведение преобразования указателя на объект в void * и преобразования void * в uintptr_t.

person Eric Postpischil    schedule 20.06.2019
comment
Я собирался попросить разъяснений, можете ли вы напрямую указать любой указатель на uintptr_t, и нашел здесь соответствующее обсуждение: stackoverflow.com/questions/34291377/ - person LeoVen; 21.06.2019
comment
@LeoVen: Спасибо, обновил void * в качестве промежуточного шага. - person Eric Postpischil; 21.06.2019
comment
uintptr_t — необязательный тип. Я не думаю, что существует гарантированно существующий, строго соответствующий путь для печати значения освобожденного указателя, если только что-то вроде unsigned long long не всегда может хранить значение указателя. - person Andrew Henle; 21.06.2019
comment
@AndrewHenle: Тем не менее, это лучший вариант: если код использует указатель, когда он не определен, мы не знаем, что произойдет. Если код использует uintptr_t, мы знаем, что произойдет: либо он скомпилируется и заработает, либо не скомпилируется, предупреждая нас о том, что для этой конкретной реализации необходимы настройки. - person Eric Postpischil; 21.06.2019

Ваш код в порядке.

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

Очевидно, что если вы попытаетесь сделать что-то с *heap или heap[i] после того, как память была освобождена, вы столкнетесь с неопределенным поведением, и может случиться что угодно, но вы можете проверить или распечатать heap (или address) все, что хотите, без проблем.

<ч> <суп>

  1. Where "valid" means "pointing to an object within that object's lifetime".

person John Bode    schedule 20.06.2019
comment
Согласно стандарту C, указатель на сам объект становится неопределенным, когда время жизни объекта заканчивается. Таким образом, после free(x) попытка напечатать x не определяется стандартом C. Многие реализации C не вызовут проблем в этом отношении (причина этого в стандарте заключается в том, чтобы приспособить реализации, в которых указатель имеет «живую» часть — для его использования требуется поиск данных, которые могут измениться или исчезнуть при освобождении указанного указателя). -для объекта). Однако было бы лучше преобразовать указатель в uintptr_t, прежде чем выполнять free и использовать это значение для отслеживания и печати. - person Eric Postpischil; 21.06.2019
comment
может напечатать значение NULL, которое по определению является недопустимым значением указателя, используя %p без проблем. --› Не обязательно. NULL не указывается в C как указатель. Это может быть какой-то целочисленный тип со значением 0. Присвоение/сравнение NULL с указателем хорошо определено, но printf("%p", NUULL); также, безусловно, недопустимо. sizeof(void *) может отличаться от sizeof(NULL) - person chux - Reinstate Monica; 21.06.2019
comment
@EricPostpischil Но uintptr_t - это необязательный тип. (Эта часть C - беспорядок...) - person Andrew Henle; 21.06.2019
comment
@AndrewHenle: Тем не менее, это лучший вариант: если код использует указатель, когда он не определен, мы не знаем, что произойдет. Если код использует uintptr_t, мы знаем, что произойдет: либо он скомпилируется и заработает, либо не скомпилируется, предупреждая нас о том, что для этой конкретной реализации необходимы настройки. - person Eric Postpischil; 21.06.2019

Согласно стандарту C:

Спецификаторы преобразования и их значения:

...

p

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

Итак, чтобы полностью соответствовать стандарту C, вам нужно привести heap к void *:

logger("Heap at %p successfully deallocated", ( void * ) address);

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

person Andrew Henle    schedule 20.06.2019

void *address = &heap; -->> void *address = heap;

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

person 0___________    schedule 20.06.2019

Из-за моего ограниченного рейтинга я пока не могу комментировать сообщения других, поэтому использование «ответа», пока это не ответ, а комментарий.

Ответ/комментарий Эрика Постпишила выше требует рецензирования. Я не согласен и не понимаю его рассуждений.

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

Обратите внимание на прототип стандартного вызова C free(): void free(void *ptr). В месте вызова ptr value останется неизменным до и после бесплатного вызова — он передается через value.

«Использовать» указатель будет/может потерпеть неудачу по-разному, если под использованием мы подразумеваем уважение к нему после вызова free(). Печать значения указателя не приводит к его уважению. Также печать (значения) указателя с вызовом printf с использованием %p должна быть корректной, даже если представление значения указателя определено реализацией.

"*... ​​Однако, поскольку это правило существует, даже менее экзотические реализации C могут использовать его для оптимизации, поэтому компилятор C может реализовать free(x); printf("%p", (void *) x); эквивалентно free(x); printf("%p", (void ) NULL); например. ..."

Даже если он (компилятор) сделает это, я не могу легко понять, что бы это оптимизировало, вызов специального минимизированного printf со значением 0 для указателя? Также рассмотрите что-то вроде:

extern недействительным my_free (недействительным * ptr); // где-то определено.. my_free(myptr); printf("%p", myptr);

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

Так что нет, я не могу согласиться с вашей интерпретацией стандарта C на данном этапе.

(мое) РЕДАКТИРОВАТЬ:

Если только в некоторых «экзотических» реализациях с указателем памяти, реализованным в виде структуры, и он передается в/из «пользовательского» void*. Тогда пользовательское значение кучи будет недействительным или нулевым при попытке его распечатать.. Но тогда каждый вызов C, который принимает void* в качестве указателя на память, должен будет различать, какой void* указывает на кучу, а не - скажем, memcpy, memset, - и тогда это становится довольно занятым...

person v01d    schedule 20.06.2019
comment
Во-первых, вы не обратились к явному утверждению стандарта C: «Значение указателя становится неопределенным…». Обратите внимание, что здесь говорится, что значение становится неопределенным. Далее в вашем ответе говорится: «Ptr value останется прежним». Но это неверно; стандарт C прямо говорит, что значение становится неопределенным — мы не знаем, каким будет его значение. - person Eric Postpischil; 21.06.2019
comment
Во-вторых, стандарт C дополнительно сообщает нам, что простое использование значения приводит к неопределенному поведению: ненормативный пункт J.2 перечисляет вещи, вызывающие неопределенное поведение, и включает: «Используется значение указателя на объект, время жизни которого закончилось». (6.2.4)». - person Eric Postpischil; 21.06.2019
comment
В-третьих, вы не последовали объяснению, которое я дал. Например, реализация C может поддерживать схему адресации, которая использует вспомогательную память, а также основную память. Чтобы справиться с этим, указатель — это не адрес в памяти, а дескриптор базы данных объектов. Когда указатель используется для доступа к объекту, дескриптор ищется в базе данных, и данные при необходимости загружаются в основную память, а затем осуществляется доступ к данным. Но даже когда указатель просто печатается, ищется дескриптор, и печатается полная информация о месте вспомогательного хранения… - person Eric Postpischil; 21.06.2019
comment
… Теперь предположим, что в такой реализации у вас есть free(x); printf("%p", (void *) x);. При выполнении free дескриптор удаляется из базы данных. Затем, когда выполняется printf, байты в x используются для поиска дескриптора. К сожалению, его больше нет в базе данных. Возможно, программа обнаруживает ошибку и вылетает. Возможно, он возвращает мусорные данные. Возможно, он возвращает нулевой адрес. Стандарт C не говорит нам, что произойдет; он просто говорит, что значение указателя неопределенно. - person Eric Postpischil; 21.06.2019
comment
В-четвертых, ваше заявление о том, что «я не могу понять, что это оптимизирует; вызов специального минимизированного printf со значением 0 для указателя» не поддерживает ваш случай. Конечно, я видел оптимизации для printf с постоянными значениями. printf("%d", 34); можно изменить на fputs("34", stdout);, что быстрее, поскольку printf не нужно анализировать строку формата во время выполнения. - person Eric Postpischil; 21.06.2019
comment
В-пятых, ваш пример случая, в котором printf не оптимизирован, не имеет значения - нет утверждения, что желаемый эффект кода всегда будет нарушен оптимизацией, разрешенной стандартом C, просто что он может быть нарушен. Показ случая, когда он не сломан, не опровергает того, что он может быть сломан. - person Eric Postpischil; 21.06.2019
comment
Подводя итог, пусть стандарт C говорит сам за себя: «Значение указателя становится неопределенным…» Это спецификация семантики C для данной ситуации; это авторитетное заявление по этому вопросу. Остальное просто объяснение для тех, кто еще не понял. - person Eric Postpischil; 21.06.2019
comment
Ну, я как бы потом подумал о более сложном указателе кучи... Но все еще очень удивлен, что я не могу полагаться на то, что мое значение void* останется прежним после вызова, в котором я передал его по значению, а не по ptr (в ptr) ... То же самое, я имею в виду, с точки зрения пользовательского интерфейса C lib, независимо от того, что реализация библиотеки стоит за этим.. Немного шока от холодной воды, tbh.. И да, я не могу утверждать, что понимаю стандарт или регулярно читаю его, последний, который я частично просмотрел, был C99 - person v01d; 21.06.2019
comment
В целом, приносим извинения за то, что вам пришлось все это напечатать, все достижения мои, ваше время потеряно (но вы, по крайней мере, набрали очки ..) - person v01d; 21.06.2019