分段错误的常见原因的确定列表

时间:2015-10-09 21:31:42

标签: c++ c segmentation-fault

  

注意:我们有很多段错误问题,大致相同   答案,所以我试图将它们折叠成一个典型的问题   我们有undefined reference

     

虽然我们有一个涵盖what a segmentation fault is的问题,但它涵盖了内容,但没有列出很多原因。最好的答案是"有很多原因",只列出一个,而其他大多数答案都没有列出任何理由。

     

总而言之,我认为我们需要一个组织良好的社区wiki 来讨论这个主题,其中列出了所有常见原因(以及一些原因)来获取段错误。目的是帮助调试,如答案的免责声明中所述。

我知道分段错误是什么,但是如果不知道它们通常是什么样子,就很难在代码中找到它。毫无疑问,在C和C ++中,无法详尽列出,分段错误的最常见原因是什么

1 个答案:

答案 0 :(得分:66)

  

警告!

     

以下是潜在分段错误的原因。 几乎不可能列出所有原因。此列表的目的是帮助诊断现有的段错误。

     

分段错误和未定义行为之间的关系不能足够强调!以下所有可能造成分段错误的情况都是技术上未定义的行为。这意味着他们可以执行任何,而不仅仅是段错误 - 就像有人曾经在USENET上说的那样,&#34 ; it is legal for the compiler to make demons fly out of your nose.&#34 ;.每当你有不明确的行为时,不要指望发生段错误。您应该了解C和/或C ++中存在哪些未定义的行为,并避免编写具有它们的代码!

     

有关未定义行为的更多信息:

     

什么是Segfault?

简而言之,当代码尝试访问无权访问的内存时,会导致分段错误。每个程序都有一块内存(RAM)可供使用,出于安全考虑,只允许访问该块中的内存。

有关 的细分错误的更全面的技术说明,请参阅What is a segmentation fault?

以下是分段错误错误的最常见原因。同样,这些应该用于诊断现有的段错误。要了解如何避免它们,请了解您的语言未定义的行为

此列表无法替代自己的调试工作。 (请参阅答案底部的该部分。)这些是您可以查找的内容,但您的调试工具是解决问题的唯一可靠方法。

访问NULL或未初始化的指针

如果你的指针是NULL(ptr=0)或者是完全未初始化的指针(它还没有设置为任何东西),那么尝试使用该指针进行访问或修改时会有未定义的行为。

int* ptr = 0;
*ptr += 5;

由于分配失败(例如使用mallocnew)将返回空指针,因此在使用之前应始终检查指针是否为NULL。

另请注意,即使读取值(未取消引用)未初始化的指针(以及一般的变量)也是未定义的行为。

有时,对未定义指针的访问可能非常微妙,例如在尝试将此指针解释为C打印语句中的字符串时。

char* ptr;
sprintf(id, "%s", ptr);

另见:

访问悬空指针

如果您使用mallocnew分配内存,然后通过指针使用freedelete内存,则该指针现在被视为悬空指针即可。取消引用它(以及简单地读取它的值 - 授予你没有为它分配一些新值,如NULL)是未定义的行为,并且可能导致分段错误。

Something* ptr = new Something(123, 456);
delete ptr;
std::cout << ptr->foo << std::endl;

另见:

堆栈溢出

[不,不是您现在所使用的网站,名为的内容是什么。]过度简化,&#34;堆栈&#34;就像那个秒杀你在一些食客中贴上你的订单纸。当您在该峰值上放置太多订单时,可能会发生此问题。在计算机中,任何未动态分配的变量和任何尚未由CPU处理的命令都会进入堆栈。

这可能是深度或无限递归的一个原因,例如当函数调用自身而无法停止时。因为那个堆栈已经溢出,订单文件开始&#34;掉落&#34;并占用其他不适合他们的空间。因此,我们可以得到分段错误。另一个原因可能是尝试初始化一个非常大的数组:它只是一个单一的顺序,但它本身已经足够大了。

int stupidFunction(int n)
{
   return stupidFunction(n);
}

堆栈溢出的另一个原因是一次有太多(非动态分配)变量。

int stupidArray[600851475143];

野外堆栈溢出的一种情况来自于条件中的return语句的简单省略,旨在防止函数中的无限递归。这个故事的寓意,始终确保您的错误检查工作!

另见:

狂野指针

在内存中创建指向某个随机位置的指针就像在您的代码中使用俄罗斯轮盘赌一样 - 您很容易错过并创建指向您无法访问权限的位置的指针。

int n = 123;
int* ptr = (&n + 0xDEADBEEF); //This is just stupid, people.

作为一般规则,不要创建指向文字内存位置的指针。即使他们工作一次,下次他们可能也不会。您无法预测程序在任何执行时的记忆位置。

另见:

尝试读取数组末尾

数组是连续的内存区域,其中每个连续元素位于内存中的下一个地址。但是,大多数数组都不具备与它们有多大或与最后一个元素相关的先天感。因此,很容易超越数组的末尾并且永远不会知道它,特别是如果你使用指针算法。

如果你读过数组的末尾,你可能会进入未初始化或属于其他东西的内存。这在技术上是未定义的行为。段错误只是众多潜在的未定义行为之一。 [坦率地说,如果你在这里遇到段错,你很幸运。其他人更难诊断。]

// like most UB, this code is a total crapshoot.
int arr[3] {5, 151, 478};
int i = 0;
while(arr[i] != 16)
{
   std::cout << arr[i] << std::endl;
   i++;
}

或者常见的for使用<=代替<(读取1个字节太多):

char arr[10];
for (int i = 0; i<=10; i++)
{
   std::cout << arr[i] << std::endl;
}

甚至是一个不太好的拼写错误(编译here)并且只分配了1个用dim而不是dim元素初始化的元素。

int* my_array = new int(dim);

另外应该注意的是,甚至不允许你创建(更不用说解除引用)一个指向数组外部的指针(只有当它指向数组中的一个元素时才能创建这样的指针,或者只有结束)。否则,您将触发未定义的行为。

另见:

忘记C字符串上的NUL终止符。

C字符串本身就是具有一些其他行为的数组。它们必须为空终止,这意味着它们最后有一个\0,可以可靠地用作字符串。这在某些情况下会自动完成,而在其他情况下则不会。

如果忘记这一点,一些处理C字符串的函数永远不知道何时停止,并且你可以遇到与读取数组末尾相同的问题。

char str[3] = {'f', 'o', 'o'};
int i = 0;
while(str[i] != '\0')
{
   std::cout << str[i] << std::endl;
   i++;
}

使用C字符串,\0是否会产生任何影响,确实值得一试。你应该假设它会避免未定义的行为:所以写得更好char str[4] = {'f', 'o', 'o', '\0'};

尝试修改字符串文字

如果将字符串文字指定给char *,则无法修改它。例如......

char* foo = "Hello, world!"
foo[7] = 'W';

...触发未定义的行为,并且分段错误是一种可能的结果。

另见:

不匹配的分配和释放方法

您必须同时使用mallocfreenewdelete以及new[]delete[]。如果你混淆了,你可能会遇到段错误和其他奇怪的行为。

另见:

工具链中的错误。

编译器的机器代码后端中的错误非常能够将有效代码转换为可执行的段错误。链接器中的错误也可以做到这一点。

特别可怕的是,这不是您自己的代码调用的UB。

那就是说,除非另有证明,否则你应该始终认为问题就在于你。

其他原因

分段错误的可能原因与未定义行为的数量一样多,甚至要列出的标准文档也有太多。

一些不太常见的原因需要检查:

调试

调试工具有助于诊断段错误的原因。使用调试标志(-g)编译程序,然后使用调试器运行它以查找可能发生段错误的位置。

最近的编译器支持使用-fsanitize=address进行构建,这通常会导致程序运行速度降低约2倍,但可以更准确地检测地址错误。但是,此方法不支持其他错误(例如从未初始化的内存中读取或泄漏非内存资源(如文件描述符)),并且不可能同时使用许多调试工具和ASan。 / p>

某些内存调试程序

  • GDB | Mac,Linux
  • valgrind(memcheck)|的Linux
  • 博士。记忆|视窗

此外,建议使用静态分析工具来检测未定义的行为 - 但同样,它们只是帮助您找到未定义行为的工具,并且它们不能保证找到所有出现的未定义行为。

但是,如果你真的不走运,使用调试器(或者,更少见,只需重新编译调试信息)可能会充分影响程序的代码和内存,从而不再发生段错误,这种现象称为{ {3}}