stepping into destructors in VC++ express 2005

Started by
7 comments, last by jpetrie 17 years, 5 months ago
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.
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Advertisement
Put a breakpoint on the destructor, then you can step into it from the delete.
.
Okay, but, is this normal? I've completely gone blank on whether its standard or not.
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
"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.
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.
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
bump
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
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?
enum Bool { True, False, FileNotFound };
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.
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
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:
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