有什么方法可以(当然,通过编程)确定给定的指针是否“有效”?检查NULL很容易,但是0x00001234之类的东西呢?尝试取消引用此类指针时,会发生异常/崩溃。
最好使用跨平台方法,但也可以使用特定于平台的方法(对于Windows和Linux)。
更新说明: 问题不是陈旧/已释放/未初始化的指针;相反,我正在实现一个API,该API接收来自调用者的指针(例如,指向字符串的指针,文件句柄等)。调用者可以发送(有意或无意)无效值作为指针。如何防止车祸?
有什么方法可以(当然,通过编程)确定给定的指针是否“有效”?检查NULL很容易,但是0x00001234之类的东西呢?尝试取消引用此类指针时,会发生异常/崩溃。
最好使用跨平台方法,但也可以使用特定于平台的方法(对于Windows和Linux)。
更新说明: 问题不是陈旧/已释放/未初始化的指针;相反,我正在实现一个API,该API接收来自调用者的指针(例如,指向字符串的指针,文件句柄等)。调用者可以发送(有意或无意)无效值作为指针。如何防止车祸?
Answers:
更新说明:问题不是陈旧的,释放的或未初始化的指针。相反,我正在实现一个API,该API接收来自调用者的指针(例如,指向字符串的指针,文件句柄等)。调用者可以发送(有意或无意)无效值作为指针。如何防止车祸?
你不能做那张支票。根本无法检查指针是否为“有效”。您必须相信,当人们使用带有指针的函数时,这些人知道他们在做什么。如果它们将0x4211作为指针值传递给您,那么您必须信任它指向地址0x4211。而且,如果它们“意外地”击中了某个对象,那么即使您使用一些令人恐惧的操作系统功能(IsValidPtr或其他功能),您仍然会陷入bug之中并且不会快速失败。
开始使用空指针来发信号通知这种事情,并告诉您的库用户,如果它们倾向于意外地传递无效指针,则不要使用指针,严重的是:)
对于Linux下的C程序,以下三种简单的方法可以对其运行所在的内存状态进行自省,以及在某些情况下为什么该问题具有适当的复杂答案。
在Microsoft Windows下,有一个QueryWorkingSetEx函数,该函数记录在“进程状态API”下(也在NUMA API中)。作为复杂NUMA API编程的必然结果,此功能还使您可以进行简单的“有效性测试指针(C / C ++)”工作,因此,至少15年不建议使用此功能。
防止由调用方发送无效指针导致的崩溃是使难以发现的静默错误的好方法。
使用您的API的程序员通过崩溃而不是隐藏它来获得清晰的消息,表明他的代码是伪造的,这不是更好吗?
在Win32 / 64上,有一种方法可以做到这一点。尝试读取指针,并捕获失败时将引发的SEH感知。如果没有抛出,则它是一个有效的指针。
但是,此方法的问题在于它仅返回您是否可以从指针读取数据。它不能保证类型安全或任何其他数量的不变量。通常,此方法除了说“是的,我可以在已经过去的时间读取内存中的特定位置”之外,对其他方面没有什么好处。
简而言之,不要这样做;)
Raymond Chen对此主题发表了一篇博客文章:http : //blogs.msdn.com/oldnewthing/archive/2007/06/25/3507294.aspx
AFAIK没有办法。您应该尝试通过在释放内存后始终将指针设置为NULL来避免这种情况。
关于这个线程的答案:
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进程而不是主进程崩溃。
首先,我认为保护自己免受来电者故意造成崩溃的保护没有任何意义。他们可以通过尝试自己通过无效的指针来轻松实现此目的。还有许多其他方法-它们可能会覆盖您的内存或堆栈。如果您需要防止此类情况发生,则需要使用套接字或某些其他IPC在单独的进程中运行以进行通信。
我们编写了很多软件,可让合作伙伴/客户/用户扩展功能。不可避免地,任何错误都会首先报告给我们,因此能够轻松显示问题出在插件代码中很有用。此外,还存在安全问题,某些用户比其他用户更受信任。
我们根据性能/吞吐量要求和可信赖性使用多种方法。从最喜欢的:
使用套接字将进程分开(通常将数据作为文本传递)。
使用共享内存(如果要传递大量数据)来分离进程。
同一进程通过消息队列(如果是频繁的短消息)将线程分开。
同一进程将所有线程从内存池分配的所有线程分开。
通过直接过程调用实现相同的过程-所有传递的数据均从内存池分配。
在与第三方软件打交道时,我们绝不会求助于您要做的事情-尤其是当我们以二进制文件而不是源代码的形式获得插件/库时。
在大多数情况下,使用内存池非常容易,并且效率不高。如果您首先分配数据,则对照分配的值检查指针很简单。您还可以存储分配的长度,并在数据前后添加“魔术”值,以检查有效的数据类型和数据超限。
我很同情你的问题,因为我本人的职位几乎相同。我很欣赏很多答复所说的话,而且它们是正确的-提供指针的例程应该提供有效的指针。就我而言,几乎无法想象他们可能损坏了指针-但是,如果他们进行了管理,那将是我的软件崩溃,而我将受到指责:-(
我的要求不是在出现分段错误后继续运行-这很危险-我只想在终止之前向客户报告发生的事情,以便他们可以修复其代码而不是怪我!
这是我发现的方法(在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);
}
现在,这看起来像我希望的那样(它会打印错误消息,然后终止程序)-但是如果有人可以发现缺陷,请告诉我!
exit()
,它会绕过RAII,从而可能导致资源泄漏。
exit()
,我的便携式C ++警铃开始响起。在这种特定于Linux的情况下应该没问题,因为您的程序无论如何都会退出,对此感到抱歉。
man 2 signal
在Linux上有一段解释了为什么。
在Unix上,您应该能够利用内核syscall进行指针检查并返回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()[也许访问]更好的syscall,因为这有可能导致实际的文件创建代码路径以及随后的关闭要求。
C ++中没有任何规定可以测试指针作为一般情况的有效性。显然,可以假设NULL(0x00000000)是不好的,并且各种编译器和库都喜欢在此处和此处使用“特殊值”来简化调试(例如,如果我在Visual Studio中看到一个指针显示为0xCECECECE,我知道我做错了),但事实是,由于指针只是内存中的索引,因此仅通过查看指针是否为“正确”索引就几乎无法分辨。
您可以使用dynamic_cast和RTTI进行多种技巧,以确保所指向的对象具有所需的类型,但是所有这些都要求您首先要指向有效的内容。
如果要确保程序可以检测到“无效”指针,那么我的建议是:将创建时立即声明的每个指针设置为NULL或有效地址,并在释放指向其的内存后立即将其设置为NULL。如果您对这种做法很勤奋,那么只需检查NULL。
没有任何可移植的方式来执行此操作,并且针对特定平台执行此操作可能在困难与不可能之间。无论如何,您永远都不要编写依赖于这种检查的代码-不要让指针首先使用无效值。
在使用前后将指针设置为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);
}
在公共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)
}
正如其他人所说,您不能可靠地检测到无效的指针。考虑无效指针可能采用的某些形式:
您可能有一个空指针。那是您可以轻松检查并执行某些操作的一个。
您可能有一个指向有效内存之外的地方的指针。构成有效内存的内容因系统的运行时环境如何设置地址空间而异。在Unix系统上,它通常是一个虚拟地址空间,从0开始,到大量兆字节。在嵌入式系统上,它可能很小。无论如何,它可能都不以0开始。如果您的应用恰好在超级用户模式或等效模式下运行,则您的指针可能引用真实地址,该地址可能会或可能不会使用真实内存进行备份。
您可能有一个指向有效内存中某处的指针,甚至在您的数据段,bss,堆栈或堆中,但没有指向有效对象。此方法的一种变体是一个指针,用于在对象发生不良情况之前指向有效对象。在这种情况下,坏事包括释放,内存损坏或指针损坏。
您可以拥有一个完全合法的非法指针,例如一个被非法引用的指针。
当您考虑基于段/偏移的体系结构和其他奇数指针实现时,该问题会变得更加严重。通常,良好的编译器和对类型的明智使用会对开发人员隐藏这种情况,但是,如果您想揭开面纱,并试图超越操作系统和编译器开发人员,那么可以,但是,没有一种通用的方法这样做可以解决您可能遇到的所有问题。
您能做的最好的事情是允许崩溃并发出一些好的诊断信息。
本文的MEM10-C。定义并使用指针验证功能表示可以进行某种程度的检查,尤其是在Linux OS下。
通常,这是不可能的。这是一个特别讨厌的情况:
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]
您正在强迫运行时系统错误地解释内存位,但是在这种情况下,它不会崩溃,因为这些位都有意义。这是语言的(看看C风格的多态性与设计的一部分struct inaddr
,inaddr_in
,inaddr_in6
),所以你不能可靠地防止它在任何平台上。
令人难以置信的是,您在以上文章中阅读了多少误导性信息...
甚至在Microsoft msdn文档中,IsBadPtr也被禁止。哦,很好-我更喜欢工作的应用程序而不是崩溃。即使长期工作可能无法正常工作(只要最终用户可以继续进行申请)。
通过谷歌搜索,我没有找到任何有用的Windows示例-找到了适用于32位应用的解决方案,
但我还需要支持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 ++调用相比),那将是很好的。
以下操作在Windows中确实有效(以前有人建议过):
static void copy(void * target, const void* source, int size)
{
__try
{
CopyMemory(target, source, size);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
doSomething(--whatever--);
}
}
该功能必须是某个类的静态,独立或静态方法。要测试只读,请在本地缓冲区中复制数据。要在不修改内容的情况下进行写入测试,请将其覆盖。您只能测试第一个/最后一个地址。如果指针无效,则将控制权传递给“ doSomething”,然后移至方括号之外。只是不要使用任何需要析构函数的东西,例如CString。
在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
Windows的IsBadReadPtr(),IsBadWritePtr(),IsBadCodePtr(),IsBadStringPtr()。
这些时间与块的长度成正比,因此对于完整性检查,我只检查起始地址。
我已经看到各种库使用某种方法来检查未引用的内存等。我相信他们只是简单地“覆盖”了内存分配和释放方法(malloc / free),这些方法具有跟踪指针的逻辑。我认为这对于您的用例来说是多余的,但这将是实现此目的的一种方法。
从技术上讲,您可以重写operator new(和delete)并收集有关所有已分配内存的信息,因此您可以使用一种方法来检查堆内存是否有效。但:
您仍然需要一种方法来检查是否在堆栈()上分配了指针
您将需要定义什么是“有效”指针:
a)分配该地址上的内存
b)该地址处的内存是对象的起始地址(例如,地址不在大型数组中间)
c)该地址处的内存是预期类型的对象的起始地址
底线:有问题的方法不是C ++方式,您需要定义一些规则以确保函数接收有效的指针。
没有办法在C ++中进行检查。如果其他代码向您传递了无效的指针,该怎么办?你应该崩溃。为什么?查看此链接:http : //blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx
附有答案的答案:
假设您的指针只能包含三个值-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检查的徒劳。
您应避免使用这些方法,因为它们不起作用。blogs.msdn.com/oldnewthing/archive/2006/09/27/773741.aspx – JaredPar '09 Feb 15'在16:02
如果它们不起作用-下一个Windows Update将解决此问题吗?如果它们在概念级别上不起作用-函数可能会完全从Windows api中删除。
MSDN文档声称它们被禁止,其原因很可能是应用程序进一步设计的缺陷(例如,通常,您不应默默地吃掉无效的指针-如果您当然负责整个应用程序的设计)以及性能/时间指针检查。
但是您不应该因为某些博客而声称它们不起作用。在我的测试应用程序中,我已经验证了它们确实起作用。
这些链接可能会有所帮助
_CrtIsValidPointer验证指定的内存范围对于读取和写入有效(仅调试版本)。 http://msdn.microsoft.com/zh-CN/library/0w1ekd5e.aspx
_CrtCheckMemory确认调试堆中分配的内存块的完整性(仅调试版本)。 http://msdn.microsoft.com/zh-CN/library/e73x0s4b.aspx