首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么地址清除器在malloc()内存未被释放后没有指示内存泄漏?

为什么地址清除器在malloc()内存未被释放后没有指示内存泄漏?
EN

Stack Overflow用户
提问于 2022-03-10 04:22:29
回答 1查看 558关注 0票数 1

(这段代码不是我写的,是我的教授写的。)我在看我的教授写的一些代码,除了一件事之外,这对我来说都是有意义的。(因为我们的时间不多了,他根本不想释放任何内存),然而,他正在用地址杀菌剂进行编译。但是当他运行代码时,没有显示地址消毒程序错误警告?

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

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

以下是代码:

代码语言:javascript
复制
#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] = '\0';
        // 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] = '\0';  // 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;
}
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-03-10 06:15:09

这里的问题是“内存泄漏”的定义。我本想引用LeakSanitizer文档中的一个章节,其中提供了对这个概念的清晰而精确的定义,这似乎是它的操作的基础,但我找不到,所以您将不得不忍受一些我的预测。

动态分配的内存区域(即与malloc或朋友一起)在没有可能成为freed的情况下泄漏。换句话说,如果程序在分配空闲之前分配内存并丢弃地址,内存就泄漏了。

这与你可能认为的定义有很大的不同。您可能会认为,如果程序终止而不释放所分配的每一个内存块,内存就会泄漏。这当然是一个可能的定义,我不会批评它(很多),但实际上并不是很精确。

什么时候程序结束了?当main()返回时,它并没有真正终止,因为您仍然可以在atexit()中注册清理函数,并且这些函数直到main()返回之后才会执行。(或者当exit()被调用时,这实际上是一回事。)在我看来,准确地使用atexit()函数是非常普遍的(尽管在我看来是毫无意义的),因为free()对象在exit()之前可能还没有被释放。

好的,您不能通过检查当free返回时内存分配是否是main d来检查内存分配是否已被释放。如果你想这样做,你需要把测试推迟到真正可能的最后一刻。但在最后一个可能的时刻,进程即将不复存在,操作系统将回收进程使用的所有内存,包括内存分配库获得的任何资源。所以在最后一个可能的时刻,没有内存泄漏,因为没有内存。

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

一个关键问题是,atexit()处理程序需要能够找到它正在清理的对象,而且由于它在main()结束后执行,所以它不能使用任何自动(即堆栈分配)对象。只有具有静态生存期的对象才对其可用。因此,要使它能够执行其任务,在终止时要清理的对象的地址必须存储在全局内存中。如果该区域的内存没有存储在某个持久的位置,则内存已经泄漏(根据我上面的定义),我们实际上不必等待atexit是否能够管理内存。

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

lines数组是一个全局变量。事实上,你在问题中指出了这一点:

,我想lines不会抛出内存泄漏,因为它是在全局空间中声明的?

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

代码语言:javascript
复制
void cleanup(void) {
  for (int i = 0; i < line_count; ++i) { free(lines[i]); }
  free(lines);
}

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

因此,我们需要:

,但是当调用add_line函数时,为什么crnt不抛出内存泄漏呢?

crnt包含动态分配缓冲区的地址,该缓冲区包含当前行。如果调用add_line(crnt),则该指针存储在lines中。因此,它可以用于清理功能,如上面所示。您可以在方便的情况下将crnt设置为NULL,因为它不再是指向该缓冲区的唯一指针。

但是,如果不调用add_line,那么crnt是指向该缓冲区的唯一指针,当您将crnt设置为NULL时,就不再有指向缓冲区的指针了。缓冲区泄漏了,AddressSanitizer在那里告诉您。(即使您没有将AddressSanitizer设置为NULLcrnt也会遇到这个问题,因为当main()返回或调用exit()时,crnt就不再存在了,此时地址已经丢失。或者如果您用不同的分配地址覆盖crnt。)

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

内存泄漏

代码语言:javascript
复制
#include <stdlib.h>
int main(void) {
  void* megabyte = malloc(1<<20);
  (void)megabyte; /* Suppress unused variable warning */
}

无内存泄漏

代码语言:javascript
复制
#include <stdlib.h>
void* megabyte;
int main(void) {
  megabyte = malloc(1<<20);
}

请注意,Valgrind工具可以报告内存,就像第二个示例中的megabyte一样,它永远不是freed,尽管它仍然可以在free认为的执行结束时到达。但在默认情况下,它不会这么做。如果您在第二个程序上运行--show-leak-kinds=all --leak-check=full标志,它将报告一个兆字节的内存“仍然可以访问”。(我认为,要想尝试val差制,就必须在没有AddressSanitizer的情况下编译程序。这两个工具并不完全兼容。)

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71418808

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档