我需要一位真正的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->数据真的为NULL,那么任何人都可以想到为什么第一个代码可以传递if-statement而不会崩溃的任何原因?这对我来说完全是神秘的。它似乎没有确定性。
重要的额外信息:
这两条评论之间的代码实际上是完整,没有遗漏任何内容。此外,应用程序是单线程,因此没有其他线程可以意外地改变后台中的任何内存。发生这种情况的平台是PPC CPU(G4,如果可以发挥任何作用)。如果有人想知道“善意”,这是因为“信息”包含一个名为“kind”的“union”,而name又是一个struct(kind是一个union,每个可能的union值都是不同类型的struct);但这一切在这里都不重要。
我很感激这里有任何想法。如果这不仅仅是一种理论,我会更感激,但如果有办法我可以验证这种理论真的适用于客户。
我已经接受了正确的答案,但万一有人在Google上发现了这个问题,这就是真正发生的事情:
指针指向已经释放的内存。释放内存不会使其全部为零或导致进程立即将其返回系统。因此,即使错误地释放了内存,它也包含正确的值。在执行“ if check ”时,有问题的指针不为NULL。
在那之后我分配一些新的内存,调用malloc。不确定malloc究竟是做什么的,但每次调用malloc或free都会对进程的虚拟地址空间的所有动态内存产生深远的影响。在malloc调用之后,指针实际上是NULL。不知何故,malloc(或某些系统调用malloc使用)将指针本身所在的已释放内存归零(不是它指向的数据,指针本身在动态内存中)。归零该内存,指针现在的值为0x0,在我的系统上等于NULL,并且当调用strlcpy时,它当然会崩溃。
因此导致这种奇怪行为的真正错误是在我的代码中完全不同的位置。永远不要忘记:释放内存可以保持它的价值,但是你无法控制多久。要检查您的应用是否存在访问已释放内存的内存错误,只需确保释放内存在释放之前始终为零。在OS X中,您可以通过在运行时设置环境变量来完成此操作(无需重新编译任何内容)。当然,这会使程序运行速度变慢,但是你会更早地发现这些错误。
答案 0 :(得分:13)
首先,取消引用空指针是未定义的行为。它可能会崩溃,而不会崩溃,或者将壁纸设置为海绵宝宝的照片。
也就是说,取消引用空指针通常会导致崩溃。所以你的问题可能与内存损坏有关,例如从写过你的一个字符串的结尾。这可能会导致延迟效果崩溃。我特别怀疑,因为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
为空,但信息 - > kind.name->长度为100,那么此语句相当于:
if (*(information->kind.name->data + 100) != '\0') {
哪个不取消引用NULL而是取消引用地址100.如果这不会崩溃,并且地址100恰好包含0,则此测试将通过。
答案 1 :(得分:11)
结构可能位于已free()
'的内存中,或者堆已损坏。在这种情况下,malloc()
可能会修改内存,认为它是免费的。
您可以尝试在内存检查器下运行程序。一个支持Mac OS X的内存检查器是valgrind,虽然它仅支持Intel上的Mac OS X,而不支持PowerPC。
答案 2 :(得分:5)
据我所知,标准未定义取消引用空指针的效果。
根据C标准6.5.3.2/4:
如果为指针指定了无效值,则无法确定一元*运算符的行为。
所以可能会发生崩溃或者可能没有。
答案 3 :(得分:3)
您可能遇到堆栈损坏。您所指的代码行可能根本没有被执行。
答案 4 :(得分:2)
我的理论是information->kind.name->length
是一个非常大的值,因此information->kind.name->data[information->kind.name->length]
实际上是指有效的内存地址。
答案 5 :(得分:1)
标准未定义取消引用NULL指针的行为。它不能保证崩溃,除非你真的尝试写入内存,否则往往不会崩溃。
答案 6 :(得分:1)
作为一个FYI,我看到这一行:
if (information->kind.name->data[information->kind.name->length] != '\0') {
我最多可以看到三个不同的指针解除引用:
您检查非空信息,但不检查名称而不是数据。是什么让你如此确定它们是正确的?
我也在这里回应其他一些可能早先破坏你的堆的事情。如果您在Windows上运行,请考虑使用gflags来执行页面分配等操作,这可用于检测您或其他人是否正在写入缓冲区的末尾并踩到堆上。
看到你在Mac上 - 忽略gflags评论 - 它可能会帮助其他读这篇文章的人。如果您运行的是早于OS X的东西,可以使用一些方便的Macsbug工具来强调堆(比如堆scramble命令,'hs')。
答案 7 :(得分:1)
我对strlcpy调用中的char * cast感兴趣。
类型数据*的大小可能与系统上的char *不同吗?如果char指针较小,则可以获得数据指针的子集,该子集可能为NULL。
示例:
int a = 0xffff0000;
short b = (short) a; //b could be 0 if lower bits are used
修改:更正了拼写错误。
答案 8 :(得分:1)
这是您可以通过
中的'数据'指针为NULL的一种特定方式if (information->kind.name->data[information->kind.name->length] != '\0') {
说出信息 - > kind.name->长度很大。至少大于 4096,在具有特定编译器的特定平台上(比如说,大多数*带有库存gcc编译器的nixes),代码将导致内存读取“kind.name-> data + information-> kind.name-的地址>长度]。
在较低级别,该读取是“在地址(0 + 8653)处读取存储器”(或无论长度如何)。 在* nixes上常见的是将地址空间中的第一页标记为“不可访问”,这意味着取消引用读取内存地址0到4096的NULL指针将导致硬件陷阱传播到应用程序并使其崩溃。
阅读第一页后,您可能会碰到有效的映射内存,例如:一个共享库或恰好映射到那里的其他东西 - 内存访问不会失败。那没关系。取消引用NULL指针是未定义的行为,没有任何要求它失败。
答案 9 :(得分:1)
在最后一个if语句之后缺少'{'意味着“// ...上面的代码跳过,不相关......”部分中的某些内容正在控制对整个代码片段的访问。在粘贴的所有代码中,只执行strlcpy。解决方案:永远不要使用没有大括号的if语句来阐明控制。
考虑一下......
if(false)
{
if(something == stuff)
{
doStuff();
.. snip ..
if(monkey == blah)
some->garbage= nothing;
return -1;
}
}
crash();
只有“崩溃();”被执行。
答案 10 :(得分:1)
我会在valgrind下运行您的程序。您已经知道NULL指针存在问题,因此请对该代码进行概要分析。
valgrind在这里的优势在于它检查每个指针引用并检查以前是否已声明该内存位置,并且它将告诉您行号,结构以及您需要了解的有关内存的任何其他内容
正如其他人提到的那样,引用0记忆位置是一个“雀血清,血清”的事情。
我的狡猾的蜘蛛感告诉我你应该打破
上的那些结构散步if (information->kind.name->data[information->kind.name->length] != '\0') {
像
一样 if (information == NULL) {
return -1;
}
if (information->kind == NULL) {
return -1;
}
等等。
答案 11 :(得分:0)
如果信息和数据是好指针(非空)会发生什么,但是information.kind.name为null。在strlcpy行之前不要取消引用此指针,因此如果它为null,则在此之前它可能不会崩溃。当然,比你早做取消引用数据[1]将它设置为\ 0,它也应该崩溃,但是由于无论什么侥幸,你的程序可能恰好具有对0x01但不是0x00的写访问权。
另外,我看到你在一个地方使用信息 - > name.length但在另一个地方使用信息 - > kind.name.length,不确定这是否是错字或者是否需要。
答案 12 :(得分:0)
尽管取消引用空指针会导致未定义的行为而不一定会导致崩溃,但您应该检查information->kind.name->data
的值而不是information->kind.name->data[1]
的内容。
答案 13 :(得分:0)
char * p = NULL;
p [i]就像
p += i;
这是一个有效的操作,即使在nullpointer上也是如此。然后它指向内存位置0x0000 [...] i
答案 14 :(得分:0)
无论如何,您应该始终检查信息 - > kind.name->数据是否为空,但在这种情况下
在
if (*result == NULL)
freeParsedData(information);
return -1;
}
你错过了一个{
应该是
if (*result == NULL)
{
freeParsedData(information);
return -1;
}
这是编码风格的一个很好的理由,而不是
if (*result == NULL) {
freeParsedData(information);
return -1;
}
你可能没有发现缺少的大括号,因为你习惯了代码块的形状而没有大括号将它与if子句分开。
答案 15 :(得分:0)
* result = malloc(realLength); // ???
新分配的内存段的地址存储在变量“result”中包含的地址引用的位置。
这是意图吗?如果是这样,可能需要修改strlcpy。
答案 16 :(得分:-1)
根据我的理解,此问题的特殊情况是使用Null指针尝试读取或写入导致的无效访问。这里问题的检测非常依赖于硬件。在某些平台上,使用NULL指针访问内存以进行读取或写入将导致异常。