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

Cod*_*e92 56 c c++ segmentation-fault

注意:我们有很多段错误问题,答案基本相同,所以我试图将它们折叠成一个规范的问题,就像我们对未定义的引用一样.

虽然我们有一个问题涉及分段错误是什么,但它涵盖了什么,但没有列出很多原因.最佳答案说"有很多原因",只列出一个,其他大多数答案都没有列出任何理由.

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

我知道什么是分段错误,但是在不知道它们通常是什么样的情况下很难发现代码.虽然毫无疑问,有太多的内容无法详尽列出, C和C++ 中分段错误的最常见原因是什么?

Cod*_*e92 68

警告!

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

分割故障和未定义行为之间的关系,不能强调不够!以下所有可能产生分段错误的情况都是技术上未定义的行为.这意味着他们可以做任何事情,而不仅仅是段错误 - 就像有人曾经在USENET上说的那样," 编译器让恶魔飞出你的鼻子是合法的. " 每当您有未定义的行为时,不要指望发生段错误.您应该了解C和/或C++中存在哪些未定义的行为,并避免编写具有它们的代码!

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


什么是Segfault?

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

有关分段错误什么是更彻底的技术解释,看什么是分段故障?.

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

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


访问NULL或未初始化的指针

如果你有一个NULL(ptr=0)指针或完全未初始化的指针(它根本没有设置为任何东西),尝试使用该指针访问或修改具有未定义的行为.

int* ptr = 0;
*ptr += 5;
Run Code Online (Sandbox Code Playgroud)

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

另请注意,即使读取未初始化指针(以及通常的变量)的值(不解除引用)也是未定义的行为.

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

char* ptr;
sprintf(id, "%s", ptr);
Run Code Online (Sandbox Code Playgroud)

也可以看看:


访问悬空指针

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

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

也可以看看:


堆栈溢出

[不,不是你现在所在的网站,是什么被命名.]过度简化,"堆栈"就像你在一些食客中粘贴你的订单纸.当您在该峰值上放置太多订单时,可能会发生此问题.在计算机中,任何未动态分配的变量以及尚未由CPU处理的任何命令都会进入堆栈.

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

int stupidFunction(int n)
{
   return stupidFunction(n);
}
Run Code Online (Sandbox Code Playgroud)

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

int stupidArray[600851475143];
Run Code Online (Sandbox Code Playgroud)

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

也可以看看:


狂野的指针

在内存中创建一个指向某个随机位置的指针就像用你的代码玩俄罗​​斯轮盘赌 - 你很容易错过并创建一个指向你没有访问权限的位置的指针.

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

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

也可以看看:


试图读取数组的结尾

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

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

// 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++;
}
Run Code Online (Sandbox Code Playgroud)

或者经常看到的for<=而不是<(读取1个字节太多):

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

甚至是一个不好的拼写错误,编译得很好(在这里看到)并且只分配了1个用元素dim而不是元素初始化的dim元素.

int* my_array = new int(dim);
Run Code Online (Sandbox Code Playgroud)

此外,应该注意的是,甚至不允许创建(更不用说解除引用)指向数组外部的指针(只有当指向数组中的元素或者指向结束时的元素时,才可以创建此类指针).否则,您将触发未定义的行为.

也可以看看:


忘记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++;
}
Run Code Online (Sandbox Code Playgroud)

使用C字符串,它是否\0会有所不同.你应该假设它会避免未定义的行为:所以写得更好char str[4] = {'f', 'o', 'o', '\0'};


试图修改字符串文字

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

char* foo = "Hello, world!"
foo[7] = 'W';
Run Code Online (Sandbox Code Playgroud)

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

也可以看看:


不匹配的分配和释放方法

您必须使用mallocfree在一起,newdelete在一起,new[]delete[]一起.如果你混淆了,你可能会遇到段错误和其他奇怪的行为.

也可以看看:


工具链中的错误.

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

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

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


其他原因

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

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


调试

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

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

一些内存调试器

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

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

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

  • 在什么体系结构上你可以通过阅读未初始化的内存获得段错误? (4认同)
  • 虽然通过读取数组末尾来获取段错误肯定是可能的,但我会说它不太可能.它必须位于VAS的映射页面的边界上,或者你必须在结尾读取*很多*. (2认同)
  • @NathanOliver:我认为“ C字符串”正好是:一个以null终止的`char`数组,旨在与C的字符串函数一起使用。当然,并不是所有的`char`数组都需要以null结尾,但是我不认为您会以其他方式将它们称为C字符串。 (2认同)

归档时间:

查看次数:

8623 次

最近记录:

7 年,3 月 前