为什么在未释放 malloc() 内存后地址清理器不指示内存泄漏?

Why is address sanitizer not indicating a memory leak after malloc() memory was not freed?

(这段代码不是我写的,是我教授写的...) 我正在看我的教授写的一些代码,除了一件事之外,这对我来说都很有意义。 (因为我们 运行 没时间了,他没有费心释放任何内存),但是,他在地址清理器打开的情况下进行编译。但是当他 运行 代码时,没有显示地址消毒器错误警告?

我们 运行 gcc 9.3 在 Ubuntu 机器上。当我注释掉 add_line 函数时,它会抛出泄漏,仅针对 crnt。我猜 lines 不会引发内存泄漏,因为它是在全局 space 中声明的?但是为什么 crnt 在调用 add_line 函数时不会引发内存泄漏?

(此外,这里是使用的编译标志。 -g -std=c99 -Wall -Wvla -fsanitize=address,undefined)

代码如下:

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

#define DEBUG 1

#define BUFSIZE 8
#define LISTLEN 16

char **lines;
int line_count, line_array_size;

void add_line(char *p)
{
    if (DEBUG) printf("Adding |%s|\n", p);
    if (line_count == line_array_size) {
    line_array_size *= 2;
    lines = realloc(lines, line_array_size * sizeof(char *));
    // TODO: check whether lines is NULL
    }

    lines[line_count] = p;
    line_count++;
}

int main(int argc, char **argv)
{
    int fd, bytes;
    char buf[BUFSIZE];
    char *crnt;
    int len;
    int pos, start;

    // TODO: move array list management to separate functions
    lines = malloc(sizeof(char *) * LISTLEN);
    if (!lines) {
    printf("malloc failed\n");
    return EXIT_FAILURE;
    }

    line_array_size = LISTLEN;
    line_count = 0;

    if (argc > 1) {
    fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror(argv[1]);
        return EXIT_FAILURE;
    }
    } else {
    fd = 0;
    }

    crnt = NULL;
    len = 0;
    while ((bytes = read(fd, buf, BUFSIZE)) > 0) {
    // read buffer and break file into lines

    start = 0;
    for (pos = 0; pos < bytes; pos++) {
        if (buf[pos] == '\n') {
        if (crnt == NULL) {
            len = pos - start;
            crnt = malloc(len + 1);
            memcpy(crnt, &buf[start], len);
        } else {
            len += pos;
            crnt = realloc(crnt, len + 1);
            memcpy(&crnt[len - pos], buf, pos);
        }
        crnt[len] = '[=10=]';
        // add_line(crnt); <------------- When I uncomment this line, no address-sanitizer leak is detected. With this line commented, asan does throw a leak only for the crnt variable. Why is that?
        crnt = NULL;
        start = pos + 1;
        }
    }

    if (start < pos) {
        if (crnt == NULL) {
        len = pos - start;
        crnt = malloc(len + 1);
        memcpy(crnt, &buf[start], len);
        } else {
        int newlen = len + (pos - start);
        crnt = realloc(crnt, newlen + 1);
        memcpy(&crnt[len], &buf[start], pos - start);
        len = newlen;
        }
        crnt[len] = '[=10=]';  // technically unnecessary
    }
    }
    if (bytes == -1) {
    perror("read");
    return EXIT_FAILURE;
    }

    // if we reach here, we have read the entire file
    // sort and print the list
    

    return 0;
}

这里的问题是“内存泄漏”的定义。我本来想引用 LeakSanitizer 文档中的一个部分,其中提供了对该概念的清晰和精确的定义,这似乎是其操作的基础,但我找不到,所以你必须忍受一点我的投影。

一个动态分配的区域(即与 malloc 或朋友一起)的内存已经泄漏,但不可能 freed。换句话说,如果您的程序分配内存并在分配释放之前丢弃地址,则内存已泄漏。

这与您可能认为的定义略有不同。如果您的程序在没有释放它分配的每个内存块的情况下终止,您可能会认为内存已泄漏。这当然是一个可能的定义,我不会批评它(太多),但它实际上不是很精确。

程序什么时候终止的?它在 main() returns 时并没有真正终止,因为您可能仍然有 clean-up 函数注册到 atexit(),并且这些函数直到 才执行之后 main() returns。 (或者当 exit() 被调用时,这实际上是一回事。)为了 free() 对象而精确地使用 atexit() 函数实际上是很常见的(尽管在我看来,这是毫无意义的)在 exit().

之前可能没有被释放

OK,当main returns时,你不能通过检查它是否已经freed来检查内存分配是否已经被释放。如果你想那样做,你需要将测试推迟到最后可能的时刻。但在真正可能的最后时刻,进程将不复存在,操作系统将回收进程使用的所有内存,包括内存分配库获取的所有资源。所以在最后一刻,没有内存泄漏,因为没有内存。

(有些嵌入式系统没有独立进程内存等概念,所以我在那里写的内容可能不适用于 所有可能的计算系统。但它适用于实施 AddressSanitizer 的所有内容。)

一个关键点是 atexit() 处理程序需要能够找到它正在清理的对象,并且由于它在 main() 终止后执行,它不能使用任何自动(即stack-allocated) 对象。它只能使用具有静态生命周期的对象。因此,为了能够完成它的任务,终止时要清理的对象的地址必须存储在全局内存中。如果该区域的内存没有持久存储在某处,则内存已经泄漏(根据我上面的定义),我们实际上不必等待 atexit 是否设法 free 内存。

这让我们回到我声称的内存泄漏的可行定义:动态分配的内存,其地址不再存在于可执行文件中。该内存区域不能再使用,所以它是垃圾,但它不能 freed 因为程序不知道它的地址是什么。

您的 lines 数组是一个全局变量。确实,您在问题中指出了这一点:

I guess lines does not throw a memory leak because it was declared in the global space?

没错。 lines 是一个全局变量,所以它的内容即使在 main() returns 之后仍然可以访问。不仅它的内容是可访问的,它指向的数组中的某个对象指向的任何内存也是如此。如果你愿意,你可以 freeatexit 处理程序中保存的行:

void cleanup(void) {
  for (int i = 0; i < line_count; ++i) { free(lines[i]); }
  free(lines);
}

(要使用它,您只需在初始化 linesline_count 之后调用 atexit(cleanup)。)

所以这将我们带到:

But why doesn't crnt throw a memory leak when the add_line function is called?

crnt 包含包含当前行的 dynamically-allocated 缓冲区的地址。如果您调用 add_line(crnt),该指针将存储在 lines 中。所以它可用于 clean-up 函数,如上所述。您可以在方便时将 crnt 设置为 NULL,因为它不再是指向该缓冲区的唯一指针。

但是如果你不调用 add_line,那么 crnt 唯一指向那个缓冲区的指针,当你设置 crntNULL,不再有指向缓冲区的指针。缓冲区已泄漏,AddressSanitizer 会告诉您这件事。 (即使您没有将 crnt 设置为 NULL,AddressSanitizer 也会发现问题,因为 crntmain() returns 或调用 exit(),此时地址已经丢失。或者如果你用不同的分配地址覆盖 crnt。)

对于一个更简单的例子,试试这两个非常相似的程序:

内存泄漏

#include <stdlib.h>
int main(void) {
  void* megabyte = malloc(1<<20);
  (void)megabyte; /* Suppress unused variable warning */
}

没有内存泄漏

#include <stdlib.h>
void* megabyte;
int main(void) {
  megabyte = malloc(1<<20);
}

请注意,Valgrind 内存检查工具 可以 报告内存,如第二个示例中的 megabyte,它永远不会 freed,即使它是在 Valgrind 认为执行结束时仍然可以访问。但默认情况下它不会这样做。如果你 运行 Valgrind 在第二个 prgram 带有标志 --show-leak-kinds=all --leak-check=full,它将报告 1 兆字节的内存“仍然可以访问”。 (要尝试 valgrind,我相信你必须在没有 AddressSanitizer 的情况下编译程序。这两个工具并不完全兼容。)