C 总线错误 (bus error) - 段错误 (segmentation fault)

C 总线错误 (bus error) - 段错误 (segmentation fault)

两个常见的运行时错误

  • bus error (core dumped) - 总线错误 (信息已转储)
  • segmentation fault (core dumped) - 段错误 (信息已转储)

错误信息对引起这两种错误的源代码错误并没有作简单的解释,上面的信息并未提供如何从代码中寻找错误的线索,而且两者之间的区别也并不是十分清楚,时至今日依然如此。

错误就是操作系统所检测到的异常,而这个异常是尽可能地以操作系统方便的原则来报告的。总线错误和段错误的准确原因在不同的操作系统版本上各不相同。这里所描述是运行于 SPARC 架构的 SunOS 出现的这两类错误以及产生错误的原因。

当硬件告诉操作系统一个有问题的内存引用时,就会出现这两种错误。操作系统通过向出错的进程发送一个信号与之交流。信号就是一种事件通知或一个软件中断,在 UNIX 系统编程中使用很广,但在应用程序编程中几乎不使用。在缺省情况下,进程在收到总线错误段错误信号后将进行信息转储并终止。不过可以为这些信号设置一个信号处理程序 (signal handler),用于修改进程的缺省反应。

信号是由于硬件中断而产生的。对中断的编程是非常困难的,因为它们是异步发生的 (其发生时间是不可预测的)。阅读信号的主文档和头文件 usr/include/sys/signal.h

1. 在 PC 上捕捉信号

信号处理函数是 ANSI C 的一部分,与 UNIX 一样,它也同样适用于 PC。例如 PC 程序员可以使用 signal() 函数来捕捉 Ctrl-Break 信号,防止用户用这种方法中断程序。

在任何使用信号的源文件中,都必须在文件前面增加一行 #include <singal.h>

这条信息的 core dumped 部分来源于很早的过去,那时所有的内存都是由铁氧化物圆环 (也就是 core,指磁心) 制造的。半导体成为内存的主要制造材料的时间已经超过十五年,但 core 这个词仍然被用作内存的同义词。

core [kɔː(r)]:n. 核心,要点,果心,磁心 vt. 挖...的核

2. 总线错误 (bus error)

事实上,总线错误几乎都是由于未对齐的读或写引起的。它之所以称为总线错误,是因为出现未对齐的内存访问请求时,被堵塞的组件就是地址总线。对齐 (alignment) 的意思就是数据项只能存储在地址是数据项大小的整数倍的内存位置上。在现代的计算机架构中,尤其是 RISC 架构,都需要数据对齐,因为与任意的对齐有关的额外逻辑会使整个内存系统吏大且更慢。通过迫使每个内存访问局限在一个 Cache 行或一个单独的页面内,可以极大地简化 (并加速) 如 Cache 控制器和内存管理单元这样的硬件。

我们表达数据项不能跨越页面或 Cache 边界规则的方法多少有些问接,因为我们用地址对齐这个术语来陈述这个问题,而不是直截了当说是禁止内存跨页访问,但它们说的是同一回事。例如,访问一个 8 字节的 double 数据时,地址只允许是 8 的整数倍。所以一个 double 数据可以存储于地址 24、8008 或 32768,但不能存储于地址 1006 (因为它无法被 8 整除)。页和 Cache 的大小是经过精心设计的,这样只要遵守对齐规则就可以保证一个原子数据项不会跨越一个页或 Cache 块的边界。

2.1 引起总线错误的程序

//============================================================================
// Name        : main
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

#include <stdio.h>

int main(int argc, char *argv[])
{
	union union_name
	{
		char a[10];
		int i;
	} union_object;

	printf("argc = %d\n", argc);

	for (int idx = 0; idx < argc; ++idx)
	{
		printf("argv[%d] --> %s\n", idx, argv[idx]);
	}

	printf("argv[argc] = %p\n\n", (void*)argv[argc]);

	int *pt = (int *)&(union_object.a[1]);
	int *pi = (int *)&(union_object.i);

	*pt = 17;

	printf("*pt = %d\n", *pt);
	printf("pt = %p\n", pt);
	printf("pi = %p\n", pi);

	return 0;
}
argc = 1
argv[0] --> D:\visual_studio_workspace\yongqiang\Debug\yongqiang.exe
argv[argc] = 00000000

*pt = 17
pt = 008FFA39
pi = 008FFA38
请按任意键继续. . .

在这里插入图片描述

pt 中未对齐的地址会引起一个总线错误!
在实际的运行中并没有出现错误,运行环境如下:
在这里插入图片描述

x86 体系结构,如果没有默认对齐的话,访问速度会降低。读一个 int 本来只读一次的,没有对齐的话要读 2 次才行,把第一次的尾巴和第二次的头拼起来。如果在代码中将对齐检查功能打开,运行后能显示 bus error

这可能导致一个总线错误,因为数组和 int 的联合确保数组 a 是按照 int 的 4 字节对齐的,a+l 的地址肯定未按 int 对齐。我们试图往这个地址存储 4 个字节的数据,但这个访问只是按照单字节的 char 对齐,这就违反了规则。一个好的编译器发现不对齐的情况时会发出警告,但它并不能检测到所有不对齐的情况。

编译器通过自动分配和填充数据 (在内存中) 来进行对齐。当然,在磁盘或磁带上并没有这样的对齐要求,所以程序员对它们可以很偷快地不必关心数据对齐。但是,当他们把一个 char 指针转换为 int 指针时,就会出现神秘的总线错误。几年前,当检测到一个内存奇偶检验错误时也会产生总线错误。现在,内存芯片已经非常可靠,而且很好地得到了错误检测和修正电路的保护,所以在应用程序编程这一级,奇偶检验错误几乎不再听闻。总线错误也可能由于引用一块物理上不存在的内存引起。如果不遭遇一个淘气的驱动程序,你恐怕不大可能遭遇这种不幸。

//============================================================================
// Name        : main
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

#include <stdio.h>

int main(int argc, char **argv)
{
#if defined(__GNUC__)
#if defined(__i386__)
	/* Enable Alignment Checking on x86 */
	__asm__("pushf\norl $0x40000,(%esp)\npopf");
#elif defined(__x86_64__)
	/* Enable Alignment Checking on x86_64 */
	__asm__("pushf\norl $0x40000,(%rsp)\npopf");
#endif
#endif

	union
	{
		char a[10];
		int i;
	} u;

	int *p = (int*) &(u.a[1]);
	*p = 17;

	return 0;
}

16:26:55 **** Build of configuration Debug for project yongqiang_example ****
make all 
Building file: ../src/yongqiang.c
Invoking: GCC C Compiler
gcc -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/yongqiang.d" -MT"src/yongqiang.o" -o "src/yongqiang.o" "../src/yongqiang.c"
Finished building: ../src/yongqiang.c
 
Building target: yongqiang_example
Invoking: GCC C Linker
gcc  -o "yongqiang_example"  ./src/yongqiang.o   
Finished building target: yongqiang_example
 

16:26:56 Build Finished (took 524ms)
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ pwd
/home/strong/eclipse-work/yongqiang_example/Debug
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ ll
total 56
drwxrwxr-x 3 strong strong  4096 Mar 30 16:26 ./
drwxrwxr-x 5 strong strong  4096 Mar 28  2018 ../
-rw-rw-r-- 1 strong strong  1009 Mar 30 16:26 makefile
-rw-rw-r-- 1 strong strong   231 Mar 30 16:26 objects.mk
-rw-rw-r-- 1 strong strong   392 Mar 30 16:26 sources.mk
drwxrwxr-x 2 strong strong  4096 Mar 30 16:26 src/
-rwxrwxr-x 1 strong strong 29720 Mar 30 16:26 yongqiang_example*
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ 
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ ./yongqiang_example 
Bus error (core dumped)
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$

3. 段错误 (segmentation fault)

段错误或段违规 (segmentation violation)。在 Sun 的硬件中,段错误是由于内存管理单元 (负责支持虚拟内存的硬件) 的异常所致,而该异常则通常是由于解除引用一个未初始化或非法值的指针引起的。如果指针引用一个并不位于你的地址空间中的地址,操作系统便会对此进行干涉。

一个小型的会引起段错误的程序如下:

	int *pt = 0;
	*pt = 17;
//============================================================================
// Name        : main
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

#include <stdio.h>

int main(int argc, char *argv[])
{
	printf("argc = %d\n", argc);

	for (int idx = 0; idx < argc; ++idx)
	{
		printf("argv[%d] --> %s\n", idx, argv[idx]);
	}

	printf("argv[argc] = %p\n\n", (void*)argv[argc]);

	int *pt = 0;
	*pt = 17;

	printf("*pt = %d\n", *pt);
	printf("pt = %p\n", pt);

	return 0;
}

16:31:07 **** Build of configuration Debug for project yongqiang_example ****
make all 
Building file: ../src/yongqiang.c
Invoking: GCC C Compiler
gcc -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/yongqiang.d" -MT"src/yongqiang.o" -o "src/yongqiang.o" "../src/yongqiang.c"
Finished building: ../src/yongqiang.c
 
Building target: yongqiang_example
Invoking: GCC C Linker
gcc  -o "yongqiang_example"  ./src/yongqiang.o   
Finished building target: yongqiang_example
 

16:31:08 Build Finished (took 571ms)
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ pwd
/home/strong/eclipse-work/yongqiang_example/Debug
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ 
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ ll
total 56
drwxrwxr-x 3 strong strong  4096 Mar 30 16:31 ./
drwxrwxr-x 5 strong strong  4096 Mar 28  2018 ../
-rw-rw-r-- 1 strong strong  1009 Mar 30 16:31 makefile
-rw-rw-r-- 1 strong strong   231 Mar 30 16:31 objects.mk
-rw-rw-r-- 1 strong strong   392 Mar 30 16:31 sources.mk
drwxrwxr-x 2 strong strong  4096 Mar 30 16:31 src/
-rwxrwxr-x 1 strong strong 29680 Mar 30 16:31 yongqiang_example*
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ 
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$ ./yongqiang_example 
argc = 1
argv[0] --> ./yongqiang_example
argv[argc] = (nil)

Segmentation fault (core dumped)
strong@foreverstrong:~/eclipse-work/yongqiang_example/Debug$

一个微妙之处是导致指针具有非法的值通常是由于不同的编程错误所引起的。和总线错误不同,段错误更像是一个间接的症状而不是引起错误的原因。

一个更糟糕的微妙之处是,如果未初始化的指针恰好具有未对齐的值 (对于指针所要访问的数据而言),它将会产生总线错误,而不是段错误。对于绝大多数架构的计算机而言确实如此,因为 CPU 先看到地址,然后再把它发送给 MMU。

violation [ˌvaɪəˈleɪʃn]:n. 违反,妨碍,侵害,违背,强奸

在你的代码中,对非法指针值的解除引用操作可能会像上面这样显式地出现,也可能在库函数中出现 (传递给它一个非法值)。令人不快的是,你的程序如果进行了修改 (如在调试状态下编译或增加额外的调试语句),内存的内容便很容易改变,于是这个问题被转移到别处或干脆消失。段错误是非常难于解决的,而且只有非常顽固的段错误才会一直存在。当你看到同事们神色严峻地带着逻辑分析器和示波器进入测试实验室时,便知道他们肯定遇到了真正的麻烦。

通常导致段错误的几个直接原因:

  • 解除引用一个包含非法值的指针。
  • 解除引用一个空指针 (常常由于从系统程序中返回空指针,并未经检查就使用)。
  • 在未得到正确的权限时进行访问,例如,试图往一个只读的文本段存储值就会引起段错误。
  • 用完了堆栈或堆空间 (虚拟内存虽然巨大但绝非无限)。

下面这个说法可能过于简单,但在绝大多数架构的绝大多数情况下,总线错误意味着 CPU 对进程引用内存的一些做法不满,而段错误则是 MMU 对进程引用内存的一些情况发出抱怨。

以发生频率为序,最终可能导致段错误的常见编程错误是:

  1. 坏指针值错误:在指针赋值之前就用它来引用内存,或者向库函数传送一个坏指针 (不要上当!如果调试器显示系统程序中出现了段错误,并不是因为系统程序引起了段错误,问题很可能还存在于自己的代码中)。第三种可能导致坏指针的原因是对指针进行释放之后再访问它的内容。可以修改 free 语句,在指针释放之后再将它置为空值。
    free(p); p = NULL;

这样,如果在指针释放之后继续使用该指针,至少程序能在终止之前进行信息转储。

  1. 改写 (overwrite) 错误:越过数组边界写入数据,在动态分配的内存两端之外写入数据,或改写一些堆管理数据结构 (在动态分配的内存之前的区域写入数据就很容易发生这种情况)。
    p = malloc(256); p[-1] = 0; p[256] = 0;

  2. 指针释放引起的错误:释放同一个内存块两次,或释放一块未曾使用 malloc 分配的内存,或释放仍在使用中的内存,或释放一个无效的指针。一个极为常见的与释放内存有关的错误就是在 for(p = start; p; p = p-> next) 这样的循环中迭代一个链表,并在循环体内使用 free(p) 语句。这样,在下一次循环迭代时,程序就会对己经释放的指针进行解除引用操作,从而导致不可预料的结果。

//============================================================================
// Name        : main
// Author      : Yongqiang Cheng
// Version     : Version 1.0.0
// Copyright   : Copyright (c) 2019 Yongqiang Cheng
// Description : Hello World in C++, Ansi-style
//============================================================================

#include <stdio.h>

int main(int argc, char *argv[])
{
	printf("argc = %d\n", argc);

	for (int idx = 0; idx < argc; ++idx)
	{
		printf("argv[%d] --> %s\n", idx, argv[idx]);
	}

	printf("argv[argc] = %p\n\n", (void*) argv[argc]);

	int *pt = NULL;

	free(pt);

	printf("pt = %p\n", pt);

	return 0;
}

argc = 1
argv[0] --> /home/strong/eclipse-work/yongqiang_example/Debug/yongqiang_example
argv[argc] = (nil)

pt = (nil)

在遍历链表时正确释放元素的方法是使用临时变量存储下一个元素的地址。这样就可以安全地在任何时候释放当前元素,不必担心在取下一个元素的地址时还要引用它,代码如下:

	struct node *p, *start, *tmp;
	for (p = start; p; p = tmp)
	{
		tmp = p->next;
		free(p);
	}

3.1 程序空间是否足够?

如果你的程序所需的内存超过了操作系统所能提供给它的数量,程序就会发出一条段错误信息并终止。可以用一种简单的方法把这种段错误与其他基于 Bug 的段错误区分开来。

要弄清程序是否用完了堆栈,可以在 dbx 命令下运行该程序:

% dbx a.out
(dbx) catch SIGSEGV

(dbx) run
...
signal SEGV (segmentation violation) in <some_routine> at 0xeff57708
(dbx) where

如果现在可以看到调用链,那说明堆栈空间还没有用完。
但是,如果看到像下面这样的东西:

fetch at 0xeffe7a60 failed -- I/O error
(dbx)

那么,堆找很可能已经用完。上面这个十六进制数就是可以提取或映射的堆栈地址。
你也可以尝试在 C-shell 中调整堆栈段的大小限制。

limit stacksize 10

你可以在 C-shell 中调整堆栈段和数据段的最大值。进程的总地址空间仍然受交换区大小的限制,可以用 swap -s 命令查看交换区的大小。

当程序出现坏指针值时,什么样的结果都有可能发生。一种广被接受的说法是,如果你走运,指针将指向你的地址空间之外,这样第一次使用该指针时就会使程序进行信息转储后终止。如果你不走运,指针将指向你的地址空间之内,并损坏 (改写) 所指向的内存的任何信息。这将引起隐晦的 Bug,非常难以捕捉。

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读