为什么在使用 gcc-5.2.0 编译程序时 valgrind 没有发现泄漏

Why doesn't valgrind spot the leak when program was compiled with gcc-5.2.0

今天我在写一些东西,完成后,我用 valgrind 进行了检查,我得到了一个惊喜。

如果我在 Ubuntu (15.04 64BIT) 上用 gcc-4.9.2 编译我的程序,如下:

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

然后 运行 valgrind:

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

我得到以下输出:

==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)

如您所见,泄漏已被发现,但看看如果我使用 gcc-5.2.0 编译时会发生什么:

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

现在 valgrind 说:

==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)

如您所见,总堆使用量:0 次分配,0 次释放,0 字节分配

我试过的代码如下:

#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 using this method

现在我的问题是:是 GCC 还是 valgrind 出了问题?为什么会发生这种情况,我该如何避免?

最后一件事,如果我改变:

printf("Bye\n");

对此:

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

发现泄漏:

==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)

这让我问自己为什么? printf() 以某种方式帮助了这个故事。

似乎 GCC 5.2.0 能够通过 strcpy 检测到 string2 是一个常量 "Hello"。所以它只是优化了 string2 而没有在 HEAP 中分配新的内存块。我的猜测是 string.h 在 header 本身中实现了 strcpystrlen

检测内存泄漏的最佳方法是在不进行优化的情况下进行编译。尝试使用 -O0 而不是 -O2 重新编译它。在这种情况下,编译器将创建尽可能接近您的源代码的二进制文件。

With this:

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

The leak is spotted:

这里编译器似乎检测到对 string2 的依赖,所以它没有优化它。 可能是因为 printf 的实现在您的源代码编译时不可用,或者可能是因为 printf 使用可变参数。但这只是我的猜测...

继续我们在 中的评论中的讨论,valgrind 输出中的差异是编译器优化 -O2 优化代码分配的结果。为什么?让我们看看您的代码:

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

虽然您已经为 string2 分配了内存并且您已经将 "Hello" 复制到 sting2,但 您永远不会使用 string2在您的代码的其余部分。由于没有依赖于 string2 指向的内存或其中包含的值的后续操作,因此编译器可以自由地从最终可执行文件中完全删除该代码。

"optimizing" 中,编译器寻找可以使代码 运行 更高效、指令更少,同时仍提供相同功能的方法.由于没有任何东西依赖于与 string2 关联的内存或值,编译器简单地得出结论,如果它完全忽略分配和复制,则代码可以 运行 更快,指令更少。

(这就是为什么当您使用 string2 调用 printf 时出现泄漏,编译器不能简单地优化分配和复制,因为 printf 取决于 string2)

的内存和值

验证正在发生的事情的关键是查看编译器生成的汇编代码gcc -S生成汇编文件,添加选项-masm=intel 告诉编译器以 intel 格式而不是 ATT)

输出程序集

让我们从禁用优化开始 -O0。生成的组件的显着部分是:

    .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

现在,让我们看看优化后的版本(gcc (GCC) 6.1.1 20160602 使用 -Ofast 优化):

    .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

根本没有 malloc -- 它已被优化掉。 优化 应该做到的是,它 运行 的指令要少得多。

现在 valgrind 如何报告它看到的内容在 valgrind 版本和 OS 版本之间会有所不同,但底线是相同的。如果您声明、分配但从不使用任何值,编译器可以自由地优化 declaration/allocation 掉,并且找出正在发生的事情的一种方法是查看汇编文件。如果要重现程序集,使用的完整编译字符串为:

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

那就看看valgrindtest.asm。希望这能为您增加另一块拼图。