【问题标题】:How to determine if returned pointer is on the stack or heap如何判断返回的指针是在栈上还是堆上
【发布时间】:2013-05-18 13:52:12
【问题描述】:

我有一个插件架构,我在其中调用动态库中的函数,它们返回给我一个 char* 这是答案,它会在稍后阶段使用。

这是插件函数的签名:

char* execute(ALLOCATION_BEHAVIOR* free_returned_value, unsigned int* length);

其中ALLOCATION_BEHAVIOR 必须是:DO_NOT_FREE_MEFREE_MEDELETE_ME 插件(在库中)告诉我插件如何分配它刚刚返回的字符串:DO_NOT_FREE_ME 告诉我,这是我不应该接触的变量(例如永远不会改变的const static char*FREE_ME 告诉我应该使用free() 来释放返回的值,DELETE_ME 告诉我使用delete[] 来获取消除内存泄漏。

显然,我不信任插件,所以我希望能够检查他是否告诉我 free() 变量,确实它是可以真正释放的东西......这可能使用当今 Linux/Windows 上的 C/C++ 技术?

【问题讨论】:

  • 在一个正常设计的软件中,资源管理并不像你解释的那样。应该明确谁以及何时释放资源。
  • 如果您不信任插件,请不要加载它们。
  • @fritzone 没有。显然,插件作者没有告诉你任何事情。这似乎是你的问题。如果他们确实告诉了你一些事情,你就不必自己弄清楚了。你的要求是矛盾的:你希望插件编写者给你这个信息,一旦你得到它,你想丢弃它并自己发现它。下定决心,你的问题就会迎刃而解。
  • 我讨厌这样的设计。我更喜欢不透明/半透明的库/插件返回一个带有虚拟“释放()”方法的对象,以便插件在不再需要时处理删除/重新池/任何数据。插件中的代码可以随心所欲地覆盖“release()”。
  • 另外...插件如何在“堆栈上”分配一些东西并返回一个有效的指针?你支持book thievery插件吗?

标签: c++ c memory


【解决方案1】:

我正在使用以下代码检查学生作业。返回堆栈内存是一个常见的陷阱,所以我想自动检查它。

使用sbrk

此方法应适用于所有 Unix 变体和所有 CPU 架构。

#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

bool points_to_heap(void* init_brk, void* pointer){
    void* cur_brk = sbrk(0);
    return ((init_brk <= pointer) && (pointer <= cur_brk));
}

int main(void){
    void* init_brk = sbrk(0);
    int* heapvar = malloc(10);
    int i = 0;
    int* stackvar = &i;
    assert(points_to_heap(init_brk, heapvar));
    assert(!points_to_heap(init_brk, stackvar));
    return 0;
}

使用/proc/self/maps

这种方法有两个问题:

  • 此代码专用于在 64 位 x86 CPU 上运行的 Linux。
  • 此方法似乎不适用于使用libcheck 框架编写的单元测试。在那里,所有堆栈变量也被视为堆变量。
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

void get_heap_bounds(uint64_t* heap_start, uint64_t* heap_end){
    FILE *stream;
    char *line = NULL;
    size_t len = 0;
    ssize_t nread;

    stream = fopen("/proc/self/maps", "r");

    while ((nread = getline(&line, &len, stream)) != -1) {
        if (strstr(line, "[heap]")){
            sscanf(line, "%" SCNx64 "-%" SCNx64 "", heap_start, heap_end);
            break;
        }
    }

    free(line);
    fclose(stream);
}

bool is_heap_var(void* pointer){
    uint64_t heap_start = 0;
    uint64_t heap_end = 0;
    get_heap_bounds(&heap_start, &heap_end);

    if (pointer >= (void*)heap_start && pointer <= (void*)heap_end){
        return true;
    }
    return false;
}

欢迎反馈此代码!

【讨论】:

    【解决方案2】:

    插件/库/任何东西都不应该通过传递的“ALLOCATION_BEHAVIOR*”指针返回枚举。充其量是混乱的。 'deallocation' 方案属于数据,应与数据一起封装。

    我希望返回某个基类的对象指针,该对象指针具有一个虚拟的“release()”函数成员,主应用程序可以在它想要/需要时调用它,并根据该对象的要求处理“dealloaction”。 release() 什么都不做,将对象重新池化在对象的私有数据 memebr 中指定的缓存中,只是 delete() 它,这取决于插件子类应用的任何覆盖。

    如果这是因为插件是用不同的语言编写的,或者是用不同的编译器构建的,那么插件可以返回一个函数以及数据,以便主应用程序可以使用数据指针调用它用于解除分配的参数。这至少允许您将 char* 和 function* 放入 C++ 端的同一个对象/结构中,因此至少保持一些封装的外观并允许插件选择它想要的任何释放方案。

    编辑 - 如果插件使用与主应用不同的堆,这样的方案也可以安全地工作 - 也许它位于具有自己的子分配器的 DLL 中。

    【讨论】:

      【解决方案3】:

      区分malloc/freenew/delete 通常是不可能的,至少不是以可靠和/或可移植的方式。更何况new 在许多实现中只是简单地包装了malloc

      以下区分堆/堆栈的替代方法均未经过测试,但它们应该都有效。

      Linux:

      1. Luca Tettananti 提出的解决方案,解析/proc/self/maps 得到栈的地址范围。
      2. 作为启动时的第一件事,clone 你的进程,这意味着提供一个堆栈。既然你提供了它,你就会自动知道它在哪里。
      3. 调用 GCC 的 __builtin_frame_address 函数,增加 level 参数直到它返回 0。然后你就知道深度了。现在再次调用 __builtin_frame_address 并使用最高级别,并使用级别 0 调用一次。堆栈中的任何内容都必须位于这两个地址之间。
      4. sbrk(0) 作为启动时的第一件事,并记住该值。每当您想知道堆上是否有东西时,再次sbrk(0) - 堆上的东西必须在两个值之间。请注意,这不适用于使用内存映射进行大分配的分配器。

      知道堆栈的位置和大小(备选方案 1 和 2),确定地址是否在该范围内就很简单了。如果不是,则必然是“堆”(除非有人试图成为超级聪明的人并给你一个指向静态全局的指针,或者一个函数指针,等等......)。

      Windows:

      1. 使用CaptureStackBackTrace,堆栈中的任何内容都必须位于返回的指针数组的第一个和最后一个元素之间。
      2. 如上所述使用 GCC-MinGW(和 __builtin_frame_address,应该可以正常工作)。
      3. 使用GetProcessHeapsHeapWalk 检查每个分配的块是否匹配。如果没有与任何堆匹配,则它因此被分配到堆栈上(......或内存映射,如果有人试图对你超级聪明)。
      4. 使用HeapReAllocHEAP_REALLOC_IN_PLACE_ONLY 并且大小完全相同。如果失败,则不会在堆上分配从给定地址开始的内存块。如果它“成功”了,那就是空操作。
      5. 使用GetCurrentThreadStackLimits(仅限Windows 8 / 2012)
      6. 调用NtCurrentTeb()(或阅读fs:[18h])并使用返回的TEB的StackBaseStackLimit字段。

      【讨论】:

      • 我会接受这个,因为它给了我最接近问题的答案。
      【解决方案4】:

      当它们返回时,它们如何在堆栈上分配一些您可以释放的东西?那只会死得很惨。即使使用它也会死得很惨。

      如果你想检查他们是否返回了指向静态数据的指针,那么你可能想要掌握你的堆顶部和底部(我很确定这在 linux 上可用,使用 sbrk),然后查看返回的指针是否在该范围内。

      当然,有可能即使是该范围内的有效指针也不应该被释放,因为他们已经将另一个副本存储到该范围内,以便稍后使用。如果你不相信他们,你就不应该相信他们。

      【讨论】:

        【解决方案5】:

        您必须使用一些调试工具来确定指针是在堆栈上还是在堆上。 在 Windows 上,下载 Sysinternals Suite。这提供了各种调试工具。

        【讨论】:

          【解决方案6】:

          在 Linux 上,您可以解析 /proc/self/maps 以提取堆栈和堆的位置,然后检查指针是否落入某个范围。

          这不会告诉你内存应该由 free 还是 delete 处理。如果您控制架构,则可以让插件释放分配的内存,添加适当的 API(IOW,一个与您的 execute 对称的 plugin_free 函数)。另一种常见的模式是跟踪上下文对象(在初始化时创建)中的分配,该对象在每次调用时传递给插件,然后在关闭时由插件用于进行清理。

          【讨论】:

            【解决方案7】:

            几年前我在 comp.lang.c 上做过同样的问题,我喜欢 James Kuyper 的回答:

            是的。分配时跟踪它。

            做到这一点的方法是使用内存所有权的概念。在 在分配的内存块的生命周期中的所有时间,您 应该始终只有一个“拥有”该块的指针。 其他指针可能指向该块,但只有拥有指针 应该永远传递给 free()。

            如果可能的话,应该为 拥有指针的目的;它不应该用于存储指向 它不拥有的内存。我通常会尝试安排拥有 指针通过调用 malloc() 进行初始化;如果那不是 可行,首次使用前应设置为 NULL。我也 尝试确保拥有指针的生命周期结束 在我释放()它拥有的内存之后立即。然而,当那 不可能,在释放内存后立即将其设置为 NULL。 有了这些预防措施,你不应该让一个 非空拥有指针结束,而不首先将其传递给 free()。

            如果您无法跟踪“拥有”哪些指针 指针,在他们的声明旁边放一个关于这个事实的评论。如果 你有很多麻烦,使用命名约定来跟踪 这个功能。

            如果由于某种原因无法保留拥有指针 变量专门用于它指向的内存的所有权,你 应该留出一个单独的标志变量来跟踪是否 并不是那个指针当前拥有它指向的内存。创建一个 包含指针和所有权标志的结构是一个非常 处理这个问题的自然方法 - 它确保它们不会分开。

            如果您有一个相当复杂的程序,则可能需要 将内存所有权从一个拥有的指针变量转移到 其他。如果是这样,请确保目标指针拥有的任何内存都是 free()d 在传输之前,除非源的生命周期 指针在传输后立即结束,将源指针设置为 空值。如果您使用所有权标志,请相应地重置它们。

            【讨论】:

              猜你喜欢
              • 2016-11-09
              • 2017-09-18
              • 2011-03-14
              • 2015-07-21
              • 2010-11-06
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多