繁体   English   中英

如何在C中取消引用NULL指针不会使程序崩溃?

[英]How can dereferencing a NULL pointer in C not crash a program?

我需要一个真正的C专家的帮助来分析我的代码中的崩溃。 不是为了解决崩溃; 我可以轻松修复它,但是在这样做之前,我想了解一下什至崩溃是可能的,因为对我来说这似乎是完全不可能的。

该崩溃仅发生在客户计算机上,并且我无法在本地复制它(因此,我无法使用调试器逐步浏览代码),因为无法获得该用户数据库的副本。 我的公司也不允许我只更改代码中的几行并为该客户进行自定义构建(因此,我无法添加一些printf行并让他再次运行代码),当然,客户拥有的构建没有调试符号。 换句话说,我的调试能力非常有限。 尽管如此,我仍然可以确定崩溃并获得一些调试信息。 但是,当我查看这些信息然后查看代码时,我无法理解程序流如何到达所讨论的行。 代码在到达该行之前应该早已崩溃。 我在这里完全迷路了。

让我们从相关代码开始。 这是很少的代码:

// ... code above skipped, not relevant ...

if (data == NULL) return -1;

information = parseData(data);

if (information == NULL) return -1;

/* Check if name has been correctly \0 terminated */
if (information->kind.name->data[information->kind.name->length] != '\0') {
    freeParsedData(information);
    return -1;
}

/* Copy the name */
realLength = information->kind.name->length + 1;
*result = malloc(realLength);
if (*result == NULL) {
    freeParsedData(information);
    return -1;
}
strlcpy(*result, (char *)information->kind.name->data, realLength);

// ... code below skipped, not relevant ...

就是这样。 它在strlcpy中崩溃。 我什至可以告诉您在运行时如何真正调用strlcpy。 实际上使用以下参数调用了strlcpy:

strlcpy ( 0x341000, 0x0, 0x1 );

知道这一点很明显,为什么strlcpy崩溃了。 它尝试从NULL指针读取一个字符,这当然会崩溃。 并且由于最后一个参数的值为1,所以原始长度必须为0。我的代码显然在这里有一个错误,它无法检查名称数据是否为NULL。 我可以解决这个问题,没问题。

我的问题是:
此代码如何首先到达strlcpy?
为什么在if语句中此代码不会崩溃?

我在机器上本地尝试过:

int main (
    int argc,
    char ** argv
) {
    char * nullString = malloc(10);
    free(nullString);
    nullString = NULL;

    if (nullString[0] != '\0') {
        printf("Not terminated\n");
        exit(1);
    }
    printf("Can get past the if-clause\n");

    char xxx[10];
    strlcpy(xxx, nullString, 1);
    return 0;   
}

此代码永远不会传递if语句。 它在if语句中崩溃,这绝对是预期的。

那么,任何人都可以想到任何原因,使name-> data确实为NULL的情况下,第一个代码可以通过if语句传递而不会崩溃的原因吗? 这对我来说完全是神秘的。 它似乎不是确定性的。

重要的额外信息:
两个注释之间的代码非常完整 ,没有遗漏任何内容。 此外,该应用程序是单线程的 ,因此没有其他线程可以意外更改后台的任何内存。 发生这种情况的平台是PPC CPU(如果可以扮演任何角色,则为G4)。 如果有人想知道“种类”,这是因为“信息”包含一个名为“种类”的“联盟”,并且名称又是一个结构(种类是一个联合,每种可能的联合值都是不同类型的结构); 但这一切在这里都不重要。

我对这里的任何想法表示感谢。 如果这不仅仅是一种理论,我将不胜感激,但是如果有一种方法可以验证这一理论对客户是否真的成立,我将不胜感激。

我已经接受了正确的答案,但是以防万一有人在Google上发现此问题,这是真正发生的事情:

指针指向已经释放的内存。 释放内存不会使内存全部为零,也不会导致进程立即将其返回给系统。 因此,即使错误地释放了内存,它仍包含正确的值。 在执行“ 如果检查 ”时,所讨论的指针不是NULL。

在检查之后,我分配了一些新的内存,调用了malloc。 不确定malloc到底在做什么,但是对malloc或free的每次调用都会对进程的虚拟地址空间的所有动态内存产生深远的影响。 在malloc调用之后,指针实际上为NULL。 malloc(或malloc使用的某些系统调用)以某种方式将指针本身所在的已释放内存(不是指针指向的数据,指针本身在动态内存中)清零。 将内存归零后,指针现在的值为0x0,在我的系统上等于NULL,并且在调用strlcpy时,它当然会崩溃。

因此,导致这种奇怪行为的真正错误是在我的代码中一个完全不同的位置。 永远不会忘记:释放的内存可以保持其价值,但是它超出了您的控制范围。 要检查您的应用是否存在访问已释放的内存的内存错误,只需确保释放的内存在释放之前始终为零即可。 在OS X中,您可以通过在运行时设置环境变量来完成此操作(无需重新编译任何内容)。 当然,这会使程序变慢很多,但是您会更早地发现这些错误。

首先,取消引用空指针是未定义的行为。 它可能会崩溃,而不是崩溃,或者将您的壁纸设置为海绵宝宝的图片。

也就是说,取消引用空指针通常会导致崩溃。 因此,您的问题可能与内存损坏有关,例如,由于编写超出了字符串之一的末尾。 这可能会导致延迟效果崩溃。 我尤其感到怀疑,因为除非您的程序紧接其可用虚拟内存的末尾,否则malloc(1)不太可能失败,并且您可能会注意到这种情况。

编辑:OP指出不是结果为null,而是information->kind.name->data 那么这是一个潜在的问题:

不检查information->kind.name->data是否为空。 唯一的检查是

if (information->kind.name->data[information->kind.name->length] != '\0') {

假设information->kind.name->data为null,但是information-> kind.name-> length为100。那么此语句等效于:

if (*(information->kind.name->data + 100) != '\0') {

它不会取消引用NULL,而是取消引用地址100。如果这不会崩溃,并且地址100恰好包含0,则此测试将通过。

该结构可能位于已被free()内存中,或者堆已损坏。 在那种情况下, malloc()可能正在修改内存,并认为它是空闲的。

您可以尝试在内存检查器下运行程序。 valgrind一个支持Mac OS X的内存检查器,尽管它仅在Intel上支持Mac OS X,而在PowerPC上不支持。

据我所知,标准没有定义解引用空指针的效果。

根据C标准6.5.3.2/4:

如果已将无效值分配给指针,则一元*运算符的行为未定义。

因此可能会崩溃,也可能不会崩溃。

您可能正在遇到堆栈损坏。 您所引用的代码行可能根本没有执行。

我的理论是information->kind.name->length是一个非常大的值,因此information->kind.name->data[information->kind.name->length]实际上是指有效的内存地址。

标准未定义取消引用NULL指针的行为。 除非您实际尝试写入内存,否则不能保证它会崩溃,并且通常情况下不会崩溃。

仅供参考,当我看到此行时:

if (information->kind.name->data[information->kind.name->length] != '\0') {

我看到多达三个不同的指针取消引用:

  1. 信息
  2. 名称
  3. 数据(如果是指针而不是固定数组)

您检查信息是否为非空,但不检查名称,也不检查数据。 是什么让您确保它们是正确的?

我在这里也回荡了其他观点,认为这可能会更早破坏您的堆。 如果您在Windows上运行,请考虑使用gflags执行诸如页面分配之类的操作,该操作可用于检测您或其他人是否正在写缓冲区的末尾并踩到堆上。

看到您在Mac上-忽略gflags注释-可能会帮助其他人阅读本文。 如果您在OS X之前的版本上运行,则有许多方便的Macsbugs工具可以对堆施加压力(例如,堆争夺命令“ hs”)。

我对strlcpy调用中的char *感兴趣。

类型数据*的大小可以与系统上的char *大小不同吗? 如果char指针较小,则可以获取数据指针的子集,该子集可以为NULL。

例:

int a = 0xffff0000;
short b = (short) a; //b could be 0 if lower bits are used

编辑 :拼写错误已更正。

这是您可以克服“数据”指针为NULL的一种特定方式

if (information->kind.name->data[information->kind.name->length] != '\0') {

说信息-> kind.name->长度很大。 至少大于4096,在具有特定编译器的特定平台上(例如,大多数* nixes使用通用gcc编译器),该代码将导致内存读取为“地址为kind.name-> data + information-> kind.name ->长度]。

在较低级别,该读取是“读取地址(0 + 8653)处的内存”(或任何长度)。 在* nixes上,通常会将地址空间中的第一页标记为“不可访问”,这意味着取消引用读取内存地址0至4096的NULL指针将导致硬件陷阱传播到应用程序并使之崩溃。

阅读第一页之后,您可能碰巧进入了有效的映射内存,例如共享库或碰巧被映射到那里的其他内容,并且内存访问不会失败。 没关系。 取消引用NULL指针是未定义的行为,没有要求失败的行为。

如果最后一个if语句后缺少'{',则表示“ // ...以上代码已跳过,不相关...”部分中的某些内容正在控制对整个代码片段的访问。 在所有粘贴的代码中,仅执行strlcpy。 解决方案:永远不要使用不带大括号的if语句来澄清控件。

考虑一下...

if(false)
{
    if(something == stuff)
    {
        doStuff();

    .. snip ..

    if(monkey == blah)
        some->garbage= nothing;
        return -1;
    }
}
crash();

只有“ crash();” 被执行。

我将在valgrind下运行您的程序。 您已经知道NULL指针存在问题,因此请分析该代码。

valgrind存在的优点是它检查每个单个指针引用并检查是否先前已声明该内存位置,并且它将告诉您行号,结构以及您想了解的有关内存的任何其他信息。

正如其他每个人所提到的,引用0内存位置是一件“ que sera,sera”之类的事情。

我的间谍意识在告诉我,您应该打破那些结构

if (information->kind.name->data[information->kind.name->length] != '\0') {

像线

    if (information == NULL) {
      return -1; 
    }
    if (information->kind == NULL) {
      return -1; 
    }

等等。

哇,真奇怪。 一件事对我来说似乎有点可疑,尽管可能没有帮助:

如果信息和数据是好的指针(非null),但information.kind.name为null,将会发生什么。 直到strlcpy行时才取消引用该指针,因此,如果该指针为null,则直到那时它才可能崩溃。 当然,在此之前,您需要取消引用data [1]以将其设置为\\ 0,这也会崩溃,但是由于意外,您的程序可能恰好具有对0x01的写访问权限,但对0x00的写权限没有。

另外,我看到您在一个地方使用information-> name.length,但在另一个地方使用information-> kind.name.length,不确定这是否是错字或是否需要。

尽管取消引用空指针会导致未定义的行为,并且不一定会导致崩溃,但是您应该检查information->kind.name->data的值,而不是information->kind.name->data[1]的内容。 information->kind.name->data[1]

char * p = NULL;

p [i]就像

p += i;

这是有效的操作,即使在空指针上也是如此。 然后它指向内存位置0x0000 [...] i

无论如何,您应该始终检查information-> kind.name-> data是否为空,但是在这种情况下

if (*result == NULL) 
    freeParsedData(information);
    return -1;
}

您错过了{

它应该是

if (*result == NULL)
{ 
     freeParsedData(information);
     return -1;
}

这是采用这种编码风格的一个很好的理由,而不是

if (*result == NULL) { 
    freeParsedData(information);
    return -1;
}

在这里您可能不会发现丢失的括号,因为您习惯了代码块的形状,而没有将括号与if子句分开。

*结果= malloc(realLength); // ???

新分配的内存段的地址存储在变量“结果”中包含的地址所引用的位置。

这是意图吗? 如果是这样,则可能需要修改strlcpy。

根据我的理解,此问题的特殊情况是使用Null指针尝试读取或写入而导致的无效访问。 在此问题的检测很大程度上取决于硬件。 在某些平台上,使用NULL指针访问内存以进行读取或写入将导致异常。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM