What is a "known bad pointer value"?
Asked Answered
M

3

1

In this answer to this question, it was noted that:

If you're going to 'clear' the pointer in the dtor, a different idiom would be better - set the pointer to a known bad pointer value.

and also that the destructor should be:

~Foo()
{
    delete bar;
    if (DEBUG) bar = (bar_type*)(long_ptr)(0xDEADBEEF);
}

I have two question about these parts of the answer.

Firstly, how can you set the pointer to a known bad pointer value? How can you set a pointer to an address which you can assure won't be allocated?

Secondly, what does: if (DEBUG) bar = (bar_type*)(long_ptr)(0xDEADBEEF); even do? What's DEBUG? I couldn't find a macro named so. Also, what's long_ptr? What does it do?

Macneil answered 24/6 at 19:23 Comment(34)
DEBUG is not a standard macro, it's one that you would define to work with this code.Impede
How can you set a pointer to an address which you can assure won't be allocated? You have to know how the target allocates memory. Often there are little holes left for a variety pf purposes, including use as an easily detectable error value.Cow
@Impede -- alright thanksMacneil
@Cow -- then am I supposed to use one of those "holes" address? And I do, how am I supposed to do it? And what're these holes addresses?Macneil
The code just assumes that a randomly selected address like 0xDEADBEEF will not be valid. There's no standard way to get an invalid pointer other than NULL.Impede
The reason to prefer 0xDEADBEEF over NULL is that it will be more obvious in debugging output. NULL pointers are often used for initial values, so you can't distinguish a pointer that hasn't been updated from one that has been cleared.Impede
On a 32 bit system the odds of getting DEADBEAF even without guarantees are astronomically low. The odds of getting one exactly where you don't want it will be staggeringly low.Cow
@Impede -- But what if there's data stored in 0xDEADBEEF ? How can it know if it's random garbage or actual address?Macneil
In that case it just sucks to be you. But the odds are one in literally billions on a system with 32 bit addressing. Fuggeddabout it on a 64 bit system..Cow
you don't need to. if the pointer points to DEADBEEF then you "know" that the pointer was set to that value to indicate it has been cleared. If it was spurious, oh well, 1 in a couple billion shot.Tally
you can't know. That's why it's an assumption. Most addresses aren't in use, so it's very unlikely you'll choose something that's valid.Impede
Even if it's in valid memory, the chance that it's the start of an object is really low. So if you see it in a pointer, you can be practically certain it's this placeholder value.Impede
The big-ass cast: 0xDEADBEEF relatively safe value for bad pointers decided upon by programmer. (long_ptr)(0xDEADBEEF) cast the value to a long_ptr, which is presumably an alias for a pointer on a 32 bit address system. It's probably void* these days. The (bar_type*) tells the C++ compiler, "Yes, I really mean to violate strict aliasing and assign a pointer of the wrong type to a bar. I know what I'm doing, so don't complain."Cow
I understand what you're saying, but it's really weird for me. You're relying on luck. I might just use all the memory I have. Setting it a random address is hoping that a bug won't show up. Aren't there pre-determined unused addresses which you can set a pointer to as an indicator of has-freed-its-memory ?Macneil
The idea of using a special value like 0xDEADBEEF is that when you see it in a debugger it is extremely likely that it is deallocated storage. The goal is not to implement a robust system, you can't (or I guess shouldn't) write code that compares pointers to 0xDEADBEEF to manage errors, it's just a flag a programmer can spot while debugging.Laparotomy
You might use all the memory, but take not of Barmar's comment about that address must be at the start of an object. If you're allocating memory byte-by-byte to make this more likely, you've got other, bigger, problems.Cow
@CSStudent The whole point is to make it more likely for a bug to show up in a recognizable way while you are testing. If the rest of the program doesn't have undefined behavior, then there is absolutely zero effect of writing anything to bar. When the destructor exits reading from it will be undefined behavior. But if you made a mistake somewhere and accidentally try to read it after it was destructed, then you want your program to fail as quickly and hard as possible while testing. A random address gives a better chance than zero or a previous value that has been valid earlier.Polyester
Also, for this to have any effect, you need to make sure that you compile with optimizations disabled while testing/debugging. Otherwise the compiler is going to just remove the write completely, because it doesn't have any observable effect.Polyester
And I really don't like this being done in the destructor of each and every class. Instead let your compiler or other tooling overwrite memory on free, use ASAN/valgrind, or if that isn't supported, overwrite the global operator delete to do it for you in every delete call.Polyester
And I have no idea why the quoted code 1. uses if(DEBUG), when it would much more likely be something like #ifdef DEBUG and 2. why it uses the weird cast combination when the simpler and cleaner bar = reinterpret_cast<bar_type*>(0xDEADBEEF) would have exactly the same effect.Polyester
Because of alignment, an odd-address pointer (like 0xDEADBEEF) has a very good chance of not being a valid pointer on many architectures. (Or even if valid, not likely to be malloc'd by the heap manager.)Dagon
@Polyester The weird casting is probably because the code originally came from C, and they didn't bother rewriting it in C++ style.Impede
@Impede I am pretty sure that the single cast (bar_type*)0xDEADBEEF has also always been allowed in C.Polyester
Instead of those crappy old tricks, please just enable Address Sanitizer which is a powerful debugging tool available on all major compilers. It will catch for you a lot of different memory access errors, not only use-after-free.Conceited
@Cow -- if long_ptr is equivalent to void*, why then is it requried? what does it do? Wouldn't bar = (bar_type*) 0xDEADBEEF work the same?Macneil
@CSStudent I asked the poster of the answer. I don't see any use for (long_ptr) (whatever that is) but it could be some legacy thing, taking 16 bit short jump pointers into account perhaps.Untruth
@TedLyngmo -- Thanks, I'll look for an answerMacneil
Thank you all too, for the answers and advices!Macneil
The example you gave is most likely someone hacking C code to make it work in C++. In C you can assign anything to void* and anything from void*, so the code probably originally looked like if (DEBUG) bar = (long_ptr)(0xDEADBEEF);. C++ is significantly more paranoid about casting because casting is one hell of a fast way to blow your program up if you're not very, very careful.Cow
And if you go back in time, you get some really interesting pointer use, for example the old Segment and Offset used to get >16 bit addressing on 16 bit systems. long_ptr likely would have been a bit more interesting that just void* back in those days. Mind you, you wouldn't be stuffing DEADBEAF in as a address on one of those systems.Cow
Btw, one additional question: he wrote: 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 but what would result in a crash? dereferencing the pointer? Because from my tests, it doesn't crash when dereferencing the pointer wafter assigning 0xDEADBEEF to itMacneil
Best I can say is "This is unfortunate". If you want to crash on a failed dereference, use nullptr. It's not as easy to visually recognize as being wrong, many reasons to park a pointer at nullptr, but it almost certainly will crash. Unfortunately we can't guarantee it will, Undefined Behaviour's undefined after all, but I haven't worked on a processor that doesn't reserve some amount of space around 0 as a no-man's land and force a crash if someone steps into it in quite a while.Cow
And because of the downsides of using NULL, the linked question recommends not using nullptr. If you can find a magic, non NULL you can guarantee will crash, use that. If you cannot, you takes yer chances. These days we've got some really good tools like valgrind and ASAN you can use to trap a lot of problems like this without vandalizing your code. Consider using them.Cow
It's worth noting that answer was written in 2011. You should be aware that a decade is a long time in software, and the current recommendations like asan just didn't exist then.Bicknell
S
1

how can you set the pointer to a known bad pointer value? How can you set a pointer to an address which you can assure won't be allocated?

First, let me be clear that the example code you refer to is a dirty hack and is intended to provide some guidance to assist debugging. It is not intended as a production quality memory management tool; it isn't even intended to be "drop in" code - it's an example of a debugging technique.

Setting a pointer to a hardcoded value isn't guaranteed to be a "bad pointer" unless you know something about the target environment. 0xDEADBEEF is a value that is likely to be an invalid pointer on many environments just out of luck. The value was chosen in because I had seen it used as a marker for "invalid data" in other code and it is easily spotted when viewing memory dumps. I believe it is (or was, maybe not anymore - that answer was from 14 years ago!) commonly used to indicate memory areas that are invalid/unused. Similar to some of the values Microsoft used in their debug library versions of some memory management routines (see https://stackoverflow.com/a/370362)

what does: if (DEBUG) bar = (bar_type*)(long_ptr)(0xDEADBEEF); even do? What's DEBUG? I couldn't find a macro named so.

I might not have said explicitly in the answer you refer to, but the example code might more properly be call a pseudo-code example. if (DEBUG) is used to indicate a bit of code that is conditionally executed "if this is a debug build".

For example, DEBUG could be a macro (or variable) that is defined as non-zero during a debug build of the problem. Possibly on the compiler's command line (maybe something like -DDEBUG=1). A DEBUG macro is something that I found commonly used for code that is enabled in debug builds only.

Also, what's long_ptr? What does it do?

To aid in the transition from 32-bit to 64-bit systems, MS added types that are size of pointers. LONG_PTR is an integer type that has the size of a pointer (32 or 64 bits as appropriate). I probably should have used LONG_PTR instead of long_ptr. I believe the cast is technically unnecessary, but I think it's still useful as a notation that makes clear that an integer is being 'converted' to a pointer - a coding idiom that uses dirty looking casts to call out a dirty hack.

Santoyo answered 26/6 at 8:44 Comment(2)
So you used long_ptr for more clarity? If you did do it only so it'd be more apparent that "an integer is being 'converted' to a pointer", then I think that just seeing the asterisk would've suffice. Also, excuse me for the hassle, but in your answer, why did you write: 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: ? Because from what I've checked, it doesn't crashMacneil
I honestly can't remember nuances of what I was thinking 14 years ago. I don't think it's a crucial part of the answer. Today's C++ provides smart pointer types such as unique_pointer and shared_pointer which are likely to be better tools to deal with the problem that was being discussed back then.Santoyo
B
3

0xDEADBEEF is a pointer value like any other. Strictly speaking, there is no "known bad pointer value".

However, as a very rough simplification we can assume pointer values to be random. The chances to see 0xDEADBEEF as 32-bit value is 2^-32, that is: 2.3283064e-10. It is much more likely to be hit by a lightning or to find 2 four leaved glovers in a row than to see exactly that value on any given pointer. In other words, for all practical purposes we can assume that a 0xDEADBEEF is from our assignment when we see it.

DEBUG is not a standard macro. The author just wanted to illustrate that doing the assignment can be enabled in debug builds and disabled in non-debug builds (by employing a dedicated flag).

Blacktop answered 24/6 at 20:11 Comment(9)
Memory is allocated in pages and all addresses in the same page have the same validity.Impede
There is the additional factor that 0xDEADBEEF is odd, so the only object it can point to is an object of alignment 1. It could only point to an object of size 1 which further restricts its appearance as a pointer value.Laparotomy
So you think it's a trap, you are expecting the program to crash and then look in the debugger and seeing DEADBEEF you realize you are accessing the object that you deleted and then you are trying to get through the program to find where it could possibly come from. I don't see how it's different from null pointer, you have to do the same. But actually with DEADBEEF you may not see access violation/alignment or segfault, because it's an undefined behavior and compiler could optimize away your assignment.Fortuitous
@Fortuitous -- Hence using a "magic number" or using nullptr wouldn't matter? Btw, then what would you advise? Assigning to nullptr for possibe-crash and hence bug detection, or leaving the pointer as is after destructor's call?Macneil
Question goes for you all please, a better solution than the one in: this would be awesome.Macneil
@CSStudent The better solution is to avoid manual memory management. If you absolutely need a pointer, then use unique_ptr which manages the lifetime for you. If everything is an RAII type then no memory errors can occur unless there is UB in the code somewhere.Tally
@CS Student -- you don't assign 'trap' after destruction, the pointer should either be gone out of scope (preferably) or you assign a valid or null pointer to it, if it outlives the object.Fortuitous
So either use unique_ptr or if I use raw pointer, let it be unless if it outlives the object, and then I should handle the pointer accordinglyMacneil
unique_ptr is a very good tool for dealing with raw pointers. People should keep in mind: 1) the question asked was about managing raw pointers, and 2) 14 years ago it was 2010, so it was very common for projects to be using C++ 98 or C++ 03, not C++ 11. Clearly, the question and answer are not as relevant today.Santoyo
S
3

Unless you have system-level access, pointer values near NULL are all bad. Like 1, 2, 3, etc.

I disagree with the premise of the linked question. If you think that testing should find memory leaks using magic numbers for pointer values, then there is a serious design failure in your application and testing strategies.

Write good code. Don’t write bad code to find bad code.

Strutting answered 24/6 at 22:42 Comment(5)
Then how would you replicate the "magic numbers" method?Macneil
It is unclear what you are asking, but as I understand it my answer is don’t use magic numbers.Picoline
@CSStudent the problem is the use of raw pointers in general. You're trying to prevent a "use after free"-type of situation, but in most cases, that's not going to help much. You're even doing this in a destructor, so that "bar" pointer will be gone anyway. "Use after free" is much more likely to show up in other objects or threads that happen to have their own copy of that pointer, which is not "DEADBEEF"...Duenna
@ChristianStieber -- and how would you solve such a problem then?Macneil
@CSStudent I don't have this kind of problem. "Use after free" is rarely an issue these days, since using automatic resource management through destructors and smartpointers mostly prevents it by also forcing us to be more aware of pointer lifetimes: if I never store a "raw pointer" anywhere, and now I suddenly do, there's bound to be some thought behind it how that interacts with the unique_ptr or smart_ptr that it came from. And while valgrind is too bugged for me to be useful right now, I also keep an eye open for crashes. Also, runtime libraries might assist by trashing freed memory.Duenna
S
1

how can you set the pointer to a known bad pointer value? How can you set a pointer to an address which you can assure won't be allocated?

First, let me be clear that the example code you refer to is a dirty hack and is intended to provide some guidance to assist debugging. It is not intended as a production quality memory management tool; it isn't even intended to be "drop in" code - it's an example of a debugging technique.

Setting a pointer to a hardcoded value isn't guaranteed to be a "bad pointer" unless you know something about the target environment. 0xDEADBEEF is a value that is likely to be an invalid pointer on many environments just out of luck. The value was chosen in because I had seen it used as a marker for "invalid data" in other code and it is easily spotted when viewing memory dumps. I believe it is (or was, maybe not anymore - that answer was from 14 years ago!) commonly used to indicate memory areas that are invalid/unused. Similar to some of the values Microsoft used in their debug library versions of some memory management routines (see https://stackoverflow.com/a/370362)

what does: if (DEBUG) bar = (bar_type*)(long_ptr)(0xDEADBEEF); even do? What's DEBUG? I couldn't find a macro named so.

I might not have said explicitly in the answer you refer to, but the example code might more properly be call a pseudo-code example. if (DEBUG) is used to indicate a bit of code that is conditionally executed "if this is a debug build".

For example, DEBUG could be a macro (or variable) that is defined as non-zero during a debug build of the problem. Possibly on the compiler's command line (maybe something like -DDEBUG=1). A DEBUG macro is something that I found commonly used for code that is enabled in debug builds only.

Also, what's long_ptr? What does it do?

To aid in the transition from 32-bit to 64-bit systems, MS added types that are size of pointers. LONG_PTR is an integer type that has the size of a pointer (32 or 64 bits as appropriate). I probably should have used LONG_PTR instead of long_ptr. I believe the cast is technically unnecessary, but I think it's still useful as a notation that makes clear that an integer is being 'converted' to a pointer - a coding idiom that uses dirty looking casts to call out a dirty hack.

Santoyo answered 26/6 at 8:44 Comment(2)
So you used long_ptr for more clarity? If you did do it only so it'd be more apparent that "an integer is being 'converted' to a pointer", then I think that just seeing the asterisk would've suffice. Also, excuse me for the hassle, but in your answer, why did you write: 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: ? Because from what I've checked, it doesn't crashMacneil
I honestly can't remember nuances of what I was thinking 14 years ago. I don't think it's a crucial part of the answer. Today's C++ provides smart pointer types such as unique_pointer and shared_pointer which are likely to be better tools to deal with the problem that was being discussed back then.Santoyo

© 2022 - 2024 — McMap. All rights reserved.