【问题标题】:In C, why is the pointer returned by getenv automatically reclaimed?在C中,为什么getenv返回的指针会自动回收?
【发布时间】:2022-01-21 10:28:46
【问题描述】:

我用下面的代码测试getenv返回的指针,如果不空闲,测试会导致内存泄漏。

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

void demo() {
    const char *home = getenv("HOME");
    printf("%s\n", home);
}

int main() {
    demo();
    return 0;
}

我使用 Valgrind 来检测内存泄漏:

$ gcc main.c -o main
$ valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./main

结果如下:

==134679== Memcheck, a memory error detector
==134679== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==134679== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==134679== Command: ./demo
==134679== 
/home/aszswaz
==134679== 
==134679== HEAP SUMMARY:
==134679==     in use at exit: 0 bytes in 0 blocks
==134679==   total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==134679== 
==134679== All heap blocks were freed -- no leaks are possible
==134679== 
==134679== For lists of detected and suppressed errors, rerun with: -s
==134679== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

表示申请了一块内存,回收了一块内存。为什么getenv得到的指针会被自动回收?

如果我在代码中添加free((void *)home),会不会影响全局环境变量?

【问题讨论】:

  • 为什么你认为从getenv得到的指针实际上指向的是堆上分配的内存?
  • 不要将指针与其指向的内容混淆。指针本身有自动存储,它指向的对象是一个字符串,不是你管理的
  • 这将具体到如何指定getenv(),以及实现者如何选择满足该规范。在 C++ 中,任何具有静态存储持续时间的对象都会在main() 正常返回后被销毁,其构造顺序与它们的构造顺序相反。因此,实现getenv() 以提供您在 C++ 中描述的行为肯定是可行的。 (不对它是否用 C++ 实现发表任何评论 - 只是,任何语言的可行性都表明它在技术上总体上是可行的)。
  • 你永远不应该free 一个由getenv 获得的指针,因为它没有指向malloc 返回的内存。有问题的内存块可能由printf 分配和释放,而不是getenv

标签: c memory-leaks environment-variables valgrind


【解决方案1】:

https://man7.org/linux/man-pages/man3/getenv.3.html 没有提到getenv() 的返回值是在堆上分配的。因此,将其视为argv

虽然我自己很少看到这种情况发生,但在某些实现中,getenv() 可能会在堆上分配内存并返回指向它的指针。因此,最好查看操作系统的手册页。

只有在堆上分配的内存必须是free()d。

而且,通常仅在调用malloc()calloc()realloc()strdup() 等时返回指向分配在堆上的内存的指针。

无论如何:

如果您确实在getenv() 返回的指针上调用free(),如果您的操作系统没有返回指向堆内存的指针,这是未定义的行为。

哇,我差点没看到这个!:
我认为这让你感到困惑:

usage: 1 allocs, 1 frees

printf() 可以在堆上分配并在返回之前释放它。

编辑:

使用gdb,我们可以发现:

#0  __GI___libc_malloc (1024) // and indeed it calls with 1024 bytes
#1  __GI__IO_file_doallocate ()
#2  __GI__IO_doallocbuf ()
#4  _IO_new_file_xsputn ()
#5  _IO_new_file_xsputn ()
#6  __GI__IO_puts
#7  demo ()
#8  main ()

printf() 好像被puts() 代替了,puts() 通过长长的函数调用链调用malloc()

但是,它似乎没有调用free()。我认为它调用了其他一些释放内存的函数。我还在研究。

【讨论】:

  • “getenv() 在堆上分配并在返回之前释放它”,为什么要这样做?你想到了什么样的 getenv 实现?
  • 我的意思是说getenv()可能。只是一个错字。
  • "... 在某些实现中,getenv() 可能会在堆上分配内存并返回指向它的指针。"你有参考吗?我不认为这是正确的,所以我想看看参考。 C 标准中对getenv 的完整描述表明该指针指向一个已经存在于内存中的字符串。
  • @4386427 我觉得还是可以的,linux是一个非常大的开源社区,有很多人修改linux代码让linux在自己的设备上运行。我们无法知道他们将如何修改 linux。
  • @aszswaz getenv 是标准的 C 函数,所以行为必须一致
【解决方案2】:

环境在初始进程堆栈上初始化,就在参数argcargvmain()的上方。

一般情况下,由于环境生命和参数本身就是整个程序生命,所以常见的实现是在栈中压入环境字符串、环境数组、主函数参数字符串和命令行数组,就在压入栈之前。三个变量(历史上main() 有第三个参数environ,它也被传递给main)由于遗留代码的原因,这个环境指针仍然被传递给main。

试试下面的程序:

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

int main(int argc, char **argv, char **environ)
{
    printf("argc = %d\n", argc);
    printf("args:");
    for (int i = 0; i < argc; i++) {
        printf(" [%s]", argv[i]);
    }
    printf("\n\nEnvironment:\n");
    for (char **p = environ; *p; p++) {
        printf("  %s\n", *p);
    }
    return 0;
}

getenv() 返回的不是静态分配的数据,也不是堆中动态分配的数据。它存储在堆栈中(几乎所有类似 POSIX 的操作系统都以相同的方式解决此问题)就在 main() 参数上方(您可以使用调试器检查它,查看字符串的确切位置,我已经完成了也)

所以您的问题与getenv() 无关,它不使用malloc() 将动态分配的字符串返回给您。 您需要在别处寻找 Valgrind 识别为动态的内存(它没有显示任何问题)

认为 valgrind 可以识别您使用malloc()(或其任何朋友)而不是free()d 分配的内存,并且报告说exit() 没有分配内存。所以getenv() 不会分配内存来给你环境内容。

如果您阅读内核如何初始化进程内存的初始堆栈,您会发现非常有趣的事情(从堆栈的最深处到堆栈的顶部列出):

  • 当存在挂起的中断时,有一些固定的机器代码可以正确地从内核模式返回(只有在内核从内核模式切换到用户模式时才会执行中断,因为它们必须在用户模式下执行处理程序代码,而不是在内核中模式,原因很明显)
  • 有一个遗留结构来保存命令行参数并允许内核访问命令参数以使 ps(1) 命令正常工作。这不再是真的,因为它代表了将内核信息放入用户空间的安全漏洞,但结构仍然存在,供遗留代码使用。命令的参数现在在内核空间中,只能通过特殊的系统调用来修改。
  • 所有的字符串都关联到进程的接收环境。
  • 指向环境字符串的指针数组也存储在堆栈中。这包括一个最终的 NULL 指针,以便能够识别数组的结尾。
  • 命令行参数的所有字符串也都存储在那里。
  • 命令行参数的指针数组(包括最后一个NULL指针,不计入argc)。
  • 参数environ 带有指向环境字符串数组的指针。
  • 参数argv 带有指向命令行参数数组的指针。
  • 参数argc带命令行参数个数(包括程序名,但不包括最后一个NULL指针)

【讨论】:

    猜你喜欢
    • 2017-01-08
    • 2012-03-18
    • 2012-10-18
    • 1970-01-01
    • 2022-01-17
    • 2015-05-25
    • 2022-01-02
    • 2022-11-21
    • 2012-10-30
    相关资源
    最近更新 更多