В этом ответе на этот вопрос было отмечено, что:
Если вы собираетесь «очистить» указатель в dtor, лучше использовать другую идиому — установить указатель на известное неверное значение указателя.
а также что деструктор должен быть:
~Foo() { delete bar; if (DEBUG) bar = (bar_type*)(long_ptr)(0xDEADBEEF); }
У меня есть два вопроса по поводу этих частей ответа.
Во-первых, как установить указатель на заведомо неверное значение указателя? Как вы можете установить указатель на адрес, который, как вы можете быть уверены, не будет выделен?
Во-вторых, что: if (DEBUG) bar = (bar_type*)(long_ptr)(0xDEADBEEF);
вообще делает? Что такое DEBUG
? Я не смог найти макрос с таким названием. И что такое long_ptr
? Что оно делает?
Как вы можете установить указатель на адрес, который, как вы можете быть уверены, не будет выделен? Вы должны знать, как цель распределяет память. Часто остаются небольшие дыры для различных целей, включая использование в качестве легко обнаруживаемого значения ошибки.
@Бармар -- хорошо, спасибо
@user4581301 user4581301 — тогда я должен использовать один из этих «дырочных» адресов? И я это делаю, как мне это сделать? А какие адреса у этих дырок?
Код просто предполагает, что случайно выбранный адрес, например 0xDEADBEEF
, не будет действительным. Не существует стандартного способа получить недопустимый указатель, кроме NULL.
Причина предпочтения 0xDEADBEEF
NULL
заключается в том, что это будет более очевидно при отладке. Указатели NULL часто используются для начальных значений, поэтому вы не сможете отличить необновленный указатель от очищенного.
В 32-битной системе шансы получить DEADBEAF даже без гарантий астрономически низки. Шансы получить его именно там, где он вам не нужен, будут ошеломляюще низкими.
@Barmar — Но что, если в 0xDEADBEEF хранятся данные? Как он может узнать, случайный ли это мусор или реальный адрес?
В таком случае просто отстой быть тобой. Но в системе с 32-битной адресацией шансы составляют один из буквально миллиардов. Подумал об этом в 64-битной системе..
вам это не нужно. если указатель указывает на DEADBEEF
, то вы «знаете», что указатель был установлен на это значение, чтобы указать, что оно было очищено. Если это была ложь, ну ладно, 1 выстрел из пары миллиардов.
ты не можешь знать. Поэтому это предположение. Большинство адресов не используются, поэтому маловероятно, что вы выберете что-то действительное.
Даже если он находится в действующей памяти, вероятность того, что это начало объекта, очень мала. Поэтому, если вы видите это в указателе, вы можете быть практически уверены, что это значение-заполнитель.
Большой состав: 0xDEADBEEF
относительно безопасное значение для плохих указателей, выбранное программистом. (long_ptr)(0xDEADBEEF)
преобразует значение в long_ptr
, который предположительно является псевдонимом указателя в 32-битной адресной системе. Наверное, сейчас void*
. (bar_type*)
сообщает компилятору C++: «Да, я действительно хочу нарушить строгое псевдонимы и присвоить bar
указатель неправильного типа. Я знаю, что делаю, поэтому не жалуйтесь».
Я понимаю, о чем вы говорите, но для меня это действительно странно. Вы полагаетесь на удачу. Я мог бы просто использовать всю свою память. Установка случайного адреса предполагает надежду, что ошибка не обнаружится. Разве нет заранее определенных неиспользуемых адресов, на которые вы можете установить указатель в качестве индикатора освобождения памяти?
Идея использования специального значения, такого как 0xDEADBEEF
, заключается в том, что когда вы видите его в отладчике, весьма вероятно, что это освобожденное хранилище. Цель не в том, чтобы реализовать надежную систему, вы не можете (или, я думаю, не должны) писать код, который сравнивает указатели на 0xDEADBEEF
для управления ошибками, это просто флаг, который программист может обнаружить во время отладки.
Вы можете использовать всю память, но не принимайте во внимание комментарий Бармара о том, что адрес должен находиться в начале объекта. Если вы распределяете память побайтно, чтобы сделать это более вероятным, у вас возникают другие, более серьезные проблемы.
@CSStudent Вся суть в том, чтобы повысить вероятность появления ошибки в узнаваемом виде во время тестирования. Если остальная часть программы не имеет неопределенного поведения, то от записи чего-либо в bar
нет абсолютно никакого эффекта. Когда деструктор завершит чтение, его поведение будет неопределенным. Но если вы где-то допустили ошибку и случайно попытались прочитать ее после того, как она была уничтожена, то вы хотите, чтобы ваша программа как можно быстрее и жестче вышла из строя во время тестирования. Случайный адрес дает больше шансов, чем ноль или предыдущее значение, которое было действительным ранее.
Кроме того, чтобы это имело какой-либо эффект, вам необходимо убедиться, что вы компилируете с отключенной оптимизацией во время тестирования/отладки. В противном случае компилятор просто полностью удалит запись, поскольку она не имеет никакого наблюдаемого эффекта.
И мне очень не нравится, что это делается в деструкторе каждого класса. Вместо этого позвольте вашему компилятору или другому инструменту бесплатно перезаписывать память, используйте ASAN/valgrind или, если это не поддерживается, перезапишите глобальный operator delete
, чтобы он делал это за вас при каждом вызове delete
.
И я понятия не имею, почему в приведенном коде 1. используется if (DEBUG)
, хотя, скорее всего, это будет что-то вроде #ifdef DEBUG
, и 2. почему он использует странную комбинацию приведения, когда более простой и понятный bar = reinterpret_cast<bar_type*>(0xDEADBEEF)
будет иметь точно такой же эффект.
Из-за выравнивания указатель с нечетным адресом (например, 0xDEADBEEF) имеет очень большую вероятность оказаться недействительным указателем на многих архитектурах. (Или, даже если оно действительно, менеджер кучи вряд ли будет его malloc
.)
@ user17732522 Странное приведение, вероятно, связано с тем, что код изначально был взят из C, и они не удосужились переписать его в стиле C++.
@Barmar Я почти уверен, что одинарное приведение (bar_type*)0xDEADBEEF
всегда разрешено и в C.
Вместо этих дрянных старых трюков просто включите Address Sanitizer, мощный инструмент отладки, доступный во всех основных компиляторах. Он отловит для вас множество различных ошибок доступа к памяти, а не только use-after-free.
@user4581301 user4581301 -- если long_ptr эквивалентен void*, тогда зачем он нужен? Что оно делает? Разве bar = (bar_type*) 0xDEADBEEF
не будет работать так же?
@CSStudent Я спросил плакат ответа. Я не вижу никакой пользы для (long_ptr)
(что бы это ни было), но это может быть какая-то устаревшая вещь, возможно, принимая во внимание 16-битные указатели короткого перехода.
@TedLyngmo -- Спасибо, я поищу ответ
Всем тоже спасибо за ответы и советы!
В приведенном вами примере, скорее всего, кто-то взломал код C, чтобы заставить его работать на C++. В C вы можете присвоить что угодно void*
и что угодно из void*
, поэтому код, вероятно, изначально выглядел как if (DEBUG) bar = (long_ptr)(0xDEADBEEF);
. C++ гораздо более параноидально относится к приведению, потому что приведение — это чертовски быстрый способ взорвать вашу программу, если вы не будете очень и очень осторожны.
А если вы вернетесь назад во времени, вы увидите действительно интересное использование указателей, например старый Сегмент и смещение, используемый для получения> 16-битной адресации в 16-битных системах. long_ptr
вероятно, было бы немного интереснее, чем void*
в те дни. Имейте в виду, вы бы не стали вставлять DEADBEAF в качестве адреса в одной из этих систем.
Кстати, еще один вопрос: он написал: Now if anything has a dangling reference to the Foo object that's been deleted, any use of bar will not avoid referencing it due to a NULL check - it'll happily try to use the pointer and you'll get a crash that you can fix
а к чему приведет сбой? разыменование указателя? Потому что, судя по моим тестам, он не дает сбоя при разыменовании указателя после присвоения ему 0xDEADBEEF.
Лучшее, что я могу сказать, это «Это прискорбно». Если вы хотите аварийно завершить работу при неудачном разыменовании, используйте nullptr
. Визуально распознать неправоту не так-то просто, причин припарковать указатель на nullptr
много, но почти наверняка произойдет сбой. К сожалению, мы не можем гарантировать, что так и будет, в конце концов, Undefine Behaviour не определен, но я не работал над процессором, который не резервирует некоторое количество пространства вокруг 0 в качестве нейтральной зоны и не приводит к сбою, если кто-то вступает в него. довольно долго.
А из-за недостатков использования NULL в связанном вопросе рекомендуется не использовать nullptr
. Если вы можете найти волшебство, отличное от NULL, которое, как вы можете гарантировать, приведет к сбою, используйте его. Если вы не можете, вы рискуете. Сегодня у нас есть несколько действительно хороших инструментов, таких как valgrind и ASAN, которые вы можете использовать для обнаружения множества подобных проблем, не портя при этом свой код. Рассмотрите возможность их использования.
Стоит отметить, что ответ был написан в 2011 году. Вы должны знать, что десятилетие — это большой срок для программного обеспечения, и нынешних рекомендаций, таких как asan, тогда просто не существовало.
0xDEADBEEF
— это значение указателя, как и любое другое. Строго говоря, не существует «известного неверного значения указателя».
Однако в качестве очень грубого упрощения мы можем предположить, что значения указателя являются случайными. Шансы увидеть 0xDEADBEEF
как 32-битное значение составляют 2^-32, то есть: 2,3283064e-10. Гораздо более вероятно, что вас ударит молния или вы найдете 2 четырехлистных перчатки подряд, чем увидеть именно это значение на любом указателе. Другими словами, для всех практических целей мы можем предположить, что 0xDEADBEEF
соответствует нашему заданию, когда мы его видим.
DEBUG
не является стандартным макросом. Автор просто хотел проиллюстрировать, что выполнение присваивания можно включить в отладочных сборках и отключить в неотладочных сборках (с помощью специального флага).
Память выделяется страницами, и все адреса на одной странице имеют одинаковую действительность.
Существует дополнительный фактор: 0xDEADBEEF
является нечетным, поэтому единственный объект, на который он может указывать, — это объект с выравниванием 1. Он может указывать только на объект размера 1, что еще больше ограничивает его внешний вид в качестве значения указателя.
Итак, вы думаете, что это ловушка, вы ожидаете, что программа выйдет из строя, а затем заглядываете в отладчик и видите DEADBEEF, вы понимаете, что получаете доступ к объекту, который вы удалили, а затем пытаетесь пройти через программу, чтобы найти, откуда он мог прийти. от. Я не понимаю, чем он отличается от нулевого указателя, вам нужно сделать то же самое. Но на самом деле с DEADBEEF вы можете не увидеть нарушение прав доступа/выравнивание или ошибку сегмента, потому что это неопределенное поведение, и компилятор может оптимизировать ваше задание.
@Gene - Значит, использование «магического числа» или nullptr не имеет значения? Кстати, а что бы ты посоветовал? Назначить nullptr для возможного сбоя и, следовательно, обнаружения ошибок, или оставить указатель как есть после вызова деструктора?
Вопрос ко всем, пожалуйста, лучшее решение, чем то, что есть: это было бы здорово.
@CSStudent Лучшее решение — избегать ручного управления памятью. Если вам абсолютно необходим указатель, используйте unique_ptr
, который управляет временем жизни за вас. Если все имеет тип RAII, то никаких ошибок памяти возникнуть не может, если где-то в коде нет UB.
@CS Student - вы не назначаете «ловушку» после уничтожения, указатель должен либо выйти за пределы области видимости (предпочтительно), либо вы назначаете ему действительный или нулевой указатель, если он переживет объект.
Поэтому либо используйте unique_ptr
, либо, если я использую необработанный указатель, пусть так и будет, если только он не переживет объект, и тогда я должен обработать указатель соответствующим образом.
unique_ptr
— очень хороший инструмент для работы с необработанными указателями. Люди должны иметь в виду: 1) заданный вопрос касался управления необработанными указателями, и 2) 14 лет назад это был 2010 год, поэтому в проектах было очень распространено использование C++ 98 или C++ 03, а не C++ 11. Очевидно, что вопросы и ответы сегодня не столь актуальны.
Если у вас нет доступа на уровне системы, значения указателя, близкие к NULL, являются плохими. Например, 1, 2, 3 и т. д.
Я не согласен с предпосылкой связанного вопроса. Если вы считаете, что тестирование должно обнаруживать утечки памяти, используя магические числа для значений указателей, то в вашем приложении и стратегиях тестирования произошел серьезный сбой в проектировании.
Пишите хороший код. Не пишите плохой код, чтобы найти плохой код.
Тогда как бы вы воспроизвели метод «магических чисел»?
Непонятно, о чем вы спрашиваете, но насколько я понимаю, мой ответ: не используйте магические числа.
@CSStudent, проблема в использовании необработанных указателей в целом. Вы пытаетесь предотвратить ситуацию типа «использование после освобождения», но в большинстве случаев это не сильно поможет. Вы даже делаете это в деструкторе, так что указатель «bar» все равно исчезнет. «Использовать после освобождения» с гораздо большей вероятностью появится в других объектах или потоках, у которых есть собственная копия этого указателя, которая не является «DEADBEEF»...
@ChristianStieber - и как бы ты тогда решил такую проблему?
@CSStudent У меня нет такой проблемы. «Использование после освобождения» редко является проблемой в наши дни, поскольку использование автоматического управления ресурсами через деструкторы и интеллектуальные указатели в основном предотвращает это, а также заставляет нас больше знать о времени жизни указателя: если я никогда нигде не храню «необработанный указатель», и теперь я вдруг это произойдет, за этим обязательно стоит подумать, как это взаимодействует с unique_ptr или smart_ptr, из которого оно получено. И хотя valgrind слишком глючен, чтобы быть полезным прямо сейчас, я также слежу за сбоями. Кроме того, библиотеки времени выполнения могут помочь в уничтожении освобожденной памяти.
как установить указатель на известное неверное значение указателя? Как вы можете установить указатель на адрес, который, как вы можете быть уверены, не будет выделен?
Во-первых, позвольте мне прояснить, что пример кода, на который вы ссылаетесь, представляет собой грязный хак и предназначен для предоставления некоторых рекомендаций по отладке. Он не предназначен для использования в качестве инструмента управления памятью производственного качества; он даже не предназначен для «вставки» кода — это пример техники отладки.
Установка указателя на жестко запрограммированное значение не обязательно будет «плохим указателем», если вы не знаете что-то о целевой среде. 0xDEADBEEF
— это значение, которое может оказаться недопустимым указателем во многих средах просто по счастливой случайности. Это значение было выбрано потому, что я видел, как оно использовалось в качестве маркера «недопустимых данных» в другом коде, и его легко обнаружить при просмотре дампов памяти. Я считаю, что это (или, возможно, уже не было - этот ответ был 14 лет назад!) обычно используется для обозначения недействительных/неиспользуемых областей памяти. Подобно некоторым значениям, которые Microsoft использовала в своих версиях библиотеки отладки некоторых процедур управления памятью (см. https://stackoverflow.com/a/370362)
что:
if (DEBUG) bar = (bar_type*)(long_ptr)(0xDEADBEEF);
вообще делает? Что такоеDEBUG
? Я не смог найти макрос с таким названием.
Возможно, я не сказал этого явно в ответе, на который вы ссылаетесь, но пример кода правильнее было бы назвать примером псевдокода. if (DEBUG)
используется для обозначения фрагмента кода, который выполняется условно, «если это отладочная сборка».
Например, DEBUG
может быть макросом (или переменной), которая определена как ненулевая во время отладочной сборки проблемы. Возможно, в командной строке компилятора (возможно, что-то вроде -DDEBUG=1
). Макрос DEBUG
— это то, что я обнаружил, обычно используется для кода, который включен только в отладочных сборках.
И что такое
long_ptr
? Что оно делает?
Чтобы облегчить переход от 32-битных к 64-битным системам, MS добавила типы размером с указатели. LONG_PTR
— это целочисленный тип, имеющий размер указателя (32 или 64 бита в зависимости от ситуации). Наверное, мне следовало использовать LONG_PTR
вместо long_ptr
. Я считаю, что приведение технически ненужно, но я думаю, что оно по-прежнему полезно в качестве обозначения, которое ясно дает понять, что целое число «конвертируется» в указатель - идиома кодирования, которая использует грязно выглядящие приведения для выявления грязного взлома.
Итак, вы использовали long_ptr для большей ясности? Если бы вы сделали это только для того, чтобы было более очевидно, что «целое число преобразуется в указатель», то я думаю, что просто увидеть звездочку было бы достаточно. Кроме того, извините за беспокойство, но в своем ответе почему вы написали: Теперь, если что-то имеет висячую ссылку на удаленный объект Foo, любое использование bar не позволит избежать ссылки на него из-за проверки NULL - это Я с радостью попробую использовать указатель, и вы получите сбой, который можно исправить: ? Потому что, насколько я проверил, он не вылетает
Честно говоря, я не могу вспомнить нюансы того, о чем думал 14 лет назад. Я не думаю, что это важная часть ответа. Современный C++ предоставляет интеллектуальные типы указателей, такие как unique_pointer
и shared_pointer
, которые, вероятно, будут лучшими инструментами для решения проблемы, которая обсуждалась тогда.
DEBUG
— это не стандартный макрос, а тот, который вы определили бы для работы с этим кодом.