Linux程序调试--Bus Error

http://blog.csdn.net/zhaoyuping/article/details/8931662

Linux程序调试--Bus Error

Bus Error究竟是指什么

一  Bus Error,即总线错误。

   引发原因:

   CPU出于性能方面的考虑,要求对数据进行访问时都必须是地址对齐的。如果发现进行的不是地址对齐的访问,就会发送SIGBUS信号给进程,使进程产生 core dump。RISC包括SPARC(一种微处理器架构)都是这种类型的芯片。x86系列CPU都支持不对齐访问,也提供了开关禁用这个机制。x86架构不要求对齐访问的时候,必定会有性能代价。例如,对int的访问应该是4字节对齐的,即地址应该是4的倍数,对short则是2字节对齐的,地址应该是2的倍数。

   Bus Error也有可能是因为机器物理问题或者访问无效物理地址,但这种情况非常少见。

   Linux平台上执行malloc(),如果没有足够的RAM,Linux不是让malloc()失败返回,而是向当前进程分发SIGBUS信号。
          注: 对该点执怀疑态度,有机会可自行测试确认当前系统反应。

SIGBUS与SIGSEGV信号的一般区别如下:

   1) SIGBUS(Bus error)意味着指针所对应的地址是有效地址,但总线不能正常使用该指针。通常是未对齐的数据访问所致。
    2) SIGSEGV(Segment fault)意味着指针所对应的地址是无效地址,没有物理内存对应该地址。

 

二  例子程序:


1 int main(){
  6 #if defined(__GNUC__)
  7 # if defined(__i386__)
  8    
  9     __asm__("pushf\n orl $0x40000,(%esp) \n popf");
 10 # elif defined(__x86_64__)
 11     
 12     __asm__("pushf\norl $0x40000,(%rsp)\npopf");
 13 # endif
 14 #endif
 24    short array[16];
 25
 26    int * p = (int *) &array[1];
 27    *p = 1;
 28
 29    return 1;
 30 }

 short类型大小为2个字节,其地址必是2的倍数。而对于int指针来说,能够使用以访问数据的地址应该是4的倍数,转化arrary[1]的地址为int *并访问,系统会发出SIGBUS信号,导致程序崩溃。

   wiki上的例子:

    http://en.wikipedia.org/wiki/Bus_error#Bus_error_example

#include <stdlib.h> 
 
 int main( int argc, char ** argv) { 
 int * iptr; 
 char * cptr; 
 
 #if defined(__GNUC__) 
 # if defined(__i386__) 
 
__asm__( "pushf/n orl $0x40000,(%esp)/n popf" ) ; 
 # elif defined(__x86_64__)  
 
__asm__( "pushf/n orl $0x40000,(%rsp)/n popf" ) ; 
 # endif 
 #endif 
 
 
cptr = malloc( sizeof ( int ) + 1) ; 
 
 
iptr = ( int * ) ++ cptr; 
 
 
 * iptr = 42 ; 
 
 return 0 ; 
 } 
 
$ gcc -ansi sigbus.c -o sigbus
$ ./sigbus 
Bus error
$ gdb ./sigbus
(gdb) r
Program received signal SIGBUS , Bus error.
0x080483ba in main ()
(gdb) x/i $pc
0x80483ba <main+54>: mov DWORD PTR [eax],0x2a
(gdb) p/x $eax
$1 = 0x804a009
(gdb) p/t $eax & (sizeof(int) - 1)
$2 = 1
 

 

三,编译器和硬件平台相关性

 

    上述已经描述,对于x86平台,默认允许非对齐访问,只不过会有性能代价。开启检测可以使用上述代码中的宏。

 

    这段程序如果用Sun Studio编译器的话,运行就没有问题。这是因为Sun Studio默认对32位编译使用的参数是-xmemalign=8i,其中i选项设置明确指明不产生SIGBUS信号。
    不过如果编译成64位程序,Sun Studio使用的-xmemalign=8s,其中s选项设置意味对这种非对齐访问产生SIGBUS信号,则仍旧会遇到这个错误。

 

    如果坚持在SPARC上使用GCC去编译这种代码,可以如下进行:

    GCC有一个Type Attributes特性,例如在需人工对齐的变量后加上:__attribute__ ((aligned (4))); 其意义就是指定偏移量为4的倍数。比如:

 short array[10] __attribute__ ((aligned (4)));

    不过这个属性只对Linker连接器可见的变量有效,也就是说对local variable无效。而且这种特性作用粒度比较大,比如这里只对第一个元素有作用,并不为数组的每个成员设置偏移量。如果一定要针对local variable或者数组的每个成员进行偏移量设置,可以使用union类型:

 union {
short s;
int i;
}

 

Linux程序调试--Bus Error(2)

我们经常会发现有两种内存转储(core   dump)
一种是段错误(segment error)通常是在一个非法的地址上进行取值赋值操作造成。
一种是总线错误(bus   error)通常是指针强制转换,导致CPU读取数据违反了一定的总线规则。

下面请大家讨论一下总线错误

有例子如下sizeof(int)==4:
#include   <stdio.h>
#include   <stdlib.h>
int   main()
{
int   i[5];
int   j;
i[0]=65536+2;
i[1]=65536*3+4;
j=*((int   *)((char   *)i+2));      
printf( "size   of   int   is   %d\nj=%d ",sizeof(int),j);
return   0;
}

我们姑且不考字节序问题,不管j结果是几.

在一般RISC的CPU上,一般的unix机器上都会出现bus   error。
而在windows机器上,我用了vc的cl   borland的bcc32和gnu的gcc编译执行都没问题。

大家有兴趣可以讨论一下

***********************************************************************************************

我想,说来说去bus error都是一个“边界”问题。以下是我的看法:
从硬件的角度来看,大家应该发现内存是一小块一小块的吧,每一块都有固定大小,现在应该都是4的整数倍,又或者说int大小的整数倍,又或者说地址线总数的整数倍。为什么要这样?硬件好处理呀。举个例子来说吧,如果int型为四字节大小,且首字节地址不受限制的话,该int就可能跨在2个内存块之间(又或者别人说的“跨在2个页内存之间”),那么硬件如何来取数据呢?熟悉硬件的人都应该知道,内存访问是用“十字交叉”来决定地址的,也就是说,每个内存块是互斥的,这时要读这个数据岂不麻烦?如果地址硬规定4的整数倍,可想而知,问题解决了,效率也高了。
从编程的角度来说,采用的是虚内存,与页内存存在映射关系,当然会导致上述问题。

unix出现该错误,用我的观点应该可以解释,至于楼主说的vc,如果将奇地址转化为int指针再取值的话,应该会出现问题,那为什么2倍数非4倍数取能够正确执行呢?我想,这可能和操作系统的内存映射有关,unix下可能采用的是“完全对齐”原则,而windows可能给虚内存加了一个边界,也就是说实际的虚页总比实页少至少2字节以上,目的可能是为了防止越界而导致系统崩溃(个人猜测)。

想到结构体的sizeof问题,顺带提一下,之所以> =内部变量和的大小,是否仔细看过大小的规律?这也应该是“边界”问题,可以说是编译器为了追求数据访问速度而做的一点预处理。

*********************************************************************************************
一下代码
int   main()
{
int   a[4]={0,0,0,0};
int   *pi;
pi=(int   *)(((char   *)a)+1);
*pi=3;
printf( "a0   is   %d\n "   "a1   is   %d\n "   "pi   is   %d\n ",a[0],a[1],*pi);
printf( "sizeof   int   is   %d\n ",sizeof(int));
return   0;
}
同样用GCC编译
HP_UX上是出总线错误,(其他UNIX机器上也出,以前做过)
但拿到WINDOWS   2000机器上就好使。
a0   is   768
a1   is   0
pi   is   3
sizeof   int   is   4
现不考虑字节序问题,为什么在windows上就好使呢,难道编译出来的不是32位的程序。难道还作了什么特殊的处理。理论上已改出错才对。怪异
**************************************************************************************************
不知道你用的是哪种unix,我记得在Solaris是会存在类似的bus   error的。
我在linux和sco下进行一些测试,即使你采用编译器默认的对齐方式,对于奇地址强制转化为int地址的情况,程序运行正常,在linux和sco下编译器从栈中为非对齐的结构体非配空间的方式有点小差异,如上所述的结构体,linux是从偶地址开始,sco是从奇地址开始。。。

撇开上面这些先不谈。
1。每个操作系统都会有自己的进程载入和调度程序,也就是说,虚内存到实内存的映射,每种os都会有自己的特点,他们可以根据自己的情况避开某些小概率事件,“边界”也应该可以称之为小概率事件,在分配空间时做点手脚,对于分配者来说应该不会困难。
2。对齐问题,很久以前就听别人说,是为了提高访问效率,想想也是,多一种情况就不得不“if”,而且对齐也利于硬件“批量”访问数据。
3。采用非对齐方式会降低程序的效率,有些书上可能会提到这些,建议让编译器来决定采用哪种方式,毕竟每种编译器都是根据系统而来的。
4。在进行地址转换取值的时候,尽量用位拷贝,这样不同的os也不用当心。
5。不管os怎样对待bus   error问题,不管编译器如何优化,硬件访问数据时的边界问题始终存在,开发可移植系统时,我认为不应该漏掉这个问题。

你的疑问:
1。pi-> a对于开发者来说是int,对于c编译起来说是int,对于汇编器来说是一段空间,对于硬件来说是一段空间,在目标代码一级,没什么所谓的类型。
2。cpu访问数据是通过发指令来控制硬件访问数据的,通常是先读到cache、寄存器。形象一点的话,和十字路口的红绿灯的控制下的车流差不多。看《微机原理》或许会有所帮助。
3。我认为,硬件在遇到边界问题时,应该会有警告,各各os处理不同罢了。
4。Solaris的特点

建议:暂时不用钻得太深,除非你有足够的资料。知道这个问题和大致的可能原因就差不多了,不是说不去掌握它,而是计算机这个东西内容太多,很多硬件问题采用避开而不是花很大代价去解决。等有了足够的知识积累,相信这个问题不会是什么难事。

你可能感兴趣的:(Linux程序调试--Bus Error)