Search code examples
c++debuggingvisual-studio-2012stlinternals

Why in Release mode part of the variables can be watched within a debugger?


Consider the following code..

#include <vector>

std::basic_string<char> sBasicString = "basic_string";
char* buffer = new char[1000];
for (size_t i = 0 ; i < sBasicString.size() ; ++i)
{
    char c;
    c = sBasicString[i];
    buffer[i] = c;  
}

(Please ignore the memory leak - it is not relevant)

I'm compiling it in VS2012 64bit in both Release and Debug (default configuration).

When i'm running the debugger in Debug mode, i can watch the sBasicString and buffer variables as expected (query their value, etc...)

enter image description here

But when i'm running the debugger in Release mode, i still can watch the sBasicString but not the buffer.

enter image description here

Why?

As Release mode has optimization set to "Full Optimization" (default value) and "Generate Debug Info" set to "YES" - i would expect either both variables can be watched or none.

EDIT

trying to add a proper usage of the buffer variable (avoid compiler optimization) - i still get the same behavior.

enter image description here

EDIT 2 Adding 64bit disassemble output of Release mode compilation

int main()
{
000000013F091000  mov         rax,rsp  
000000013F091003  push        rbx  
000000013F091004  sub         rsp,50h  
000000013F091008  mov         qword ptr [rax-38h],0FFFFFFFFFFFFFFFEh  
    std::basic_string<char> sBasicString = "basic_string";
000000013F091010  xor         ebx,ebx  
000000013F091012  mov         qword ptr [rax-20h],rbx  
000000013F091016  mov         qword ptr [rax-18h],rbx  
000000013F09101A  mov         qword ptr [rax-18h],0Fh  
000000013F091022  mov         qword ptr [rax-20h],rbx  
000000013F091026  mov         byte ptr [rax-30h],bl  
000000013F091029  lea         r8d,[rbx+0Ch]  
000000013F09102D  lea         rdx,[__xi_z+40h (013F093238h)]  
000000013F091034  lea         rcx,[rax-30h]  
000000013F091038  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign (013F0916A0h)  
000000013F09103D  nop  
    char* buffer = new char[1000];
000000013F09103E  mov         ecx,3E8h  
000000013F091043  call        operator new[] (013F091AD8h)  
    for (size_t i = 0 ; i < sBasicString.size() ; ++i)
000000013F091048  mov         edx,ebx  
000000013F09104A  cmp         qword ptr [rsp+38h],rbx  
000000013F09104F  jbe         main+73h (013F091073h)  
    {
        char c;
        c = sBasicString[i];
000000013F091051  lea         rcx,[sBasicString]  
000000013F091056  cmp         qword ptr [rsp+40h],10h  
000000013F09105C  cmovae      rcx,qword ptr [sBasicString]  
        buffer[i] = c;  
000000013F091062  movzx       ecx,byte ptr [rcx+rdx]  
        buffer[i] = c;  
000000013F091066  mov         byte ptr [rdx+rax],cl  
    for (size_t i = 0 ; i < sBasicString.size() ; ++i)
000000013F091069  inc         rdx  
000000013F09106C  cmp         rdx,qword ptr [rsp+38h]  
000000013F091071  jb          main+51h (013F091051h)  
    }

    std::cout << buffer << std::endl;
000000013F091073  mov         rdx,rax  
000000013F091076  mov         rcx,qword ptr [__imp_std::cout (013F093068h)]  
000000013F09107D  call        std::operator<<<std::char_traits<char> > (013F0910C0h)  
000000013F091082  mov         rcx,rax  
000000013F091085  mov         rdx,qword ptr [__imp_std::endl (013F093060h)]  
000000013F09108C  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (013F093098h)]  
000000013F091092  nop  

    return 0;
000000013F091093  cmp         qword ptr [rsp+40h],10h  
000000013F091099  jb          main+0A5h (013F0910A5h)  
000000013F09109B  mov         rcx,qword ptr [sBasicString]  
000000013F0910A0  call        operator delete (013F091AEAh)  
000000013F0910A5  mov         qword ptr [rsp+40h],0Fh  
000000013F0910AE  mov         qword ptr [rsp+38h],rbx  
000000013F0910B3  mov         byte ptr [sBasicString],0  

    return 0;
000000013F0910B8  xor         eax,eax  
}
000000013F0910BA  add         rsp,50h  
000000013F0910BE  pop         rbx  
000000013F0910BF  ret  

EDIT 3 Adding 32bit disassemble output

int main()
{
013B1000  push        0FFFFFFFFh  
013B1002  push        13B2558h  
013B1007  mov         eax,dword ptr fs:[00000000h]  
013B100D  push        eax  
013B100E  mov         dword ptr fs:[0],esp  
013B1015  sub         esp,18h  
013B1018  push        esi  
    std::basic_string<char> sBasicString = "basic_string";
013B1019  push        0Ch  
013B101B  mov         dword ptr [esp+18h],0  
013B1023  mov         dword ptr [esp+1Ch],0  
013B102B  push        13B3158h  
013B1030  lea         ecx,[esp+0Ch]  
013B1034  mov         dword ptr [esp+20h],0Fh  
013B103C  mov         dword ptr [esp+1Ch],0  
013B1044  mov         byte ptr [esp+0Ch],0  
013B1049  call        std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign (013B17B0h)  
013B104E  mov         dword ptr [esp+24h],0  
    char* buffer = new char[1000];
013B1056  push        3E8h  
013B105B  call        operator new[] (013B1BD6h)  
    for (size_t i = 0 ; i < sBasicString.size() ; ++i)
013B1060  xor         edx,edx  
013B1062  add         esp,4  
013B1065  mov         esi,eax  
013B1067  cmp         dword ptr [esp+14h],edx  
013B106B  jbe         main+8Dh (013B108Dh)  
    for (size_t i = 0 ; i < sBasicString.size() ; ++i)
013B106D  lea         ecx,[ecx]  
    {
        char c;
        c = sBasicString[i];
013B1070  cmp         dword ptr [esp+18h],10h  
013B1075  lea         ecx,[esp+4]  
013B1079  cmovae      ecx,dword ptr [esp+4]  
    for (size_t i = 0 ; i < sBasicString.size() ; ++i)
013B107E  inc         edx  
        buffer[i] = c;  
013B107F  mov         al,byte ptr [ecx+edx-1]  
013B1083  mov         byte ptr [edx+esi-1],al  
013B1087  cmp         edx,dword ptr [esp+14h]  
013B108B  jb          main+70h (013B1070h)  
    }

    std::cout << buffer << std::endl;
013B108D  push        dword ptr ds:[13B3030h]  
013B1093  push        esi  
013B1094  push        dword ptr ds:[13B3034h]  
013B109A  call        std::operator<<<std::char_traits<char> > (013B10F0h)  
013B109F  add         esp,8  
013B10A2  mov         ecx,eax  
013B10A4  call        dword ptr ds:[13B3028h]  

    return 0;
013B10AA  mov         dword ptr [esp+24h],0FFFFFFFFh  
013B10B2  cmp         dword ptr [esp+18h],10h  
013B10B7  pop         esi  
013B10B8  jb          main+0C5h (013B10C5h)  
013B10BA  push        dword ptr [esp]  
013B10BD  call        operator delete (013B1BECh)  
013B10C2  add         esp,4  
}
013B10C5  mov         ecx,dword ptr [esp+18h]  

    return 0;
013B10C9  mov         dword ptr [esp+14h],0Fh  
013B10D1  mov         dword ptr [esp+10h],0  
013B10D9  mov         byte ptr [esp],0  
013B10DD  xor         eax,eax  
}
013B10DF  mov         dword ptr fs:[0],ecx  
013B10E6  add         esp,24h  
013B10E9  ret  

Solution

  • Looking at x86 Assembly posted in this question, I can apply my rudimentary Assembly knowledge to understand, where the buffer variable is hidden:

    char* buffer = new char[1000];
    013B105B  call        operator new[] (013B1BD6h) 
    013B1065  mov         esi,eax
    

    My candidate is esi register: operator new returned the result in eax and it is moved to esi. Let's follow this register:

        for (size_t i = 0 ; i < sBasicString.size() ; ++i)
    013B107E  inc         edx  
        buffer[i] = c;  
    013B107F  mov         al,byte ptr [ecx+edx-1]  
    013B1083  mov         byte ptr [edx+esi-1],al 
    

    The last line places char value al to the buffer. edx is obviously the loop counter, see ind edx. So, esi points to the buffer allocated by operator new. And finally:

    013B1093  push        esi  
    013B1094  push        dword ptr ds:[13B3034h]  
    013B109A  call        std::operator<<<std::char_traits<char> > (013B10F0h)
    

    Here esi is printed. So, the answer to your question: buffer variable is kept in the esi CPU register. You can add the line delete[] buffer; to the program and see, how operator delete is applied to esi in Assembly.

    Since the whole loop doesn't contain function calls that can change CPU registers, optimized code produced by compiler just keeps the buffer in the register. Debugger doesn't know this and cannot display it.

    x64 Assembly works by the same way, but it is more complicated and requires more time to understand. I hope you have an idea now, what happens.