检查指针是否分配了内存

88

在C语言中,我们可以检查指针是否已分配内存并传递给函数吗?

我编写了一个C函数,接受一个字符指针 - buf [指向缓冲区的指针] 和大小 - buf_siz [缓冲区大小]。实际上,在调用此函数之前,用户必须创建一个缓冲区并分配 buf_siz 的内存。

由于存在用户可能忘记进行内存分配而仅简单地将指针传递给我的函数的情况,因此我想检查这一点。那么,有没有办法在我的函数中检查传递的指针是否真正已分配了 buf_siz 数量的内存呢?

编辑1:似乎没有标准库来检查它...但是有没有什么小技巧可以检查呢...??

编辑2:虽然我知道我的函数将由优秀的C程序员使用...但我想知道是否可以检查...如果可以,我想听听它...

结论:在函数内部检测指针是否已经分配了内存是不可能的。


3
我不太认同,但我不够自信发布成回答。 - Ibrahim
除非你使用内存管理器或自己编写代码,否则无法进行检查。 - Michael Foukarakis
如果它是字符指针,我们可以使用strlen()或sizeof()函数来检查分配了多少内存(当然,如果字符串以NULL结尾)。对于其他类型,我不确定是否有某种方法。! - Sandeep
我知道这是一个老问题,但是可以在不使用hack的情况下跟踪分配的内存。我的代码提供了一些片段,让您开始。 - c1moore
1
应得出的结论是,即使可能,也不应该检查。这篇文章解释了这个问题。虽然是用Windows术语编写的,但这个问题并不特定于Windows。 - ikegami
显示剩余3条评论
19个回答

44

除非使用一些特定于实现的技巧,否则无法检查。

指针除了指向的位置外没有任何信息。你最好的做法是说:“我知道这个特定编译器版本如何分配内存,所以我将取消引用内存,将指针向后移动4个字节,检查大小,确保匹配…”等等。你不能以标准化的方式来做到这一点,因为内存分配是实现定义的。更不用说它们可能根本没有动态分配内存。

你只需要假设客户端知道如何在C中编程。我能想到的唯一非解决方案就是自己分配内存并返回它,但这几乎不是一个小的变化。(这是一个较大的设计变更。)


2
好的,这样怎么样?由于这是C语言,客户端可能使用了malloc函数,如果无法分配内存,它确实会返回一个NULL指针。那么... 我们相信malloc? - Jacob
这取决于客户端在调用函数之前是否确保了malloc的成功分配内存,如果这就是你所说的。 - GManNickG
@jacob - 我知道我们可以在malloc处进行检查...但是如果客户端忘记执行malloc,则会导致分段错误..我想避免这种情况。 - codingfreak
9
是的,最终结论就是你的函数应该只做一件事情。如果每个函数都要确保它从参数中访问的内存是有效的,那么会增加太多的开销。让你的函数只做它应该做的事情即可。 - GManNickG
这是被低估的,可能因为它不是大多数人想要听到的答案。 - user377628
显示剩余4条评论

10

以下代码是我曾经使用过的一种方法来检查某些指针是否尝试访问非法内存。机制是引发SIGSEGV信号。早期,SEGV信号被重定向到一个私有函数,该函数使用longjmp返回到程序中。这有点像黑客行为,但它有效。

代码可以改进(例如使用“sigaction”而不是“signal”等),但只是为了给出一个想法。此外,它可移植到其他Unix版本,但我不确定在Windows上是否可用。请注意,SIGSEGV信号不应在程序的其他地方使用。

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

jmp_buf jump;

void segv (int sig)
{
  longjmp (jump, 1); 
}

int memcheck (void *x) 
{
  volatile char c;
  int illegal = 0;

  signal (SIGSEGV, segv);

  if (!setjmp (jump))
    c = *(char *) (x);
  else
    illegal = 1;

  signal (SIGSEGV, SIG_DFL);

  return (illegal);
}

int main (int argc, char *argv[])
{
  int *i, *j; 

  i = malloc (1);

  if (memcheck (i))
    printf ("i points to illegal memory\n");
  if (memcheck (j))
    printf ("j points to illegal memory\n");

  free (i);

  return (0);
}

3
i = malloc(1); 是有效的 C 代码,并且比 i = (int*) malloc(1); 更可取。也许你在想另一种语言。 - chux - Reinstate Monica
注意,在 POSIX 下,setjmp()longjmp() 应该被替换为 sigsetjmp()siglongjmp()。请参见 https://dev59.com/MGIi5IYBdhLWcg3w_AjV。 - Andrew Henle
1
在我看来,无法保证无效的内存访问会导致SEGV错误 - 即使x没有指向分配的区域,你的 c = *(char *)(x); 也可能正常通过。只有当指针指向不可访问的内存段时,才会触发SEGV,但是内存段的大小为几KB,因此如果你在地址10分配了4个字节,那么地址20虽然不在分配的区域内,但很可能仍然在与地址10相同的内存段中,因此尽管未被分配,你仍然可以访问地址20而不会出现SEGV错误。 - Michael Beer
这就是为什么你应该总是将未使用的指针设置为 NULL,因为只有这个值 保证 在尝试解引用它时会导致 SEGV。对于任何其他内存地址都不能保证。 - Michael Beer
@Michael Beer:“没有保证无效的内存访问会导致SEGV” - 是的,但检查仍然有效。如果没有SEGV,则可以访问该内存。 - Peter
1
问题是:“有没有一种方法可以在C语言中检查一个指针是否被分配?”你的代码不适合检查这个。你的代码只检查操作系统/MMU是否禁止你访问它——这并不意味着它被正确地分配了…… - Michael Beer

9

针对特定平台的解决方案,您可能会对Win32函数IsBadReadPtr(以及类似的其他函数)感兴趣。该函数将能够(几乎)预测从特定内存块读取时是否会导致分段错误。

然而,在一般情况下,这并不能保护您,因为操作系统不知道C运行时堆管理器,如果调用者传递的缓冲区大小不如您所期望的那样大,则堆块的其余部分仍将从操作系统的角度可读。


@Greg - 很抱歉我对WIN32函数不太感兴趣...如果可能的话,一个能够正常工作的“脏”解决方案也可以,因为没有标准的C函数。 - codingfreak
2
好的,你没有说明你对哪个平台感兴趣。指定平台和编译器可能会得到更具体的答案。 - Greg Hewgill
2
IsBadXxxPtr 应该真正被称为随机崩溃程序。 - Alexey Frunze

8
我总是将指针初始化为null值。因此当我分配内存时,它会发生变化。当我检查内存是否已分配时,我会使用pointer != NULL。当我释放内存时,我也将指针设置为null。我想不到任何方法来判断是否分配了足够的内存。
这并不能解决你的问题,但你必须相信,如果有人编写C程序,那么他一定有足够的技能来正确完成它。

@Yelonek,我同意你的看法。但我想知道是否有任何可能性来检查... - codingfreak
1
我也是,但(尤其是在库中)有时会出现问题。 - Sellorio

7
我曾在64位Solaris上使用过一种不太正当的方法。在64位模式下,堆从0x100000000开始。通过比较指针,我可以确定它是数据或代码段中的指针 p < (void*)0x100000000,堆中的指针 p > (void*)0x100000000 或内存映射区域中的指针 (intptr_t)p < 0 (mmap返回可寻址区域的顶部地址)。
这使得我的程序可以在同一映射中保存已分配和内存映射的指针,并且我的映射模块可以释放正确的指针。但这种技巧高度不可移植,如果您的代码依赖于此类技巧,则是时候重新思考代码架构了。您可能正在做错什么。

5

我知道这是一个老问题,但在C语言中几乎任何事情都是可能的。虽然已经有一些hackish解决方案了,但判断内存是否被正确分配的有效方法是使用预测器来代替malloccallocreallocfree。这跟测试框架(如cmocka)检测内存问题(如段错误、未释放内存等)的方式相同。你可以在分配内存时维护一个内存地址列表,并在用户想要使用你的函数时简单地检查这个列表。我为自己的测试框架实现了非常类似的东西。下面是一些示例代码:

typedef struct memory_ref {
    void       *ptr;
    int        bytes;
    memory_ref *next;
}

memory_ref *HEAD = NULL;

void *__wrap_malloc(size_t bytes) {
    if(HEAD == NULL) {
        HEAD = __real_malloc(sizeof(memory_ref));
    }

    void *tmpPtr = __real_malloc(bytes);

    memory_ref *previousRef = HEAD;
    memory_ref *currentRef = HEAD->next;
    while(current != NULL) {
        previousRef = currentRef;
        currentRef = currentRef->next;
    }

    memory_ref *newRef = (memory_ref *)__real_malloc(sizeof(memory_ref));
    *newRef = (memory_ref){
        .ptr   = tmpPtr,
        .bytes = bytes,
        .next  = NULL
    };

    previousRef->next = newRef;

    return tmpPtr;
}

您需要为callocreallocfree编写类似的函数,每个包装器都以__wrap_为前缀。真正的malloc可通过使用__real_malloc获得(对于您要包装的其他函数也是如此)。每当您想要检查内存是否实际分配时,只需遍历链接的memory_ref列表并查找内存地址。如果您找到它并且它足够大,则可以确定该内存地址不会使程序崩溃;否则返回错误。在程序使用的头文件中,您将添加以下行:
extern void *__real_malloc  (size_t);
extern void *__wrap_malloc  (size_t);
extern void *__real_realloc (size_t);
extern void *__wrap_realloc (size_t);
// Declare all the other functions that will be wrapped...

我的需求相当简单,所以我实现了一个非常基本的版本,但你可以想象如何扩展它以拥有更好的跟踪系统(例如,创建一个struct除了大小外还跟踪内存位置)。然后,您只需要使用编译器编译代码。
gcc src_files -o dest_file -Wl,-wrap,malloc -Wl,-wrap,calloc -Wl,-wrap,realloc -Wl,-wrap,free

缺点是用户必须使用上述指令编译他们的源代码,但这远非我所见过的最糟糕的情况。在分配和释放内存时存在一些开销,但添加安全性时总会存在一些开销。


4

一般而言,无法实现此操作。

另外,如果您的接口仅仅是“传递一个指针到缓冲区,我会放东西进去”,那么调用者可能选择根本不分配内存,而是使用静态分配或自动变量之类的固定大小缓冲区。或者它是指向堆上更大对象的一部分的指针。

如果您的接口明确说“传递一个指向分配内存的指针(因为我将处理它)”,则您应该期望调用者这样做。未能这样做是您无法可靠检测到的问题。


虽然这通常是最好的答案,而且大多数情况下是正确的,但我会说:如果付出足够的努力,你可以实现自己的定制加载器来跟踪所有内存分配 - 或者使用现有工具,如 valgrind ;) - Michael Beer

3

嗯,我不知道是否有人已经放置了它,或者在您的程序中是否可能会出现这种情况。我在大学项目中遇到了类似的问题。

我解决了这个问题 - 在main()初始化部分中,在我声明LIST *ptr后,我只需将ptr=NULL放入即可。像这样 -

int main(int argc, char **argv) {

LIST *ptr;
ptr=NULL;

当分配内存失败或者指针根本没有被分配时,它将是NULL。所以你可以通过if语句简单地测试它。

if (ptr==NULL) {
  "THE LIST DOESN'T EXIST" 
} else {
  "THE LIST MUST EXIST --> SO IT HAS BEEN ALLOCATED"
}

我不知道你的程序是如何编写的,但你肯定明白我想指出什么。如果可以像这样检查你的分配并将参数传递给函数,那么你就可以有一个简单的解决方案。
当然,你必须小心处理好分配和创建结构体的函数,但在C语言中你不必太过小心。

3

一种您可以尝试的黑客技巧是检查指针是否指向堆栈分配的内存。 这通常不会有帮助,因为分配的缓冲区可能太小或者指针指向某个全局内存段(.bss、.const等)。

要执行此黑客技巧,首先将main()中第一个变量的地址存储下来。稍后,您可以将此地址与特定例程中的本地变量的地址进行比较。 两个地址之间的所有地址都位于堆栈上。


是的...如果我编写整个应用程序,我可以这样做。但是为了使用一个函数来检查事情可能会很复杂..? - codingfreak
这可能会让人误以为未初始化的指针在堆上。此外,如果有人恰好将指针存储到堆栈更远的某个位置(向上或向下),然后弹出以获取您的函数,则该指针也将被视为在堆上。 - Kevin
将指针区分为在堆上还是在栈上分配并不能真正帮上什么忙——那char copy[255] = {0}; snprintf(copy, sizeof(copy), "%n: %s\n", error_code, error_msg); copy[sizeof(copy) -1] = 0; write(log_fd, copy, strnlen(copy) + 1); copy[0] = 0;呢?如果snprintf像您所提出的那样执行奇怪的检查,snprintf会错误地认为copy是一个无效的指针... - Michael Beer

2

我不知道有没有库函数可以实现这个功能,但是在Linux上,你可以查看/proc/<pid>/numa_maps。它将显示所有内存段,第三列将显示“heap”或“stack”。你可以查看原始指针值来确定它所在的位置。

例如:

00400000 prefer:0 file=/usr/bin/bash mapped=163 mapmax=9 N0=3 N1=160
006dc000 prefer:0 file=/usr/bin/bash anon=1 dirty=1 N0=1
006dd000 prefer:0 file=/usr/bin/bash anon=9 dirty=9 N0=3 N1=6
006e6000 prefer:0 anon=6 dirty=6 N0=2 N1=4
01167000 prefer:0 heap anon=122 dirty=122 N0=25 N1=97
7f39904d2000 prefer:0 anon=1 dirty=1 N0=1
7f39904d3000 prefer:0 file=/usr/lib64/ld-2.17.so anon=1 dirty=1 N0=1
7f39904d4000 prefer:0 file=/usr/lib64/ld-2.17.so anon=1 dirty=1 N1=1
7f39904d5000 prefer:0 anon=1 dirty=1 N0=1
7fffc2d6a000 prefer:0 stack anon=6 dirty=6 N0=3 N1=3
7fffc2dfe000 prefer:0

位于0x01167000以上但低于0x7f39904d2000的指针位于堆中。


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