stepping into destructors in VC++ express 2005
Maybe I'm wrong, but when you're debugging, and you come upon a delete statement for an object (pointer to an object) that has a defined destructor, and attempt to step into it, shouldn't it ... you know ... work?
I just tried, on two of my projects, to run up to a 'delete' call and step into it, but it just skipped over the call.
I am able to find the destructor and explicitly run up to the destructor, but it doesn't work when I attempt to step into a delete call.
This is possible right? Tell me I haven't just been imagining it all this time.
"Standard?" Insofar as the behavior of a particular IDE's debugger can be called "standard," I suppose it is. You'll notice you can't step directly into a constructor from the "new foobar" statement as well.
The reason you can't step into these calls directly is because there's no information in the .pdb to get file/line information from -- because there is no C++ source file or, indeed, source code at all.
For non-trivial types (with constructors and/or destructors), a "call" to new or delete is more than just an invocation of a global operator new or delete function someplace. The compiler generates a bunch of code and injects it right there at the call site to call constructors or destructors at the appropriate point; there's no C++ source code to "step into" immediately (your actual C++ for your constructor/destructor is a couple jumps away yet), just machine code, so the IDE does the next best thing -- it steps over the call instead.
Examining the disassembly output for various examples of this situation can be very educational, I'd be happy to walk through an example or two if you aren't familiar enough with the practice.
The reason you can't step into these calls directly is because there's no information in the .pdb to get file/line information from -- because there is no C++ source file or, indeed, source code at all.
For non-trivial types (with constructors and/or destructors), a "call" to new or delete is more than just an invocation of a global operator new or delete function someplace. The compiler generates a bunch of code and injects it right there at the call site to call constructors or destructors at the appropriate point; there's no C++ source code to "step into" immediately (your actual C++ for your constructor/destructor is a couple jumps away yet), just machine code, so the IDE does the next best thing -- it steps over the call instead.
Examining the disassembly output for various examples of this situation can be very educational, I'd be happy to walk through an example or two if you aren't familiar enough with the practice.
Quote:Original post by jpetrie
Examining the disassembly output for various examples of this situation can be very educational, I'd be happy to walk through an example or two if you aren't familiar enough with the practice.
Yeah, definately. I'll never pass up an opportunity to learn.
Have you got some examples, or do you want to tell me how to scrounge some up? I assume it's no more difficult that getting the compiler to stop compiling at the assembly stage (even though I have no idea how to do that for VC++), and knowing where to look.
Bump for what?
Yes, it's a known and acknowledged bug in Visual Studio .NET, all versions, that stepping into destructors doesn't work. You have to set a breakpoint inside the destructor. I believe the thread already said as much?
Yes, it's a known and acknowledged bug in Visual Studio .NET, all versions, that stepping into destructors doesn't work. You have to set a breakpoint inside the destructor. I believe the thread already said as much?
It was a bump for jpetrie to come back and walk me through a couple of destructor calls, like he offered.
I mean, since he offered, we might as well do it here instead of starting a new thread.
I mean, since he offered, we might as well do it here instead of starting a new thread.
I don't use VC++ at work, so I had to defer my follow up until after I got home, sorry it took so long.
Anyway, consider the following simple example:
Place breakpoints on all four lines of main() and debug the program. Once you hit a breakpoint, right-click in the editor window and pick "Go to disassembly." This will show you the breakdown of the source lines and their "matching" assembly.
It looks something like:
Some observations: both new-expressions can be stepped into, the "delete p" cannot (as expected), and the "delete q" can. It's the call instructions that are the important parts, here. Trimming out the other instructions, then, leaves us with:
Notice how the compiler has injected the constructor call for foo (int obviously has no constructors or destructors). Also notice that "delete p" doesn't result in a call to operater delete. This is the root cause of our "problem."
While you are in disassembly mode, step and step-into work on a per-instruction level (very useful). You can step-into the call to that weird "foo::`scalar deleting destructor'" thing -- you'll most likely be taken to some random-looking collection of jmp instructions, this is not relevant to our discussion so step-into the jmp instruction, and you'll end up here:
Note the lack of C++ source line annotations -- this is purely compiler-generated code here, and that's why you can't step into it. The calls to functions that there's code for -- operater delete and foo::~foo are buried in this code.
I don't have inside knowledge as to how the debugger is written, but I imagine when doing source-level debugging the debugger will simply look up a block of information in the .pdb related to the function it is about to call (when doing a step-into). That block of information would include, if available, the file/line information for the C++ source matching that function. As we've seen, the call generated by a delete-expression for a class type doesn't have matching file/line information, so the debugger just steps over instead.
It may be possible to implement the ability to step into destructors from the delete expression, however since its a long-standing issue I don't think its quite as easy as it may seem from where we stand. Furthermore, I think the larger reason why VC++ doesn't implement that feature is that it wouldn't be strictly "correct" to do so, and a debugger should generally strive to be as correct as it possibly can.
Anyway, consider the following simple example:
struct foo{ foo() { } ~foo() { }};int main(){ foo *p = new foo; delete p; int *q = new int; delete q;}
Place breakpoints on all four lines of main() and debug the program. Once you hit a breakpoint, right-click in the editor window and pick "Go to disassembly." This will show you the breakdown of the source lines and their "matching" assembly.
It looks something like:
foo *p = new foo;004113FD push 1 004113FF call operator new (411186h) 00411404 add esp,4 00411407 mov dword ptr [ebp-11Ch],eax 0041140D mov dword ptr [ebp-4],0 00411414 cmp dword ptr [ebp-11Ch],0 0041141B je main+70h (411430h) 0041141D mov ecx,dword ptr [ebp-11Ch] 00411423 call foo::foo (411109h) 00411428 mov dword ptr [ebp-130h],eax 0041142E jmp main+7Ah (41143Ah) 00411430 mov dword ptr [ebp-130h],0 0041143A mov eax,dword ptr [ebp-130h] 00411440 mov dword ptr [ebp-128h],eax 00411446 mov dword ptr [ebp-4],0FFFFFFFFh 0041144D mov ecx,dword ptr [ebp-128h] 00411453 mov dword ptr [ebp-14h],ecx delete p;00411456 mov eax,dword ptr [ebp-14h] 00411459 mov dword ptr [ebp-104h],eax 0041145F mov ecx,dword ptr [ebp-104h] 00411465 mov dword ptr [ebp-110h],ecx 0041146B cmp dword ptr [ebp-110h],0 00411472 je main+0C9h (411489h) 00411474 push 1 00411476 mov ecx,dword ptr [ebp-110h] 0041147C call foo::`scalar deleting destructor' (4110B9h) 00411481 mov dword ptr [ebp-130h],eax 00411487 jmp main+0D3h (411493h) 00411489 mov dword ptr [ebp-130h],0 int *q = new int;00411493 push 4 00411495 call operator new (411186h) 0041149A add esp,4 0041149D mov dword ptr [ebp-0F8h],eax 004114A3 mov eax,dword ptr [ebp-0F8h] 004114A9 mov dword ptr [ebp-20h],eax delete q;004114AC mov eax,dword ptr [ebp-20h] 004114AF mov dword ptr [ebp-0ECh],eax 004114B5 mov ecx,dword ptr [ebp-0ECh] 004114BB push ecx 004114BC call operator delete (411091h) 004114C1 add esp,4
Some observations: both new-expressions can be stepped into, the "delete p" cannot (as expected), and the "delete q" can. It's the call instructions that are the important parts, here. Trimming out the other instructions, then, leaves us with:
foo *p = new foo; 004113FF call operator new (411186h) 00411423 call foo::foo (411109h) delete p;0041147C call foo::`scalar deleting destructor' (4110B9h) int *q = new int; 00411495 call operator new (411186h) delete q;004114BC call operator delete (411091h)
Notice how the compiler has injected the constructor call for foo (int obviously has no constructors or destructors). Also notice that "delete p" doesn't result in a call to operater delete. This is the root cause of our "problem."
While you are in disassembly mode, step and step-into work on a per-instruction level (very useful). You can step-into the call to that weird "foo::`scalar deleting destructor'" thing -- you'll most likely be taken to some random-looking collection of jmp instructions, this is not relevant to our discussion so step-into the jmp instruction, and you'll end up here:
foo::`scalar deleting destructor':00411550 push ebp 00411551 mov ebp,esp 00411553 sub esp,0CCh 00411559 push ebx 0041155A push esi 0041155B push edi 0041155C push ecx 0041155D lea edi,[ebp-0CCh] 00411563 mov ecx,33h 00411568 mov eax,0CCCCCCCCh 0041156D rep stos dword ptr es:[edi] 0041156F pop ecx 00411570 mov dword ptr [ebp-8],ecx 00411573 mov ecx,dword ptr [this] 00411576 call foo::~foo (41119Fh) 0041157B mov eax,dword ptr [ebp+8] 0041157E and eax,1 00411581 je foo::`scalar deleting destructor'+3Fh (41158Fh) 00411583 mov eax,dword ptr [this] 00411586 push eax 00411587 call operator delete (411091h) 0041158C add esp,4 0041158F mov eax,dword ptr [this] 00411592 pop edi 00411593 pop esi 00411594 pop ebx 00411595 add esp,0CCh 0041159B cmp ebp,esp 0041159D call @ILT+315(__RTC_CheckEsp) (411140h) 004115A2 mov esp,ebp 004115A4 pop ebp 004115A5 ret 4
Note the lack of C++ source line annotations -- this is purely compiler-generated code here, and that's why you can't step into it. The calls to functions that there's code for -- operater delete and foo::~foo are buried in this code.
I don't have inside knowledge as to how the debugger is written, but I imagine when doing source-level debugging the debugger will simply look up a block of information in the .pdb related to the function it is about to call (when doing a step-into). That block of information would include, if available, the file/line information for the C++ source matching that function. As we've seen, the call generated by a delete-expression for a class type doesn't have matching file/line information, so the debugger just steps over instead.
It may be possible to implement the ability to step into destructors from the delete expression, however since its a long-standing issue I don't think its quite as easy as it may seem from where we stand. Furthermore, I think the larger reason why VC++ doesn't implement that feature is that it wouldn't be strictly "correct" to do so, and a debugger should generally strive to be as correct as it possibly can.
This topic is closed to new replies.
Advertisement
Popular Topics
Advertisement