Why doesn't valgrind spot the leak when progra

2019-01-26 21:55发布

问题:

Today I was coding something and after I was done, I made a check with valgrind and I got a surprise.

If I compile my program on my Ubuntu (15.04 64BIT) with gcc-4.9.2 with the following:

gcc -Wextra -Werror -Wstrict-prototypes -Wconversion --std=c11 -O2 -g program.c -o program

And then run valgrind:

valgrind --leak-check=full --track-origins=yes ./program

I get the following output:

==5325== Memcheck, a memory error detector
==5325== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==5325== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==5325== Command: ./program
==5325== 
Bye
==5325== 
==5325== HEAP SUMMARY:
==5325==     in use at exit: 33 bytes in 1 blocks
==5325==   total heap usage: 1 allocs, 0 frees, 33 bytes allocated
==5325== 
==5325== 33 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5325==    at 0x4C2BBA0: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5325==    by 0x4004BD: main (program.c:11)
==5325== 
==5325== LEAK SUMMARY:
==5325==    definitely lost: 33 bytes in 1 blocks
==5325==    indirectly lost: 0 bytes in 0 blocks
==5325==      possibly lost: 0 bytes in 0 blocks
==5325==    still reachable: 0 bytes in 0 blocks
==5325==         suppressed: 0 bytes in 0 blocks
==5325== 
==5325== For counts of detected and suppressed errors, rerun with: -v
==5325== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

As you can see the leak is spotted, but take a look of what happens if I compile with gcc-5.2.0 with the following:

./install/gcc-5.2.0/bin/gcc5.2 -Wextra -Werror -Wstrict-prototypes -Wconversion --std=c11 -O2 -g program.c -o program

And now valgrind says:

==5344== Memcheck, a memory error detector
==5344== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==5344== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==5344== Command: ./program
==5344== 
Bye
==5344== 
==5344== HEAP SUMMARY:
==5344==     in use at exit: 0 bytes in 0 blocks
==5344==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==5344== 
==5344== All heap blocks were freed -- no leaks are possible
==5344== 
==5344== For counts of detected and suppressed errors, rerun with: -v
==5344== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

As you can see there is total heap usage: 0 allocs, 0 frees, 0 bytes allocated

The piece of code I tried was the following:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main(void){
    int a = 0;
    size_t len1 = 0, len2 = 0;
    char *string1 = "Hello";
    char *string2;

    string2 = malloc(33);
    strcpy(string2, "Hello");

    len1 = strlen(string1);
    len2 = strlen(string2);

    if(len1 != len2){
        a = 5;
    }else{
        a=4;
    }

    while (a != -1){
        if(a == 2){
            break;
        }
        a--;
    }


    printf("Bye\n");
    /*free(string2);*/
    return 0;
}

GCC-5.2.0 was installed using this method.

Now my question is: is it GCC or valgrind at fault? Why does this happen and how can I avoid it?

One last thing, if I change:

printf("Bye\n");

to this:

printf("String2 = %s\n",string2);

The leak is spotted:

==5443== Memcheck, a memory error detector
==5443== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==5443== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==5443== Command: ./program
==5443== 
String2 = Hello
==5443== 
==5443== HEAP SUMMARY:
==5443==     in use at exit: 33 bytes in 1 blocks
==5443==   total heap usage: 1 allocs, 0 frees, 33 bytes allocated
==5443== 
==5443== 33 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5443==    at 0x4C2BBA0: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5443==    by 0x40044D: main (program.c:11)
==5443== 
==5443== LEAK SUMMARY:
==5443==    definitely lost: 33 bytes in 1 blocks
==5443==    indirectly lost: 0 bytes in 0 blocks
==5443==      possibly lost: 0 bytes in 0 blocks
==5443==    still reachable: 0 bytes in 0 blocks
==5443==         suppressed: 0 bytes in 0 blocks
==5443== 
==5443== For counts of detected and suppressed errors, rerun with: -v
==5443== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Which makes me ask myself why? Somehow printf() helps in this story.

回答1:

Seems that GCC 5.2.0 is able to detect that string2 is a constant "Hello" through the strcpy. So it just optimizes out string2 without allocating new memory chunk in the HEAP. My guess would be that string.h has the implementation of strcpy and strlen in the header itself.

The best way to detect memory leaks is to compile without optimizations. Try recompiling it with -O0 instead of -O2. In this case the compiler will create the binary as close to your source code as possible.

With this:

printf("String2 = %s\n",string2);

The leak is spotted:

Here it seems that the compiler detects dependency on string2 so it doesn't optimize it out. Probably because the implementation of printf is not available at the compilation time of your source or maybe because printf uses variadic variable. But it is just my guess...



回答2:

Continuing our discussion from the comments in Will C automatically free memory with no pointers?, the difference in the valgrind output is the result of the compiler optimization -O2 optimizing the allocation out of your code. Why? Let's look at your code:

string2 = malloc(33);
strcpy (string2, "Hello");
...
printf("Bye\n");

While you have allocated memory for string2 and you have copied "Hello" to sting2, you never use string2 in the remainder of your code. Since there is no subsequent operation that relies on the memory pointed to by string2 or the value contained within it, the compiler is free to delete that code entirely from the final executable.

In "optimizing", the compiler looks for ways it can make the code run more efficiently, with fewer instructions, while still providing the same functionality. Since nothing relies on the memory or value associated with string2, the compiler simply concludes the code can run faster and in fewer instructions if it just ignores the allocation and copy completely.

(that is why as suggested in the other answer when you call printf using string2 the leak appears, the compiler cannot simply optimize the allocation and copy away, because the printf depends on the memory and value of string2)

The key to verifying what is taking place is to look at the assembly code produced by the compiler (gcc -S produces the assembly file, add the option -masm=intel to tell the compiler to output the assembly in intel format instead of ATT)

Let's start with optimizations disabled -O0. The salient part of the assembly produced is:

    .cfi_startproc
    push    rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    mov     rbp, rsp
    .cfi_def_cfa_register 6
    sub     rsp, 48
    mov     DWORD PTR [rbp-4], 0
    mov     QWORD PTR [rbp-16], 0
    mov     QWORD PTR [rbp-24], 0
    mov     QWORD PTR [rbp-32], OFFSET FLAT:.LC0
    mov     QWORD PTR [rbp-40], 0
    mov     edi, 33
    call    malloc                  ; the call to malloc is retained
    mov     QWORD PTR [rbp-40], rax
    mov     rax, QWORD PTR [rbp-40]
    mov     DWORD PTR [rax], 1819043144
    mov     WORD PTR [rax+4], 111
    mov     rax, QWORD PTR [rbp-32]
    mov     rdi, rax
    call    strlen
    mov     QWORD PTR [rbp-16], rax
    mov     rax, QWORD PTR [rbp-40]
    mov     rdi, rax
    call    strlen
    mov     QWORD PTR [rbp-24], rax
    mov     rax, QWORD PTR [rbp-16]
    cmp     rax, QWORD PTR [rbp-24]
    je      .L2
    mov     DWORD PTR [rbp-4], 5
    jmp     .L4

Now, let's look at the optimized version (with gcc (GCC) 6.1.1 20160602 using the -Ofast optimization):

    .cfi_startproc
    sub     rsp, 8
    .cfi_def_cfa_offset 16
    mov     edi, OFFSET FLAT:.LC0
    call    puts
    xor     eax, eax
    add     rsp, 8
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

There is no malloc at all -- it has been optimized away. And true to what optimization should do, it runs in much fewer instructions.

Now how valgrind reports what it sees will differ between valgrind versions and differ between OS's, but the bottom line is the same. If you declare, allocate, but never use any value, the compiler is free to optimize that declaration/allocation away, and one way to find out just what is happening, is to look at the assembly file. If you want to reproduce the assembly, the complete compile string used was:

gcc -S -masm=intel -Ofast -o valgrindtest.asm valgrindtest.c

Then just look at valgrindtest.asm. Hopefully this adds another piece of the puzzle for you.