C++ - 检查指针是否指向有效内存(不可使用空指针检查)

3

我正在创建一种脚本语言。 当我分配一个东西时,它会分配该东西并返回地址, 然后我可以对其进行任何操作,最后将其删除。我无法控制其中的变量, 例如在我的语言中创建结构体(具有指针和布尔值以检查指针是否指向有效数据)等, 因为这会使我的语言更慢并且占用更多 RAM。

例如:(我的脚本语言很容易理解。我怀疑您不会理解它,但是我仍会添加一些注释)

MyStruct = { //Function. For create object with it use 'new' before it.
    TestAliveVar=0
}
Func = { //I'll explain what exactly this function does every place it runs.
    if (!exists(arg0)) //C++: ???
        exit;
    arg0.TestAliveVar=1
    println "Still alive!";
}
var MyVar=new MyStruct(); //Returns address of the new object in the heap
                          //and runs on it the `MyStruct` function.
Func(MyVar);              //Sets his 'TestAliveVar' to 1
                          //and prints 'Still Alive!' with new line
delete(MyVar);            //C++: free(MyVar);
Func(MyVar);              //Does nothing

问题在于如何创建您在此代码中看到的函数exists。 顺便提一下,我可以在此语言中运行C++代码。

4
保持一个“有效”标志可能比测试指针的有效性更慢,这一点很难想象。 - Ben Voigt
@Ugo 如果我在代码中发送指针到任何地方而没有结构体,并在某处删除它,那么我如何检查指针是否仍然有效? - MessyCode
3
我的意思是,检查一个指针是否“有效”会有很多误报,并且非常慢。即使你为了拥有一个结构而承受性能损失,那也比你所询问的设计要快。 - Ben Voigt
@CodyGray 关于新的和免费的,那不是问题。我只是想向您展示我的编程语言的语法。 - MessyCode
5
记得填写清单! - R. Martinho Fernandes
显示剩余8条评论
6个回答

9
您可以使用shared_ptr<>来保存指针,并使用weak_ptr<>将指针传递给对象的消费者。通过销毁shared_ptr<>对象,您可以删除对象,然后所有的weak_ptr<>都将过期。
std::weak_ptr<int> wptr;
assert(wptr.expired());
{
    std::shared_ptr<int> intptr(new int);
    wptr = intptr;
    assert(!wptr.expired());
}
assert(wptr.expired());

所以,你的检查exists将检查weak_ptr<>是否已过期。
为了使构造的使用更具体:
Script code                 Hypothetical C++ code that gets executed
----                        ----
var MyVar=new MyStruct();   var_map["MyVar"]
                                = std::shared_ptr<Obj>(new Obj("MyStruct"));
Func(MyVar);                invoke("Func", std::weak_ptr<Obj>(var_map["MyVar"]));
exists(arg0)                !args[0].expired()
delete(MyVar);              var_map.erase("MyVar");

如果脚本要在多线程环境中运行,那么weak_ptr<>状态是一个关键部分。

1
我猜(也可能是错的)他正在寻找一种解决方案,可以保护他的解释器免受来自指向无效内存的脚本传递的指针的访问。 - user405725
就像VladLazarenko所说的那样。 - MessyCode
1
@VladLazarenko:我不理解。我没有看到我的解决方案和你的陈述之间有任何不兼容的地方。 - jxh
我想提一下,expired 可以给出有意义的正面结果,但也可能会给出错误的负面结果。 - R. Martinho Fernandes
抱歉,但每次检查值是否为指针等操作都非常耗费资源。 - MessyCode
显示剩余7条评论

5
检测内存是否不再使用可以通过维护已知死指针集合来完成。任何您创建的指针都会添加到“活动”集合中,当您删除对象时,将指针移动到“死亡”集合中。
真正棘手的部分是重用内存。当您想要为不同的对象重用相同的地址时该怎么办?您无法通过查看指针来判断,因为指针看起来都一样。因此,除非您永远不想重用内存,否则您必须更改您的要求。
以下是几种可能的方法:
  1. 一种可能的方法是在结构体中添加一个字段。我知道您说您不想这样做,但许多评论已经建议这是最佳解决方案,而我也倾向于这种方法。
  2. 另一种可能的方法是添加一个间接层,这样您就不会真正传递对象指针,而是传递到存活对象列表或其他地方的索引。
  3. 您还可以考虑引用计数和垃圾回收。这样,只有当没有人再引用它们时,对象才会被删除。这需要相当多的工作,但作为脚本语言的用户,我希望它提供垃圾回收功能。

我猜你需要存储指针和内存宽度。因为该范围内的任何内容都是有效的。例如,如果他的脚本正在传递指向有效数组中间元素的指针。但是,是的,+1。 - user405725
@VladLazarenko:只有在脚本能够执行指针算术运算时才需要宽度。如果数组访问被包装在不透明的函数中,则知道整个数组是否存在可能就足够了。 - MvG
我能够得到的最接近的是第三个。但是要实现它,需要很多性能。表达式中的任何“pop”(我在我的表达式计算器中使用堆栈)都需要放置智能指针代码。 - MessyCode
任何实现这种方法的行为都是未定义的,即使内存从未被重用:指向已释放内存的指针值甚至无法以可移植的方式进行比较。 - Davis Herring

2
这是一个非常糟糕的想法。指针是否安全使用不仅基于指针的值,还基于指针的整个历史记录。例如,假设您分配了一些内存,然后释放了它,但仍保留了指向它的指针。现在这个指针无效了。现在,其他东西被分配到了先前指针所在的内存中。现在,如果您尝试检测旧指针是否有效,似乎它是有效的,因为它指向的内存已经被分配了,但如果您尝试使用它,您会得到未定义的行为。如果您从中读取,您将得到垃圾。如果您尝试写入它,您可能会破坏堆栈。
如果您只想检测进程是否可以访问指针指向的内存,那是可能的,但不可移植,绝对不是一个好主意(它也会非常缓慢)。您基本上必须尝试从中读取或写入,然后捕获结果产生的操作系统异常或信号。正如我所说,即使这也是一个非常糟糕的想法。它只告诉您,如果您尝试访问它,操作系统是否会杀死您的进程;而不是它是否实际上是安全使用的。
要了解更多关于为什么这是一个糟糕的想法,请查看 Raymond Chen 的博客文章,他在 Windows 一些低级别的东西上工作。

IsBadXxxPtr 应该真正被称为随机崩溃程序

改进一个不好的想法是没有意义的


0

你可以使用自己的分配器来分配所有对象。你的exists函数只需查询分配器以查看对象是否仍在分配中。你可以使用类似于SMR的东西来确保在使用时你的引用不会指向其他地方...


0

最简单的解决方案是使用映射。该映射应以指向对象的指针为索引,通常作为void *。映射项的内容应为创建的对象类型。

每当脚本创建对象时,请向映射添加条目。每当脚本删除对象时,请删除映射条目。当脚本访问对象时,请在映射中查找指针,从而确认它存在且类型正确。


-1

这个有效性检查仅在Windows(VS)中进行检查,以下是函数:

#pragma once
//ptrvalid.h
__inline bool isValid(void* ptr) {
    if (((uint)ptr)&7==7)
        return false; //Not valid address at all (Maybe random pointer?)
    char _prefix;
    __try {
        _prefix=*(((char*)ptr)-1); //Get the prefix of this data
    } __except (true) { //Catch all unique exceptions (Windows exceptions) 
        return false; //Can't reach this memory
    }
    switch (_prefix) {
    case 0:    //Running release mode with debugger
    case -128: //Running release mode without debugger
    case -2:   //Running debug mode with debugger
    case -35:  //Running debug mode without debugger
        return false; //Deleted :(
        break;
    }
    return true; //Still alive!
}

使用方法:

#include <stdio.h>
#include "ptrvalid.h"

void PrintValid(void* ptr) {
    if (isValid(ptr))
        printf("%d is valid.\n",ptr);
    else
        printf("%d is not valid.\n",ptr);
}

int main() {
    int* my_array=(int*)malloc(4);
    PrintValid(my_array);
    PrintValid((void*)99);
    free(my_array);
    PrintValid(my_array);
    my_array=new int[4];
    PrintValid(my_array);
    delete my_array;
    PrintValid(my_array);
    getchar();
}

输出:

764776 is valid.
99 is not valid.
764776 is not valid.
774648 is valid.
774648 is not valid.

函数说明:(它的作用)

该函数在进行实际检查之前进行检查,以确定地址是否有效\起始点指向内存。 然后,它检查此进程是否可以访问此内存的前缀(如果无法访问,则捕获异常),最后一步是检查此内存的前缀是否以任何模式删除。(调试\无调试模式\发布模式) 如果该函数通过了所有这些检查,则返回 true。


你重新发明了IsBadXxxPtr函数,但不够健壮。请参见此处以了解为什么这可能是个坏主意的有理解释。早在此之前,已经有人回答过并引用了同样的链接。我怀疑人们因为这个原因而对此进行了负面评价,再加上这是一个真正糟糕的想法,他们希望阻止其他人使用或考虑它。 - Cody Gray
@CodyGray 当你生成随机指针时,可能会抛出异常。当你只写了(void*)52256时,它是一个有效的地址。但如果你的地址真的被分配了,它将不会抛出任何异常来返回false。 - MessyCode
不错的解决方案,知道一个函数,它可以接受一个地址并检查该地址是否绝对无效(并将导致取消引用时崩溃)或可能无效(但不会崩溃),这将是一件好事。在解释上下文中允许用户寻址任何他们想要的内存(例如解释性c)时,这是一个很好的东西。它将允许程序提供比“调试程序”更有意义的信息,例如“函数x尝试寻址非法内存,您可能需要调试它”。 - Dmytro

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