为什么valgrind会报告glibc tsearch()随机泄漏内存?

3
我正在使用 glibc tsearch() API 系列来在示例程序中存储动态分配的数据块。
我发现当我使用 tsearch() 将多个 malloc() 的块添加到树中时,valgrind 报告其中一些块为“可能丢失”。虽然“可能丢失”不完全等同于“绝对丢失”,但以前的 SO 建议通常是调查这些问题的原因。
我的示例程序如下:
#include <stdio.h>
#include <search.h>
#include <stdlib.h>
#include <signal.h>

struct data {
    int id;
    char *str;
};

static int
compare (const void *a, const void *b) {
    const struct data *data_a = a, *data_b = b;

    if (data_a->id < data_b->id) {
        return -1;
    } else if (data_a->id > data_b->id) {
        return 1;
    } else {
        return 0;
    }
}

int main (int argc, char **argv) {
    void *tree = NULL;
    struct data *d1, *d2, *d3, *d4;

    d1 = malloc(sizeof(struct data));
    d1->id = 10;
    d1->str = "Hello";
    tsearch(d1, &tree, compare);

    d2 = malloc(sizeof(struct data));
    d2->id = 30;
    d2->str = "Goodbye";
    tsearch(d2, &tree, compare);

    d3 = malloc(sizeof(struct data));
    d3->id = 20;
    d3->str = "Thanks";
    tsearch(d3, &tree, compare);

    d4 = malloc(sizeof(struct data));
    d4->id = 40;
    d4->str = "OK";
    tsearch(d4, &tree, compare);

    raise(SIGINT);

    return 0;
}

请注意,我在main()的末尾调用raise(SIGINT),以便valgrind能够在隐式free()之前分析已分配的块。
我正在以下方式下编译和运行:valgrind
$ gcc ts.c -o ts
$ valgrind --leak-check=full ./ts
==2091== Memcheck, a memory error detector
==2091== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2091== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==2091== Command: ./ts
==2091== 
==2091== 
==2091== Process terminating with default action of signal 2 (SIGINT)
==2091==    at 0x4E7AE97: raise (raise.c:51)
==2091==    by 0x1088CE: main (in /home/ubuntu/ts)
==2091== 
==2091== HEAP SUMMARY:
==2091==     in use at exit: 160 bytes in 8 blocks
==2091==   total heap usage: 8 allocs, 0 frees, 160 bytes allocated
==2091== 
==2091== 24 bytes in 1 blocks are possibly lost in loss record 8 of 8
==2091==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2091==    by 0x4F59483: tsearch (tsearch.c:338)
==2091==    by 0x108801: main (in /home/ubuntu/ts)
==2091== 
==2091== LEAK SUMMARY:
==2091==    definitely lost: 0 bytes in 0 blocks
==2091==    indirectly lost: 0 bytes in 0 blocks
==2091==      possibly lost: 24 bytes in 1 blocks
==2091==    still reachable: 136 bytes in 7 blocks
==2091==         suppressed: 0 bytes in 0 blocks
==2091== Reachable blocks (those to which a pointer was found) are not shown.
==2091== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==2091== 
==2091== For counts of detected and suppressed errors, rerun with: -v
==2091== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

$ 

Valgrind报告有一个24字节的块丢失。如果我将raise(SIGINT)放在d4分配和树添加之前,那么就不会报告任何块丢失。
为什么添加4个块时会丢失一个块,即使添加3个块时没有丢失任何块?

一些libc函数为了提高性能而不费心地释放内存。这意味着要小心保留它们使用的少量内存。Valgrind将此内存报告为泄漏,但它会在应用程序退出时返回。 - Serge
@Serge 一些libc函数为了提高性能而不费心释放内存 <sup>需要引用</sup>。 - Digital Trauma
1个回答

4
这里的 glibc tsearch() 实现有些淘气,可以在二叉搜索树中存储的块的指针的低位比特上进行操作。它使用指针的低位比特来存储标志: https://code.woboq.org/userspace/glibc/misc/tsearch.c.html#341 具体地说,该实现使用以下宏来设置或清除低位指针比特,以将块标记为红色或黑色:
#define SETRED(N) (N)->left_node |= ((uintptr_t) 0x1)
#define SETBLACK(N) (N)->left_node &= ~((uintptr_t) 0x1)

指针被有效地递增,这使得 valgrind 认为存储在 tsearch() 树中的这些有效指针已经 被覆盖并且可能丢失。但是这些并不是丢失的块——它们成功地存储在二叉搜索树中,除了低位比特。当访问树时,tsearch() 将执行必要的比特掩码操作。tsearch() 可以做到这一点,因为 malloc() 的块通常对齐到至少偶数地址。
只有标记为二叉树中的 "红色" 节点的块才设置了此位,因此哪些块是 "可能丢失" 的或者不是完全取决于实现在添加、删除和重新平衡操作期间将块分类为 "红色" 或 "黑色"。
因此,tsearch() 的位操作使得 valgrind 错误地认为这些块已经丢失。在这种情况下,valgrind 报告了错误的结果。

有点奇怪,但我不同意你的措辞。Valgrind 明确表示“可能丢失”,因此只是可能发生,它从未说过一定会发生,所以 Valgrind 在这里并没有错误。 - Stargateur
@Stargateur 好吧,尽管早期的SO建议认为“可能丢失”通常应被视为“已丢失”。 - Digital Trauma
好的,你引用了valgrind文档中提到的问题:“可能丢失或者“可疑””,很明显这种情况确实存在,该实现做了一些非常不安全和完全没有实施行为的事情。你不应该像这样玩弄指针,但他们可以这样做,因为他们是实现者(glibc)。尽管如此,valgrind关于这个指针不再存在的判断是正确的,该指针已经“移动”了一点,因此valgrind报告可能是因为一个指针仍然引用原始地址附近的某些内容。再次重申,这确实是一个问题,所以我不明白为什么要在这里认为valgrind有错误。 - Stargateur
@Stargateur 如果您在这篇文章中发现任何意味着这是一个valgrind bug的暗示,那么那不是我的意图。 - Digital Trauma
1
"可能丢失"不等于"已经丢失"。在C语言中,您可以对指针的表示执行任何可逆变换,甚至加密并上传到云端,稍后再下载和解密,而不会"丢失"所指向的对象。这里唯一的技巧是(实现特定的)知识,即"malloc"返回的指针上"|1"是可逆的。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接