97

有什么方法可以确定(当然,以编程方式)给定的指针是否“有效”?检查 NULL 很容易,但是像 0x00001234 这样的东西呢?当试图取消引用这种指针时,会发生异常/崩溃。

首选跨平台方法,但特定于平台(适用于 Windows 和 Linux)也可以。

更新澄清: 问题不在于陈旧/释放/未初始化的指针;相反,我正在实现一个 API,它从调用者那里获取指针(如指向字符串的指针、文件句柄等)。调用者可以(有意或错误地)发送一个无效值作为指针。如何防止崩溃?

4

29 回答 29

78

更新澄清:问题不在于陈旧、释放或未初始化的指针;相反,我正在实现一个 API,它从调用者那里获取指针(如指向字符串的指针、文件句柄等)。调用者可以(有意或错误地)发送一个无效值作为指针。如何防止崩溃?

你不能做那个检查。您根本无法检查指针是否“有效”。你必须相信,当人们使用带指针的函数时,这些人知道他们在做什么。如果他们将 0x4211 作为指针值传递给您,那么您必须相信它指向地址 0x4211。如果他们“不小心”撞到了一个物体,那么即使你会使用一些可怕的操作系统功能(IsValidPtr 或其他),你仍然会陷入错误并且不会很快失败。

开始使用空指针来发出这种信号,并告诉库的用户,如果他们倾向于不小心传递无效指针,他们不应该使用指针,认真的:)

于 2009-02-15T16:11:26.720 回答
43

以下是 Linux 下 C 程序自省其运行的内存状态的三种简单方法,以及为什么该问题在某些情况下具有适当的复杂答案。

  1. 在调用 getpagesize() 并将指针舍入到页面边界之后,您可以调用 mincore() 来确定页面是否有效以及它是否恰好是进程工作集的一部分。请注意,这需要一些内核资源,因此您应该对其进行基准测试并确定在您的 api 中调用此函数是否真的合适。如果您的 api 将要处理中断,或从串行端口读取到内存,调用它以避免不可预知的行为是合适的。
  2. 在调用 stat() 确定是否有 /proc/self 目录可用后,您可以打开并阅读 /proc/self/maps 以查找有关指针所在区域的信息。研究过程信息伪文件系统 proc 的手册页。显然,这是相对昂贵的,但您可能能够摆脱将解析结果缓存到一个数组中,您可以使用二进制搜索有效地查找。还要考虑 /proc/self/smaps。如果您的 api 用于高性能计算,那么程序将想了解 /proc/self/numa 的信息,该文件记录在 numa 的手册页下,非统一内存架构。
  3. get_mempolicy(MPOL_F_ADDR) 调用适用于高性能计算 api 工作,其中有多个执行线程,并且您正在管理您的工作以对非均匀内存具有亲和力,因为它与 cpu 内核和套接字资源相关。这样的 api 当然也会告诉你指针是否有效。

在 Microsoft Windows 下,有记录在 Process Status API 下的函数 QueryWorkingSetEx(也在 NUMA API 中)。作为复杂的 NUMA API 编程的必然结果,此功能还可以让您进行简单的“测试指针的有效性 (C/C++)”工作,因此它在至少 15 年内不太可能被弃用。

于 2015-05-12T17:22:16.750 回答
30

防止由于调用者发送无效指针而导致的崩溃是产生难以发现的静默错误的好方法。

对于使用你的 API 的程序员来说,通过崩溃而不是隐藏它来获得一个明确的信息,表明他的代码是伪造的不是更好吗?

于 2009-02-15T16:05:30.387 回答
28

在 Win32/64 上有一种方法可以做到这一点。尝试读取指针并捕获将在失败时引发的生成的 SEH 异常。如果它没有抛出,那么它是一个有效的指针。

这种方法的问题在于它只返回您是否可以从指针中读取数据。它不保证类型安全或任何数量的其他不变量。一般来说,除了说“是的,我可以在现在已经过去的时间读取内存中的那个特定位置”之外,这种方法几乎没有什么用处。

简而言之,不要这样做;)

Raymond Chen 有一篇关于这个主题的博文:http: //blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

于 2009-02-15T15:55:01.800 回答
16

AFAIK没有办法。您应该尝试通过在释放内存后始终将指针设置为 NULL 来避免这种情况。

于 2009-02-15T15:48:27.993 回答
8

在 Unix 上,您应该能够利用内核系统调用来进行指针检查并返回 EFAULT,例如:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>

bool isPointerBad( void * p )
{
   int fh = open( p, 0, 0 );
   int e = errno;

   if ( -1 == fh && e == EFAULT )
   {
      printf( "bad pointer: %p\n", p );
      return true;
   }
   else if ( fh != -1 )
   {
      close( fh );
   }

   printf( "good pointer: %p\n", p );
   return false;
}

int main()
{
   int good = 4;
   isPointerBad( (void *)3 );
   isPointerBad( &good );
   isPointerBad( "/tmp/blah" );

   return 0;
}

返回:

bad pointer: 0x3
good pointer: 0x7fff375fd49c
good pointer: 0x400793

可能有比 open() [也许是访问] 更好的系统调用,因为这有可能导致实际的文件创建代码路径,以及随后的关闭要求。

于 2018-05-29T22:34:34.730 回答
7

关于这个线程中的答案有点:

适用于 Windows 的 IsBadReadPtr()、IsBadWritePtr()、IsBadCodePtr()、IsBadStringPtr()。

我的建议是远离他们,有人已经发布了这个:http: //blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx

关于同一主题和同一作者(我认为)的另一篇文章是这个:http: //blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx(“IsBadXxxPtr 真的应该被称为 CrashProgramRandomly ”)。

如果你的 API 的用户发送了错误的数据,让它崩溃。如果问题是传递的数据直到以后才使用(这使得查找原因变得更加困难),添加一个调试模式,在输入时记录字符串等。如果它们不好,那将是显而易见的(并且可能会崩溃)。如果它经常发生,可能值得将您的 API 移出进程并让它们使 API 进程而不是主进程崩溃。

于 2009-02-15T18:37:51.360 回答
5

首先,我认为保护自己免受故意造成崩溃的呼叫者的影响没有任何意义。他们可以通过自己尝试通过无效指针进行访问来轻松做到这一点。还有很多其他方法——它们可能只是覆盖你的内存或堆栈。如果您需要防止此类事情发生,那么您需要使用套接字或其他 IPC 在单独的进程中运行以进行通信。

我们编写了很多允许合作伙伴/客户/用户扩展功能的软件。不可避免地,任何错误都会首先向我们报告,因此能够轻松地表明问题出在插件代码中是很有用的。此外,还有安全问题,一些用户比其他用户更受信任。

我们根据性能/吞吐量要求和可信度使用多种不同的方法。从最喜欢的:

  • 使用套接字分离进程(通常将数据作为文本传递)。

  • 使用共享内存分离进程(如果要传递大量数据)。

  • 同一进程通过消息队列分离线程(如果是频繁的短消息)。

  • 同一个进程分离线程所有传递从内存池分配的数据。

  • 通过直接过程调用的相同进程 - 所有传递的数据都是从内存池分配的。

在处理第三方软件时,我们绝不会求助于您尝试做的事情——尤其是当我们将插件/库作为二进制而不是源代码提供时。

在大多数情况下,内存池的使用非常容易,并且不一定是低效的。如果您首先分配数据,那么根据您分配的值检查指针是微不足道的。您还可以存储分配的长度并在数据之前和之后添加“魔术”值,以检查有效的数据类型和数据溢出。

于 2009-02-15T18:03:16.593 回答
5

我对你的问题非常同情,因为我自己也处于几乎相同的位置。我很欣赏很多回复所说的话,它们是正确的——提供指针的例程应该提供一个有效的指针。就我而言,他们可能破坏了指针几乎是不可想象的——但如果他们成功了,那将是我的软件崩溃,而我会受到指责:-(

我的要求不是我在出现分段错误后继续 - 那会很危险 - 我只想在终止之前报告客户发生的事情,以便他们可以修复他们的代码而不是责怪我!

这就是我发现的方法(在 Windows 上):http ://www.cplusplus.com/reference/clibrary/csignal/signal/

给出一个概要:

#include <signal.h>

using namespace std;

void terminate(int param)
/// Function executed if a segmentation fault is encountered during the cast to an instance.
{
  cerr << "\nThe function received a corrupted reference - please check the user-supplied  dll.\n";
  cerr << "Terminating program...\n";
  exit(1);
}

...
void MyFunction()
{
    void (*previous_sigsegv_function)(int);
    previous_sigsegv_function = signal(SIGSEGV, terminate);

    <-- insert risky stuff here -->

    signal(SIGSEGV, previous_sigsegv_function);
}

现在这似乎表现得如我所愿(它打印错误消息,然后终止程序) - 但如果有人能发现缺陷,请告诉我!

于 2012-04-12T13:45:52.650 回答
2

C++ 中没有规定测试指针的有效性作为一般情况。显然可以假设 NULL (0x00000000) 不好,并且各种编译器和库喜欢在这里和那里使用“特殊值”来简化调试(例如,如果我在 Visual Studio 中看到指针显示为 0xCECECECE 我知道我做错了什么)但事实是,由于指针只是内存中的索引,因此仅通过查看指针来判断它是否是“正确的”索引几乎是不可能的。

您可以使用 dynamic_cast 和 RTTI 执行各种技巧,例如确保指向的对象是您想要的类型,但它们都要求您首先指向有效的对象。

如果您想确保您的程序可以检测到“无效”指针,那么我的建议是:在创建时立即将您声明的每个指针设置为 NULL 或有效地址,并在释放它指向的内存后立即将其设置为 NULL。如果您对这种做法很认真,那么检查 NULL 就是您所需要的。

于 2009-02-15T15:57:59.030 回答
2

在使用前后将指针设置为 NULL 是一种很好的技术。如果您在类中管理指针,例如(字符串),这在 C++ 中很容易做到:

class SomeClass
{
public:
    SomeClass();
    ~SomeClass();

    void SetText( const char *text);
    char *GetText() const { return MyText; }
    void Clear();

private:
    char * MyText;
};


SomeClass::SomeClass()
{
    MyText = NULL;
}


SomeClass::~SomeClass()
{
    Clear();
}

void SomeClass::Clear()
{
    if (MyText)
        free( MyText);

    MyText = NULL;
}



void SomeClass::Settext( const char *text)
{
    Clear();

    MyText = malloc( strlen(text));

    if (MyText)
        strcpy( MyText, text);
}
于 2009-02-15T16:09:36.380 回答
2

确实,可以在特定情况下做一些事情:例如,如果你想检查一个字符串指针 string 是否有效,使用 write(fd, buf, szie) 系统调用可以帮助你做到这一点:让 fd 是临时的文件描述符您为测试创建的文件,并且 buf 指向您正在测试的字符串,如果指针无效,write() 将返回 -1 并且 errno 设置为 EFAULT,这表明 buf 在您可访问的地址空间之外。

于 2018-03-25T16:24:51.607 回答
1

没有任何可移植的方法可以做到这一点,并且为特定平台做这件事可能是困难和不可能之间的任何地方。在任何情况下,您都不应该编写依赖于这种检查的代码——首先不要让指针采用无效值。

于 2009-02-15T15:49:51.670 回答
1

一般来说,这是不可能的。这是一个特别讨厌的案例:

struct Point2d {
    int x;
    int y;
};

struct Point3d {
    int x;
    int y;
    int z;
};

void dump(Point3 *p)
{
    printf("[%d %d %d]\n", p->x, p->y, p->z);
}

Point2d points[2] = { {0, 1}, {2, 3} };
Point3d *p3 = reinterpret_cast<Point3d *>(&points[0]);
dump(p3);

在许多平台上,这将打印出来:

[0 1 2]

您正在强制运行时系统错误地解释内存位,但在这种情况下它不会崩溃,因为这些位都是有意义的。struct inaddr这是语言设计的一部分(用, inaddr_in,来查看 C 风格的多态性inaddr_in6),因此您无法在任何平台上可靠地防范它。

于 2009-02-15T16:24:02.540 回答
1

在公共 API 中接受任意指针作为输入参数并不是一个很好的策略。最好有像整数、字符串或结构这样的“纯数据”类型(当然,我的意思是内部带有纯数据的经典结构;官方上任何东西都可以是结构)。

为什么?好吧,因为正如其他人所说,没有标准的方法可以知道您是否获得了有效的指针或指向垃圾的指针。

但有时你别无选择——你的 API 必须接受一个指针。

在这些情况下,调用者有责任传递一个好的指针。NULL 可以被接受为一个值,但不能作为一个指向垃圾的指针。

你能以任何方式仔细检查吗?好吧,在这种情况下,我所做的是为指针指向的类型定义一个不变量,并在获得它时调用它(在调试模式下)。至少如果不变量失败(或崩溃),您知道您传递了一个错误的值。

// API that does not allow NULL
void PublicApiFunction1(Person* in_person)
{
  assert(in_person != NULL);
  assert(in_person->Invariant());

  // Actual code...
}

// API that allows NULL
void PublicApiFunction2(Person* in_person)
{
  assert(in_person == NULL || in_person->Invariant());

  // Actual code (must keep in mind that in_person may be NULL)
}
于 2009-02-16T08:20:04.893 回答
1

正如其他人所说,您无法可靠地检测到无效指针。考虑一些无效指针可能采用的形式:

你可以有一个空指针。那是您可以轻松检查并做一些事情的一个。

您可以有一个指向有效内存之外某处的指针。有效内存的构成取决于系统的运行时环境如何设置地址空间。在 Unix 系统上,它通常是一个虚拟地址空间,从 0 开始,一直到一些兆字节。在嵌入式系统上,它可能非常小。无论如何,它可能不会从 0 开始。如果您的应用程序恰好在超级用户模式或等效模式下运行,那么您的指针可能会引用一个真实地址,该地址可能会或可能不会使用真实内存进行备份。

您可以有一个指向有效内存内某处的指针,甚至在您的数据段、bss、堆栈或堆内,但不指向有效对象。它的一个变体是一个指针,用于在对象发生不良事件之前指向有效对象。这种情况下的坏事包括释放、内存损坏或指针损坏。

您可能有一个完全非法的指针,例如与被引用事物的非法对齐的指针。

当您考虑基于段/偏移量的架构和其他奇怪的指针实现时,问题会变得更糟。这类事情通常通过良好的编译器和明智地使用类型对开发人员隐藏,但如果你想揭开面纱并试图智取操作系统和编译器开发人员,你可以,但没有一种通用的方法这样做可以解决您可能遇到的所有问题。

您能做的最好的事情就是允许崩溃并提供一些好的诊断信息。

于 2009-02-16T08:48:44.047 回答
1

本文MEM10-C。定义和使用指针验证功能表示可以在某种程度上进行检查,尤其是在 Linux OS 下。

链接中描述的方法是跟踪 malloc 返回的最高内存地址,并添加一个函数来测试是否有人尝试使用大于该值的指针。它可能是有限的我们。

于 2012-04-11T08:17:34.153 回答
1

令人难以置信的是,您可以在上面的文章中阅读到多少误导性信息...

甚至在 microsoft msdn 文档中,IsBadPtr 也声称被禁止。哦,好吧 - 我更喜欢工作应用程序而不是崩溃。即使长期工作可能无法正常工作(只要最终用户可以继续使用应用程序)。

通过谷歌搜索,我没有找到任何有用的 windows 示例 - 找到了 32 位应用程序的解决方案,

http://www.codeproject.com/script/Content/ViewAssociatedFile.aspx?rzp=%2FKB%2Fsystem%2Fdetect-driver%2F%2FDetectDriverSrc.zip&zep=DetectDriverSrc%2FDetectDriver%2Fsrc%2FdrvCppLib%2Frtti.cpp&obid=58895&obtid=2&ovid =2

但我还需要支持 64 位应用程序,所以这个解决方案对我不起作用。

但我已经收获了 wine 的源代码,并设法编写了类似的代码,这些代码也适用于 64 位应用程序——在此处附加代码:

#include <typeinfo.h>   

typedef void (*v_table_ptr)();   

typedef struct _cpp_object   
{   
    v_table_ptr*    vtable;   
} cpp_object;   



#ifndef _WIN64
typedef struct _rtti_object_locator
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    const type_info *type_descriptor;
    //const rtti_object_hierarchy *type_hierarchy;
} rtti_object_locator;
#else

typedef struct
{
    unsigned int signature;
    int base_class_offset;
    unsigned int flags;
    unsigned int type_descriptor;
    unsigned int type_hierarchy;
    unsigned int object_locator;
} rtti_object_locator;  

#endif

/* Get type info from an object (internal) */  
static const rtti_object_locator* RTTI_GetObjectLocator(void* inptr)  
{   
    cpp_object* cppobj = (cpp_object*) inptr;  
    const rtti_object_locator* obj_locator = 0;   

    if (!IsBadReadPtr(cppobj, sizeof(void*)) &&   
        !IsBadReadPtr(cppobj->vtable - 1, sizeof(void*)) &&   
        !IsBadReadPtr((void*)cppobj->vtable[-1], sizeof(rtti_object_locator)))  
    {  
        obj_locator = (rtti_object_locator*) cppobj->vtable[-1];  
    }  

    return obj_locator;  
}  

以下代码可以检测指针是否有效,您可能需要添加一些 NULL 检查:

    CTest* t = new CTest();
    //t = (CTest*) 0;
    //t = (CTest*) 0x12345678;

    const rtti_object_locator* ptr = RTTI_GetObjectLocator(t);  

#ifdef _WIN64
    char *base = ptr->signature == 0 ? (char*)RtlPcToFileHeader((void*)ptr, (void**)&base) : (char*)ptr - ptr->object_locator;
    const type_info *td = (const type_info*)(base + ptr->type_descriptor);
#else
    const type_info *td = ptr->type_descriptor;
#endif
    const char* n =td->name();

这从指针获取类名 - 我认为它应该足以满足您的需求。

我仍然担心的一件事是指针检查的性能——在上面的代码片段中,已经进行了 3-4 个 API 调用——对于时间关键的应用程序来说可能是多余的。

如果有人可以测量指针检查的开销,例如与 C#/托管 C++ 调用相比,那就太好了。

于 2014-03-11T23:03:22.813 回答
1

以下在 Windows 中确实有效(之前有人建议过):

 static void copy(void * target, const void* source, int size)
 {
     __try
     {
         CopyMemory(target, source, size);
     }
     __except(EXCEPTION_EXECUTE_HANDLER)
     {
         doSomething(--whatever--);
     }
 }

该函数必须是某个类的静态、独立或静态方法。要测试只读,请复制本地缓冲区中的数据。要在不修改内容的情况下测试写入,请将它们重写。您只能测试第一个/最后一个地址。如果指针无效,控制权将传递给'doSomething',然后在括号外。只是不要使用任何需要析构函数的东西,比如 CString。

于 2015-09-01T20:26:17.247 回答
1

在 Windows 上,我使用以下代码:

void * G_pPointer = NULL;
const char * G_szPointerName = NULL;
void CheckPointerIternal()
{
    char cTest = *((char *)G_pPointer);
}
bool CheckPointerIternalExt()
{
    bool bRet = false;

    __try
    {
        CheckPointerIternal();
        bRet = true;
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
    }

    return  bRet;
}
void CheckPointer(void * A_pPointer, const char * A_szPointerName)
{
    G_pPointer = A_pPointer;
    G_szPointerName = A_szPointerName;
    if (!CheckPointerIternalExt())
        throw std::runtime_error("Invalid pointer " + std::string(G_szPointerName) + "!");
}

用法:

unsigned long * pTest = (unsigned long *) 0x12345;
CheckPointer(pTest, "pTest"); //throws exception
于 2019-06-28T14:17:47.407 回答
1

Peeter Joos 的回答非常好。这是一种“官方”的方法:

#include <sys/mman.h>
#include <stdbool.h>
#include <unistd.h>

bool is_pointer_valid(void *p) {
    /* get the page size */
    size_t page_size = sysconf(_SC_PAGESIZE);
    /* find the address of the page that contains p */
    void *base = (void *)((((size_t)p) / page_size) * page_size);
    /* call msync, if it returns non-zero, return false */
    int ret = msync(base, page_size, MS_ASYNC) != -1;
    return ret ? ret : errno != ENOMEM;
}
于 2021-06-19T22:52:39.243 回答
0

适用于 Windows 的 IsBadReadPtr()、IsBadWritePtr()、IsBadCodePtr()、IsBadStringPtr()。
这些花费的时间与块的长度成正比,因此为了进行完整性检查,我只检查起始地址。

于 2009-02-15T15:53:28.340 回答
0

我已经看到各种库使用一些方法来检查未引用的内存等。我相信他们只是“覆盖”了内存分配和释放方法(malloc/free),它有一些跟踪指针的逻辑。我想这对于您的用例来说有点过分了,但这将是一种方法。

于 2009-02-15T16:12:49.867 回答
0

从技术上讲,您可以覆盖 operator new(和delete)并收集有关所有已分配内存的信息,因此您可以有一种方法来检查堆内存是否有效。但:

  1. 您仍然需要一种方法来检查指针是否在堆栈上分配()

  2. 您将需要定义什么是“有效”指针:

a) 该地址上的内存已分配

b)该地址的内存是对象的起始地址(例如,地址不在大数组的中间)

c) 该地址的内存是预期类型对象的起始地址

底线:有问题的方法不是 C++ 方法,您需要定义一些规则以确保函数接收有效指针。

于 2009-02-15T16:26:34.470 回答
0

没有办法在 C++ 中进行检查。如果其他代码传递给你一个无效的指针,你应该怎么做?你应该崩溃。为什么?查看此链接:http: //blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx

于 2009-02-16T01:02:03.013 回答
0

已接受答案的附录:

假设您的指针只能包含三个值——0、1 和 -1,其中 1 表示有效指针,-1 表示无效指针,0 表示另一个无效指针。您的指针NULL 的概率是多少,所有值的可能性相同?1/3。现在,取出有效案例,因此对于每个无效案例,您都有 50:50 的比率来捕获所有错误。看起来不错吧?将此缩放为 4 字节指针。有 2^32 或 4294967294 个可能的值。其中,只有一个值是正确的,一个是 NULL,还有 4294967292 个其他无效案例。重新计算:您对 (4294967292+ 1) 个无效案例中的 1 个进行了测试。对于大多数实际目的,概率为 2.xe-10 或 0。这就是 NULL 检查的徒劳。

于 2009-02-18T20:01:38.170 回答
0

你知道,一个能够做到这一点的新驱动程序(至少在 Linux 上)可能不会那么难写。

另一方面,像这样构建程序是愚蠢的。除非你对这种东西有一些非常具体和单一的用途,否则我不会推荐它。如果你构建了一个加载了常量指针有效性检查的大型应用程序,它可能会非常慢。

于 2009-03-09T17:03:19.363 回答
0

您应该避免使用这些方法,因为它们不起作用。blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx – JaredPar 2009 年 2 月 15 日 16:02

如果它们不起作用 - 下一次 Windows 更新会修复它吗?如果它们在概念级别上不起作用 - 功能可能会完全从 Windows api 中删除。

MSDN 文档声称它们被禁止,其原因可能是进一步设计应用程序的缺陷(例如,通常你不应该默默地吃无效指针 - 如果你当然负责整个应用程序的设计),以及性能/时间的指针检查。

但是您不应该因为某些博客而声称它们不起作用。在我的测试应用程序中,我已经验证它们确实有效。

于 2014-03-17T05:59:16.293 回答
0

这些链接可能会有所帮助

_CrtIsValidPointer 验证指定的内存范围对读写是否有效(仅限调试版本)。 http://msdn.microsoft.com/en-us/library/0w1ekd5e.aspx

_CrtCheckMemory 确认调试堆中分配的内存块的完整性(仅限调试版本)。 http://msdn.microsoft.com/en-us/library/e73x0s4b.aspx

于 2014-04-12T06:32:47.930 回答