Method of Detecting Memory Leaks under Windows

       I have been working for some years, and I have been using C++ to make programs. I have done a lot of programs and changed a lot of bugs. Among them, the tracking, debugging and modification of memory leaks is one of the most time-consuming tasks. Over the years, I have accumulated some experience, and I will record it here as a summary.

        Most of the current high-level languages ​​have garbage collection mechanisms. Unless the language itself is defective, it generally does not encounter the problem of memory leaks. But C/C++ is different. The resources you apply for must be released by yourself. Otherwise, it will slowly erode your program like a chronic disease, inadvertently give your program a fatal blow, and then disappear silently, leaving you at a loss. measures. If you are in charge of the program from the beginning to the end, it is okay to solve it; if there is a problem with the program that you took over from others, it will be physically and mentally exhausting to solve it, and I don’t know how much hair will fall out.

        But the problem has to be solved, and only after the problem is solved can we sleep well and eat well. So how can we solve problems like memory leaks?

The first method: code backtracking

       This is the easiest and most time-saving way to do it. Sometimes, there is no problem with the code a few days ago, but a problem suddenly occurs today, and the workload of debugging is relatively large, so this method can be used. In the version control system, from the time of the non-problematic version to today, use the dichotomy method to take the historical code for debugging, find the time point of the problem, and then further debug. In fact, not only memory leaks, but also troublesome crashes and other problems can be solved by these methods. In this way, I still solved several seemingly troublesome problems, especially taking over other people's code.

The second method: log

        This is the most commonly used method by programmers. No matter what the problem is, you can use the method of logging to locate the problem, especially server programs and programs that need to run for a long time. Although the problem cannot be accurately located, it can narrow the scope of the analysis problem. In the log, the memory usage can be printed out regularly, in case of a memory leak, there are still some traces that can be queried.

The third method: code debugging

In VC, in Debug mode, you can add the following code at the program entry

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)

In this way, when the program exits, the leaked code can be printed out, see the following code

int main()
{
    #ifdef _DEBUG
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    #endif
    char* p = new char[1000];
    char* q = new char[500];
    return 0;
}

After debugging, Output will output:

One is 1000 and the other is 500. But this only knows that there is a leak, not where the leak is. If the program is more complicated, you have to analyze it slowly.

       We can rewrite new:

#ifdef _DEBUG
#define MY_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#else
#define MY_NEW new
#endif
 
 
int main()
{
    #ifdef _DEBUG
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    #endif
    char* p = MY_NEW char[1000];
    char* q = MY_NEW char[500];
    return 0;
}

This way you know where the allocation is not released

        But this is only suitable for projects that I have participated in from beginning to end, and I am the main developer. If it is taken over halfway, the workload of adjustment is quite large. But the project has iterations, and you are not the only one in the project team. If someone does not follow your requirements, there is still a risk of leakage.

Finally, zoom in. Use WinDbg to debug

       Next, I will focus on how to locate memory leaks through WinDbg.

       WinDbg is a very powerful debugging tool under the Windows platform. It can not only debug programs in user mode, but also debug in kernel mode. I won’t introduce this tool too much here, and there are many online tutorials. Now we only discuss how to use it to detect memory leaks (using my usual practice).

        After installing WinDbg, we need to do some simple settings before we can start our follow-up work.

        1. Since we want to use the WinDbg extension command !heap, and this command needs to download the symbols of ntdll.dll and kernel32.dll, we must configure the symbol server of Windows. We have to add an environment variable:

The variable name must be _NT_SYMBOL_PATH_,

The variable value is SRV*f:\symbols* http://msdl.microsoft.com/download/symbols, where f:\symbols is the path where I put the symbol. Of course, you can also use the command to load the symbol server in WinDbg. I don't think it is convenient, so I won't introduce it here. However, after I use the new version of WinDbg, it seems that I don’t need to set it, but in order to respect the previous habits, let’s add it.

        2. Next, we need to monitor our program memory allocation. In the WinDbg directory, there is a gflags.exe program, we use the command line to set the program

Gflags.exe /i F:\Test\Debug\Test.exe +ust

Among them, /i indicates which file to specify. For example, if my program is placed in the F:\Test\Debug directory, it is specified as F:\Test\Debug\Test.exe. +ust means to create a user-mode stack trace database. After execution, the command line prompts:

Indicates success. For more settings of gflags, you can enter gflags.exe /? on the command line to check.

(Special reminder, after the memory monitoring is turned on, the program will take up a lot of memory, so you must turn off the monitoring after the debugging is completed, the command is also very simple, Gflags.exe /i F:\Test\Debug\Test.exe -ust, + ust can be changed to -ust)

        Ok, now you can adjust the program.

        First, we open WinDbg, then open our program through the File->Open Executable menu. If our program is running, use File->Attach to a process to mount our program.

         Next, load the symbol of Test.exe (our own program) using the menu File->Symbol file path. After our program is compiled in Debug mode, a PDB file will be generated, which contains the debugging symbols of our program. After loading the symbols of the program, we can set breakpoints on the program, execute single-step operations, etc. Otherwise, we can only go to the assembly code to find a location to set a breakpoint or add a break in the program.

 Symbol paths are separated by a semicolon ";", here my path is (srv*;C:\Users\zy099\source\repos\Test\x64\Debug) and click OK. Then at the WinDbg command line enter

.reload /f

Load the symbols and wait for the symbols to be loaded. It can also be operated on the WinDbg command line, but my memory is not very good, and I am too lazy to check, so I will use the simplest way.

Since the symbol server is abroad, the download in China will be slower, so you need to wait patiently for a while....

When the WinDbg command line can be entered, it means that the symbol has been downloaded successfully. At this time, use the "lm" command to check the symbol loading status:

Here, please be sure to pay attention to the fact that ntdll, KERNEL32 and our Test program symbols are not loaded. If the above prompt is followed, it means that the symbols are loaded successfully. ntdll and KERNEL32 mainly need to use extended commands, and Test is used to debug programs.

        Next, you can use the menu File->Open Source file to open the source file to be debugged. Generally, if the symbol is loaded correctly and there is an interruption in the code, it will automatically jump to the source code. Here we open it manually.

If the symbol of Test.exe is successfully loaded, you can press F9 to set a breakpoint at the specified location. Here I set a breakpoint at the return of main

        After that, it is time to check the leak point of the program. Here, we will use the extended command of WinDbg! If I have time, I can also make another introduction.

code show as below:

class Bad
{
public:
    void AllocMemory()
    {
        for (auto i = 0; i < 100; ++i)
        {
            char* p = new char[5000];
        }
    }
};
 
int main()
{
    Bad b;
    b.AllocMemory();
    return 0;
}

It's very simple, you can see at a glance where there is a memory leak, now let's see how WinDbg finds it

Before the program is executed, let's take a look at the heap.

Enter !heap -s at the WinDbg command line to display summary information for all heaps:

0:000> !heap -s
       Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
       NtGlobalFlag enables following debugging aids for new heaps:
       stack back traces
       LFH Key                   : 0xe48d63c61a6de263
       Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                            (k)     (k)    (k)     (k) length      blocks cont. heap 
-------------------------------------------------------------------------------------
000001e134530000 08000002    1220     60   1020      2     2     1    0      0   LFH
000001e134500000 08008000      64      4     64      2     1     1    0      0      
-------------------------------------------------------------------------------------

Then press F5 to execute the program, and stop after hitting the breakpoint. Look at the heap information again:

0:000> !heap -s
        Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
        NtGlobalFlag enables following debugging aids for new heaps:
        stack back traces
        LFH Key                   : 0xe48d63c61a6de263
        Termination on corruption : ENABLED
          Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                            (k)     (k)    (k)     (k) length      blocks cont. heap 
-------------------------------------------------------------------------------------
000001e134530000 08000002    1220    652   1020     24     8     1    0      0   LFH
000001e134500000 08008000      64      4     64      2     1     1    0      0      
-------------------------------------------------------------------------------------

Here we can see that the heap at address 0x000001e134530000 has grown significantly. Commit was 60K before, and now it is 652K

       Then, we use the command !heap -stat -h 000001e134530000 to view, where the parameter -stat means to display the usage statistics of the specified heap, and -h specifies the heap address to be viewed, here is 0x000001e134530000

0:000> !heap -stat -h 000001e134530000
     heap @ 000001e134530000
 group-by: TOTSIZE max-display: 20
    size     #blocks     total     ( %) (percent of total busy bytes)
    13bc 64 - 7b570  (90.02)
    1cf0 1 - 1cf0  (1.32)
    30 8d - 1a70  (1.21)
    1234 1 - 1234  (0.83)
    1034 1 - 1034  (0.74)
    df4 1 - df4  (0.64)
    400 2 - 800  (0.36)
    100 8 - 800  (0.36)
    7c4 1 - 7c4  (0.35)
    7a2 1 - 7a2  (0.35)
    138 6 - 750  (0.33)
    390 2 - 720  (0.33)
    695 1 - 695  (0.30)
    628 1 - 628  (0.28)
    1d8 3 - 588  (0.25)
    25c 2 - 4b8  (0.22)
    470 1 - 470  (0.20)
    168 2 - 2d0  (0.13)
    50 8 - 280  (0.11)
    238 1 - 238  (0.10)

We see that there are 0x64 blocks of size 0x13bc, with a total size of 0x7B570, accounting for 90.02% of the entire block in use. We suspect that these blocks are the leaked blocks.

       Next we get the addresses of these blocks. Use the command !heap -flt s 13bc. Among them, -flt limits the display range to the heap of the specified size or size range, and the parameter s 13bc is the block with the specified size of 0x13bc.

0:000> !heap -flt s 13bc
    _HEAP @ 1e134530000
              HEAP_ENTRY Size Prev Flags            UserPtr UserSize - state
        000001e134546ce0 013f 0000  [00]   000001e134546d10    013bc - (busy)
          unknown!noop
        000001e1345480d0 013f 013f  [00]   000001e134548100    013bc - (busy)
        000001e1345494c0 013f 013f  [00]   000001e1345494f0    013bc - (busy)
        000001e13454a8b0 013f 013f  [00]   000001e13454a8e0    013bc - (busy)
        000001e13454bca0 013f 013f  [00]   000001e13454bcd0    013bc - (busy)
        000001e13454d090 013f 013f  [00]   000001e13454d0c0    013bc - (busy)
        000001e13454e480 013f 013f  [00]   000001e13454e4b0    013bc - (busy)
        000001e13454f870 013f 013f  [00]   000001e13454f8a0    013bc - (busy)
        000001e134550c60 013f 013f  [00]   000001e134550c90    013bc - (busy)
        000001e134552050 013f 013f  [00]   000001e134552080    013bc - (busy)
          unknown!noop
        000001e134553440 013f 013f  [00]   000001e134553470    013bc - (busy)
        000001e134554830 013f 013f  [00]   000001e134554860    013bc - (busy)
          unknown!printable
        000001e134555c20 013f 013f  [00]   000001e134555c50    013bc - (busy)
          unknown!printable

Here I only intercepted part of the data, in fact, it is quite long here. Here we will see a lot of heap blocks in the busy state, and these heap blocks should be the memory space that has not been released.

       We use !heap -p -a 000001e134546ce0 to output its call stack:

0:000> !heap -p -a 000001e134546ce0 
    address 000001e134546ce0 found in
    _HEAP @ 1e134530000
              HEAP_ENTRY Size Prev Flags            UserPtr UserSize - state
        000001e134546ce0 013f 0000  [00]   000001e134546d10    013bc - (busy)
          unknown!noop
        7ff9c9d3d6c3 ntdll!RtlpAllocateHeapInternal+0x00000000000947d3
        7ff9730dd480 ucrtbased!heap_alloc_dbg_internal+0x0000000000000210
        7ff9730dd20d ucrtbased!heap_alloc_dbg+0x000000000000004d
        7ff9730e037f ucrtbased!_malloc_dbg+0x000000000000002f
        7ff9730e0dee ucrtbased!malloc+0x000000000000001e
        7ff60b1c1f73 Test!operator new+0x0000000000000013
        7ff60b1c19f3 Test!operator new[]+0x0000000000000013
        7ff60b1c1e10 Test!Bad::AllocMemory+0x0000000000000040
        7ff60b1c4746 Test!main+0x0000000000000046
        7ff60b1c1eb9 Test!invoke_main+0x0000000000000039
        7ff60b1c1d5e Test!__scrt_common_main_seh+0x000000000000012e
        7ff60b1c1c1e Test!__scrt_common_main+0x000000000000000e
        7ff60b1c1f4e Test!mainCRTStartup+0x000000000000000e
        7ff9c83354e0 KERNEL32!BaseThreadInitThunk+0x0000000000000010
        7ff9c9c8485b ntdll!RtlUserThreadStart+0x000000000000002b

Here, we see the call stack of this heap, Test!Bad::AllocMemory, it is indeed the memory space that we allocated but not released. This is the stack information allocated by this heap block. Through this information, we can locate where this block of memory is allocated, and then analyze it in the corresponding function.

        In the real project, the situation is far from this simple. Sometimes, the printed heap information has a long list, so you need to find useful information in these information. Debugging can be a headache, but once the hard bones are solved, there is still a sense of accomplishment.

write at the end

It seems that it is really not easy for C++ programmers to achieve no surprises in memory usage. But we use some methods to reduce the risk of memory leaks, such as using C++ smart pointers, using memory pool technology to manage memory uniformly, and so on.

Guess you like

Origin blog.csdn.net/chenlycly/article/details/131578494