13

我需要一位真正的 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。我的代码这里显然有一个bug,它没有检查名称数据是否为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,以防万一)。如果有人想知道“kind.”,这是因为“信息”包含一个名为“kind”的“联合”,而 name 又是一个结构(kind 是一个联合,每个可能的联合值都是不同类型的结构);但这一切在这里都不重要。

我很感激这里的任何想法。如果这不仅仅是一个理论,我会更加感激,如果有一种方法可以验证这个理论是否真的适用于客户。

解决方案

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

指针指向已经被释放的内存。释放内存不会使其全部为零或导致进程立即将其还给系统。因此,即使内存被错误地释放,它也包含正确的值。在执行“ if check ”时,所讨论的指针不为 NULL 。

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

所以导致这种奇怪行为的真正错误是在我的代码中完全不同的位置。永远不要忘记:释放的内存会保持它的价值,但它会持续多久是你无法控制的。要检查您的应用程序是否存在访问已释放内存的内存错误,只需确保释放的内存在释放之前始终为零。在 OS X 中,您可以通过在运行时设置环境变量来做到这一点(无需重新编译任何东西)。当然,这会大大减慢程序的速度,但您会更早地发现这些错误。

4

17 回答 17

13

First, dereferencing a null pointer is undefined behavior. It can crash, not crash, or set your wallpaper to a picture of SpongeBob Squarepants.

That said, dereferencing a null pointer will usually result in a crash. So your problem is probably memory corruption-related, e.g. from writing past the end of one of your strings. This can cause a delayed-effect crash. I'm particularly suspicious because it's highly unlikely that malloc(1) will fail unless your program is butting up against the end of its available virtual memory, and you would probably notice if that were the case.

Edit: OP pointed out that it isn't result that is null but information->kind.name->data. Here's a potential issue then:

There is no check for whether information->kind.name->data is null. The only check on that is

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

Let's assume that information->kind.name->data is null, but information->kind.name->length is, say, 100. Then this statement is equivalent to:

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

Which does not dereference NULL but rather dereferences address 100. If this does not crash, and address 100 happens to contain 0, then this test will pass.

于 2009-08-26T14:14:47.767 回答
11

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

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

于 2009-08-26T14:37:41.300 回答
5

据我所知,取消引用空指针的效果未按标准定义。

根据 C 标准 6.5.3.2/4:

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

所以可能会发生崩溃,也可能不会。

于 2009-08-26T14:10:30.597 回答
3

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

于 2009-08-26T14:06:32.550 回答
2

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

于 2009-08-26T14:20:23.280 回答
1

Here's one specific way you can get past the 'data' pointer being NULL in

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

Say information->kind.name->length is large. Atleast larger than 4096, on a particular platform with a particular compiler (Say, most *nixes with a stock gcc compiler) the code will result in a memory read of "address of kind.name->data + information->kind.name->length].

At a lower level, that read is "read memory at address (0 + 8653)" (or whatever the length was). It's common on *nixes to mark the first page in the address space as "not accessible", meaning dereferencing a NULL pointer that reads memory address 0 to 4096 will result in a hardware trap being propagated to the application and crash it.

Reading past that first page, you might happen to poke into valid mapped memory, e.g. a shared library or something else that happened to be mapped there - and the memory access will not fail. And that's ok. Dereferencing a NULL pointer is undefined behavior, nothing requires it to fail.

于 2009-08-26T15:24:00.770 回答
1

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

类型 data* 的大小是否与系统上的 char* 不同?如果 char 指针较小,您可以获得可能为 NULL 的数据指针子集。

例子:

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

编辑:拼写错误已更正。

于 2009-08-26T15:06:55.560 回答
1

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

valgrind 的优势在于它检查每个指针引用并检查该内存位置是否先前已声明,它会告诉您行号、结构以及您关心的有关内存的任何其他信息。

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

我的 C 色感觉告诉我,你应该打破那些结构走在

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

线状

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

等等。

于 2009-08-26T17:47:20.590 回答
1

在最后一个 if 语句之后缺少 '{' 意味着“// ... 上面的代码已跳过,不相关 ...” 部分中的某些内容正在控制对整个代码片段的访问。在所有粘贴的代码中,只有 strlcpy 被执行。解决方案:永远不要使用没有大括号的 if 语句来阐明控制。

考虑这个...

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

    .. snip ..

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

只有“崩溃();” 被执行。

于 2009-08-26T16:11:44.057 回答
1

The act of dereferencing a NULL pointer is undefined by the standard. It is not guaranteed to crash and often times won't unless you actually try and write to the memory.

于 2009-08-26T14:12:10.843 回答
1

作为一个仅供参考,当我看到这一行时:

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

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

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

您检查非空信息,但不是名称和数据。是什么让你如此确定他们是正确的?

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

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

于 2009-08-26T14:36:52.893 回答
0

您应该始终检查 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 子句分开的代码块的形状。

于 2009-08-26T15:58:10.870 回答
0

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

于 2009-08-26T14:25:57.047 回答
0

Wow, thats strange. One thing does look slightly suspicious to me, though it may not contribute:

What would happen if information and data were good pointers (non null), but information.kind.name was null. You don't dereference this pointer until the strlcpy line, so if it was null, it might not crash until then. Of course, earlier than t hat you do dereference data[1] to set it to \0, which should also crash, but due to whatever fluke, your program may just happen to have write access to 0x01 but not 0x00.

Also, I see you use information->name.length in one place but information->kind.name.length in another, not sure if thats a typo or if thats desired.

于 2009-08-26T14:13:48.283 回答
0
char * p = NULL;

p[i] 就像

p += i;

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

于 2009-08-26T14:42:16.863 回答
0

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

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

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

于 2009-08-26T16:53:33.863 回答
-1

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

于 2012-07-16T08:53:44.313 回答