【问题标题】:Definitive List of Common Reasons for Segmentation Faults分段错误的常见原因的最终列表
【发布时间】:2020-11-05 02:49:46
【问题描述】:

注意:我们有很多 segfault 问题,大体相同 答案,所以我试图将它们折叠成一个规范的问题,比如 我们有undefined reference

虽然我们有一个涉及what a segmentation fault is 的问题,但它涵盖了什么,但没有列出很多原因。最上面的答案说“有很多原因”,并且只列出了一个,而其他大多数答案都没有列出任何原因。

总而言之,我相信我们需要一个关于这个主题的组织良好的社区 wiki,它列出了所有导致段错误的常见原因(以及一些)。目的是帮助调试,如答案的免责声明中所述。

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

【问题讨论】:

  • 我不认为旧答案很难找到,实际上我最近在网上搜索 seg 错误时(很容易)找到了它
  • 这样做的问题是它会重新强化以下“原因”将导致分段错误的神话。我们最好对 UB 进行更广泛的教育。
  • 我必须同意@LightnessRacesinOrbit,正如我在回答here 中所说的那样分段错误是实现定义的行为,该标准没有定义实现应该如何处理未定义的行为我>。为了让这点有意义,你必须谈论特定的平台和特定的编译器版本,即使那样你也是如履薄冰。
  • 话虽如此,“它是 UB”被过度调用了:只要指定了平台和操作系统,有时就值得检查我们行为的实际后果。例如“有段错误?首先检查空指针取消引用”。有些几乎是证书,比如那个;其他只是大概率。从这个意义上说,它可以很好地用作调试指南......只要它有足够的免责声明并非常仔细地解释情况! FWIW,总的来说这个问答非常好。

标签: c++ c segmentation-fault


【解决方案1】:

警告!

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

分段错误和未定义行为之间的关系怎么强调都不过分!以下所有可能导致分段错误的情况在技术上都是未定义的行为。这意味着他们可以做任何事情,而不仅仅是分段错误——正如有人曾经在 USENET 上所说,“@987654321 @"。当您有未定义的行为时,不要指望会发生段错误。您应该了解 C 和/或 C++ 中存在哪些未定义行为,并避免编写包含这些行为的代码!

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


什么是段错误?

简而言之,当代码尝试访问它无权访问的内存时,就会导致分段错误。每个程序都有一块内存 (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;

另见:


堆栈溢出

[不,不是您现在所在的网站,命名是为了什么。] 过于简单化了,“堆栈”就像您在某些食客中粘贴订单纸的尖刺。可以这么说,当您在该峰值上放置太多订单时,可能会出现此问题。在计算机中,任何非动态分配的变量和任何尚未被 CPU 处理的命令都会进入堆栈。

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

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&lt;= 而不是&lt;(读取1 个字节太多):

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

甚至是一个不幸的错字,它编译得很好(见here)并且只分配了一个用dim而不是dim元素初始化的元素。

int* my_array = new int(dim);

另外应该注意的是,您甚至不允许创建(更不用说取消引用)指向数组外部的指针(只有当它指向数组内的元素或超过结尾)。否则,您将触发未定义的行为。

另见:


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

C 字符串本身就是具有一些附加行为的数组。它们必须以 null 结尾,这意味着它们末尾有一个 \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';

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

另见:


不匹配的分配和释放方法

您必须同时使用mallocfree,同时使用newdelete,以及同时使用new[]delete[]。如果你把它们混在一起,你可能会出现段错误和其他奇怪的行为。

另见:


工具链中的错误。

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

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

也就是说,在被证明并非如此之前,您应该始终假设问题出在您身上。


其他原因

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

几个不太常见的检查原因:


调试中

首先,仔细阅读代码。大多数错误只是由拼写错误或错误引起的。确保检查分段错误的所有潜在原因。如果失败,您可能需要使用专用调试工具来找出根本问题。

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

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

一些内存调试器

  • GDB | Mac、Linux
  • valgrind(内存检查)| Linux
  • 博士。记忆 |窗户

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

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

在这种情况下,您可能想要做的是获取核心转储,并使用调试器获取回溯。

【讨论】:

  • 在什么架构上读取未初始化的内存会出现段错误?
  • 虽然通过读取数组末尾肯定有可能获得段错误,但我想说这不太可能。它必须位于 VAS 映射页面的边界上,否则您必须阅读很多到最后。
  • 这个答案。如果您至少有 100 个声望点,您可以点击“编辑”。这被称为“社区 wiki”,这意味着它由每个人维护。
  • 您不能“触发”或“导致”未定义的行为。当您编写此类代码时,代码 具有 未定义的行为。它的行为是未定义的。这是您触发的潜在症状。
  • @NathanOliver:我认为“C 字符串”就是这样:一个空终止的char 数组,旨在与 C 的字符串函数一起使用。当然不是所有的char 数组都需要以空值结尾,但我认为你不会将它们称为 C 字符串。
猜你喜欢
相关资源
最近更新 更多