0xDEADBEEF与NULL的区别

26

在各种代码中,我看到过在调试版本中使用NULL进行内存分配...

memset(ptr,NULL,size);

或者使用0xDEADBEEF...

memset(ptr,0xDEADBEEF,size);
  1. 每种方法的优点是什么,通常在C/C++中实现这个问题有什么更好的方式吗?
  2. 如果一个指针被赋予了0xDEADBEEF的值,它是否仍然可以解引用有效数据?

1
也许这个问题的答案能帮到你...也许... - JiminP
1
@GMan:他说的是“调试版本”。请看下面我的解释。在调试版本中,通常会使用一个明确标识未初始化内存的值来初始化内存,比如0xcdcdcdcd(我认为这就是微软的调试分配器使用的值)。这非常有用。 - EboMike
1
@sean:守卫字,正确。释放内存还将验证分配周围的守卫字是否仍然完好无损,并在情况不符合要求时进行断言。这是防止缓冲区和数组溢出的第一道防线。 - EboMike
7
假设 CHAR_BIT 是8,那么 memset(ptr, 0xDEADBEEF, size);memset(ptr, 0xEF, size); 的效果完全相同。 - pmg
3
@trinithis提到了memset()函数的描述(在标准文档中7.21.6.1节),该函数原型为void *memset(void *s, int c, size_t n);,并指出:“函数memset()将c(转换为unsigned char)复制到指向s对象的前n个字节中。” 此外,我已经测试过它,并验证了我的实现与标准描述相同。 - pmg
显示剩余13条评论
10个回答

61
  1. 使用memset(ptr, NULL, size)memset(ptr, 0xDEADBEEF, size)任意一种方式都清楚地表明作者不理解他们在做什么。

    首先,如果NULL被定义为整数零,则memset(ptr, NULL, size)确实会将C和C++中的内存块清零。

    但是,在此上下文中使用NULL表示零值不是一个可接受的做法。 NULL是一个专门用于指针上下文的宏。 memset的第二个参数是一个整数,而不是指针。正确的方法是memset(ptr, 0, size)。注意:0 而不是 NULL。我会说,即使 memset(ptr,'\0', size) 看起来比 memset(ptr,NULL,size) 更好。

    此外,最新的 C++ 标准 - C++11 - 允许将 NULL 定义为 nullptrnullptr 值不能隐式转换为类型 int,这意味着以上代码在 C++11 及更高版本中无法保证编译通过。

    在 C 语言中(您的问题也标记为 C),宏 NULL 可以扩展为 (void *) 0。即使在 C 中,(void *) 0 也不能隐式转换为类型 int,这意味着在一般情况下,memset(ptr, NULL, size) 简单地是无效的代码。

其次,尽管memset的第二个参数具有int类型,但该函数将其解释为unsigned char值。这意味着只有一个较低的字节用于填充目标内存块。因此,尽管代码作者可能天真地希望memset(ptr, 0xDEADBEEF, size)可以编译并填充目标内存区域的值为0xDEADBEEF,但实际上不会这样做。memset(ptr, 0xDEADBEEF, size)等同于memset(ptr, 0xEF, size)(假设使用8位字符)。虽然这可能足以填充某些内存区域中的意图“垃圾”数据,但像memset(ptr, NULL, size)memset(ptr, 0xDEADBEEF, size)这样的内容仍然暴露了代码作者严重的职业素养问题。

再一次强调,如其他答案所指出的那样,在这里的想法是用“垃圾”值填充未使用的内存。在这种情况下,零绝对不是一个好主意,因为它不够“垃圾”。当使用memset时,您只能使用一个字节的值,如0xAB0xEF。如果这对您的目的足够好,请使用memset。如果您想要一个更具表现力和独特的“垃圾”值,例如0xDEDABEEF0xBAADFOOD,则无法使用memset。您需要编写一个专用函数来填充内存区域的4字节模式。

  • C和C++中的指针不能被分配任意整数值(除了空指针常量,即零)。只能通过显式转换将整数值强制转换为指针来实现此类分配。从正式意义上讲,这样的强制转换的结果是实现定义的。结果值当然可以指向有效数据。


  • 通常情况下,你的答案1是正确的,答案2是错误的,但是在一些实现中可能会使用32位字符,这样memset(ptr, 0xDEADBEEF, size)就意味着将ptr指向的内存块填充为0xDEADBEEF。此外,在某些平台上存在对齐要求,这样解引用0xDEADBEEF可能会导致失败(而且实现可能不会生成非对齐指针)。 - skyking
    @skyking:我的第二点没有任何“错误”。同时,32位字符的实现是个好点子。 - AnT stands with Russia
    这要看你所说的“正常和有效数据”的意思而定,我表达得有点笨拙。很明显,在大多数平台上,0xDEADBEEF不能指向所需或编译器选择(所需)驻留在对齐地址上的数据。这也取决于“有效数据”的含义——如果你只需要能访问数据而不出现分段错误,那么在x86平台上就可以通过,但如果你需要确保数据实际上是有效的,则情况就比较少见,因为它需要是 boolchar 类型。 - skyking
    此外,有人可能会认为x86_64现在更加普遍,那么您最终将得到一个完整的64位值,该值不是有效的地址。 - skyking

    10

    写入0xDEADBEEF或其他非零位模式是一个好主意,可以捕获写后删除和读后删除的使用情况。

    1) 写后删除

    通过写入特定模式,您可以检查已经被释放的块是否由有缺陷的代码后续写入;在我们的调试内存管理器中,我们使用一个块的空闲列表,并在回收内存块之前检查自定义模式是否仍然覆盖整个块。当然,当我们发现问题时,它有点“晚”,但仍然比不进行检查要早得多。 此外,我们有一个特殊的函数,定期调用它,也可以按需调用它,只需遍历所有已释放的内存块的列表并检查它们的一致性,因此在追踪错误时我们可以经常调用此函数。使用0x00000000作为值不会像使用0xDEADBEEF那样有效,因为零可能恰好是有缺陷的代码想要写入已释放的块中的值,例如将字段清零或将指针设置为NULL(而有缺陷的代码想要写入0xDEADBEEF的可能性则更小)。

    2) 读后删除

    保留已释放块的内容或甚至仅写入零会增加某人读取死内存块内容时仍会发现值合理且符合不变量(例如,在许多架构上,NULL指针只是二进制零、整数0、ASCII NUL字符或双精度值0.0)的可能性。 相反,通过编写“奇怪”的模式,如0xDEADBEEF,大多数以只读模式访问这些字节的代码可能会发现异常不合理的值(例如,整数-559038737或双精度值-1.1885959257070704e + 148),希望触发其他自我一致性检查断言。

    当然,0xDEADBEEF位模式并非特定于任何内容,实际上我们对已释放的块、块前后区域使用不同的模式,同时内存管理器在将任何内存块分配给应用程序之前,还会向其内容部分写入另一个(依赖于地址)特定的位模式,以帮助发现未初始化内存的使用。


    9
    我一定会推荐使用0xDEADBEEF,它可以清晰地识别未初始化的变量和指针。如果在PowerPC架构上加载字时奇数解引用0xdeadbeef指针将肯定崩溃,在其他架构上也很可能因为内存位于进程地址空间之外而崩溃。
    清零内存是一种方便的方法,因为许多结构/类具有使用0作为其初始值的成员变量,但我非常建议在构造函数中初始化每个成员,而不是使用默认的内存填充。你真的需要掌握你是否正确初始化了变量。

    3
    我没有给这条评论点“踩”,但我认为“内存可能在进程的地址空间之外”的说法是完全错误的。在任何32位体系结构中,“0xDEADBEEF”都定义在进程地址空间范围之内。 - AnT stands with Russia
    然而,通常在零页中取消引用指针会导致segfault,但对于0xDEADBEEF可能并非如此。取消引用可能导致错误的唯一原因是它不是4位对齐的(这也可能是PPC不喜欢它的原因)。 - Jasper Bekkers
    @Jasper:对于解引用0也可能不是真的,并且在Windows上会导致错误,因为该内存不属于该进程。然而,重点不仅仅是强制产生段错误。它是为了创建一个值,当出现错误并且你正在跟踪它时,它会说“这是未初始化的”。如果那个错误恰好是段错误,那么很好,你早早地捕捉到了它。 - Dennis Zickefoose

    6

    http://en.wikipedia.org/wiki/Hexspeak

    这些“神奇”的数字是用于调试,以识别坏指针、未初始化的内存等问题。您需要一个在正常执行期间不太可能出现的值,并且在进行内存转储或检查变量时可见。在这方面,初始化为零不太有用。我猜当您看到人们将其初始化为零时,是因为他们需要该值为零。值为0xDEADBEEF的指针可能指向有效的内存位置,因此使用它作为NULL的替代方案是一个糟糕的想法。

    5
    “0xDEADBEEF”是否可以指向有效的内存取决于具体实现:例如在Windows下它不能指向有效的内存(因为Windows将内核代码映射到该地址)。而且它是未对齐的,通常不能指向一个对象的开头。它是未初始化内存的良好选择(与空指针相比——你对两者的区分很好)。 - James Kanze

    4

    将缓冲区清空或设置为特殊值的一个原因是您可以在调试器中轻松判断缓冲区内容是否有效。

    解引用数值为“0xDEADBEEF”的指针几乎总是危险的(可能会导致程序/系统崩溃),因为在大多数情况下,您不知道存储在那里的内容是什么。


    1
    这并不是因为存储在地址0xDEADBEEF处的内容本质上具有神秘或危险性,而是该地址不太可能是您的虚拟地址空间的一部分,从而导致内存访问冲突/SIGSEGV或类似问题。尽管如此,在我的经验中,与将指针加载为DEADBEEF相比,更常见的是使用DEADBEEF覆盖内存内容,当然,一个指向被覆盖结构的指针也会间接地变成DEADBEEF。 - Tony Delroy
    2
    ......当然,memset不能用于使用4字节模式重写内存区域。 - AnT stands with Russia

    1
    DEADBEEF是HexSpeek的一个例子。作为程序员,你可以有意地传达错误条件。

    我已经知道了,我的问题是是否应该在未初始化的内存中使用0xDEADBEEF而不是null。 - Skyler Saleh
    对此进行负面评价有点过了;从你的问题中并不清楚你已经知道这一点。虽然这显然不是你问题的答案,但我认为这仍然是有用且符合主题的信息。 - jeroenh

    1
    我个人建议使用NULL(或0x0),因为它表示预期的NULL,并在比较时非常方便。想象一下,你正在使用char *,并且由于某种原因在DEADBEEF之间(不知道为什么),那么至少你的调试器会非常方便地告诉你它是0x0。

    1
    问题在于具有有效值0x00的数据量。当您在调试器中看到0xDEADBEEF或其残留物时,您知道出了问题。但是当您看到大量的0时,您就毫无头绪了。 - mattnz

    1

    我会选择NULL,因为它比后期遍历并将所有指针设置为0xDEADBEEF更容易批量清零内存。此外,在x86上,没有任何东西可以阻止0xDEADBEEF成为有效的内存地址-尽管这可能不寻常,但远非不可能。 NULL更可靠。

    最终看起来- NULL是语言约定。 0xDEADBEEF只是看起来漂亮而已。你得不到任何好处。库将检查NULL指针,它们不会检查0xDEADBEEF指针。在C ++中,零指针的概念甚至与零值无关,仅用文字零表示,在C ++ 0x中有一个nullptr和一个nullptr_t


    清零内存将增加未被察觉的读或写已删除数据后的几率。 - 6502
    1
    库检查NULL指针的事实就是为什么你可能不应该这样做的原因,除非你100%确定行为将继续到发布版本。如果你忘记初始化一些数据,周围的代码将高兴地忽略它,因为你可能本来就打算将它初始化为零。然后,你切换到发布版本,它回归到实际上看起来像未初始化的数据,你所有的if(!p)检查变得毫无价值。只要确保选择一个系统保证无效的值;0通常不是唯一的选择。 - Dennis Zickefoose

    0

    如果这篇文章对于StackOverflow来说太主观了,请投票让我下线。但是我认为整个讨论的问题是我们用来制作软件的工具链中存在一个明显的漏洞的症状。

    通过使用“垃圾值”初始化内存来检测未初始化变量只能检测某些数据中的某些错误。

    在调试版本中检测未初始化的变量,但在发布版本中不检测,就像只在测试飞机时遵循安全程序,并告诉乘客“好吧,测试结果还可以”一样。

    我们需要硬件支持来检测未初始化的变量。就像每个可寻址的内存实体(大多数机器上的字节)都伴随着一个“无效”位一样,由操作系统在VirtualAlloc()(等同于其他操作系统上的等效函数)分配给应用程序的每个字节中设置,并且在写入字节时自动清除,但如果首先读取则会引发异常。

    现在的内存足够便宜,处理器足够快。这种方法不仅可以消除对“有趣”模式的依赖,而且可以使我们保持诚实。


    这种硬件支持取决于软件编写正确以利用它。通常情况下,硬件无法知道变量何时从初始化变为未初始化,因为这是一个软件概念。此外,您正在谈论极其非平凡的资源量专门用于此。即使只有一个“脏”位,现代系统上的物理内存也需要增加12.5%,而且可能需要更多的虚拟内存,因为您不能仅要求Windows将9位写入硬盘。 - Dennis Zickefoose
    在调试现代软件时,使用了许多级别的错误检测。这只是其中之一。没有人将其描述为解决所有问题的方法或“唯一正确”的方式。通常,它只是安全措施系统中的一个步骤。尽管这个单一步骤相当简单,但已经证明它非常有效。这一措施的本质使其更适用于调试版本(虽然我可以看到有时它也适用于发布版本)。 - AnT stands with Russia
    关于Andrey的评论:毫无疑问,垃圾填充是有用的 - 当然它是。但我很贪心:我想要更多。关于Dennis的评论:我的意图不是设计硬件解决方案,但可以想象每4K块发出一个单一指令,由操作系统在malloc/virtualalloc等上执行;“无效”位对软件来说是不可见的。这将花费12.5%的内存 - 对于这种好处来说听起来很便宜。 - jeff slesinger
    是的,它只能检测到一些错误,但另一方面它确实可以检测到一些错误,而且这些错误通常相当严重。请注意,使用“垃圾值”并不是为了提高运行时安全性 - 而是为了更有可能发现故障。与飞机的类比是在测试飞行期间模拟故障(例如关闭一个电机) - 这更像是遵循安全程序进行测试,而是故意破坏它们以查看是否仍然可以工作。 - skyking

    0
    请注意,memset 中的第二个参数应该是一个字节,也就是说它会被隐式转换为 char 或类似类型。对于大多数平台,0xDEADBEEF 会转换为 0xEF(对于一些奇怪的平台可能会有其他结果)。
    另外,请注意第二个参数应该正式地是一个 int,而 NULL 不是。
    现在来看看这种初始化的优点。首先,行为更有可能是确定性的(即使我们最终进入未定义的行为,实际上行为也是一致的)。
    具有确定性行为意味着调试变得更容易,当您发现错误时,您只需要提供相同的输入,故障就会显现出来。
    现在,当您选择要使用的值时,您应该选择一个最有可能导致错误行为的值 - 这意味着使用未初始化的数据更有可能导致观察到故障。这意味着您必须使用有关所涉及平台的某些知识(但其中许多平台的行为非常相似)。

    如果内存用于存储指针,那么清除内存确实意味着会得到一个NULL指针,并且通常对其进行间接引用将导致分段错误(这将被观察为故障)。然而,如果您以另一种方式使用它,例如作为算术类型,那么您将获得0 ,对于许多应用程序来说,这不是奇怪的数字。

    如果您改用0xDEADBEEF,则将获得相当大的整数,即使在将数据解释为浮点数时,它也将是相当大的数字(如我所知)。如果将其解释为文本,则它将非常长,并且包含非ASCII字符,如果使用UTF-8编码,则可能无效。现在,如果在某些平台上将其用作指针,则会因某些类型的对齐要求而失败-在某些平台上,该内存区域可能已映射出(请注意,在x86_64上,指针的值将为0xDEADBEEFDEADBEEF,这超出了地址范围)。

    请注意,虽然填充0xEF 将具有相似的属性,但如果要用0xDEADBEEF 填充内存,则需要使用自定义函数,因为memset 无法做到这一点。


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