信号量崩溃原因初探

信号量崩溃原因初探

1.SIGSEGV

1.什么是段错误(segmentation fault)

wiki上的是这么说的

A segmentation fault (often shortened to SIGSEGV) is a particular error condition that can occur during the operation of computer software. A segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (for example, attempting to write to a read-only location, or to overwrite part of the operating system).

Segmentation is one approach to memory management and protection in the operating system. It has been superseded by paging for most purposes, but much of the terminology of segmentation is still used, "segmentation fault" being an example. Some operating systems still have segmentation at some logical level although paging is used as the main memory management policy.

On Unix-like operating systems, a process that accesses an invalid memory address receives the SIGSEGV signal. On Microsoft Windows, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION exception.

上述文字没有给出SIGSEGV的定义,仅仅说它是“计算机软件操作过程中的一种错误情况”。文字描述了SIGSEGV在何时发生,即“当程序试图访问不被允许访问的内存区域(比如,尝试写一块属于操作系统的内存),或以错误的类型访问内存区域(比如,尝试写一块只读内存)。这个描述是准确的。为了加深理解,我们再更加详细的概括一下SIGSEGV。

  1. SIGSEGV是在访问内存时发生的错误,它属于内存管理的范畴
  2. SIGSEGV是一个用户态的概念,是操作系统在用户态程序错误访问内存时所做出的处理。
  3. 当用户态程序访问(访问表示读、写或执行)不允许访问的内存时,产生SIGSEGV。
  4. 当用户态程序以错误的方式访问允许访问的内存时,产生SIGSEGV。

从用户态程序开发的角度,我们并不需要理解操作系统复杂的内存管理机制,这是和硬件平台相关的。但是,了解内核发送SIGSEGV信号的流程,对我们理解SIGSEGV是很有帮助的。在《Understanding Linux Kernel Edition 3》和《Understanding the Linux Virtual Memory Manager》相关章节都有一幅总图对此描述,对比之下,笔者认为ULK的图更为直观。


图片

图1红色部分展示了内核发送SIGSEGV信号给用户态程序的总体流程。当用户态程序访问一个会引发SIGSEGV的地址时,硬件首先产生一个page fault,即“缺页异常”。在内核的page fault处理函数中,首先判断该地址是否属于用户态程序的地址空间[*]。以Intel的32bit IA32架构的CPU为例,用户态程序的地址空间为[0,3G],内核地址空间为[3G,4G]。如果该地址属于用户态地址空间,检查访问的类型是否和该内存区域的类型是否匹配,不匹配,则发送SIGSEGV信号;如果该地址不属于用户态地址空间,检查访问该地址的操作是否发生在用户态,如果是,发送SIGSEGV信号。

[*]这里的用户态程序地址空间,特指程序可以访问的地址空间范围。如果广义的说,一个进程的地址空间应该包括内核空间部分,只是它不能访问而已。

图2更为详细的描绘了内核发送SIGSEGV信号的流程。在这里我们不再累述图中流程,在后面章节的例子中,笔者会结合实际,描述具体的流程。


图片

2.指针越界和SIGSEGV

经常看到有帖子把两者混淆,而这两者的关系也确实微妙。在此,我们把指针运算(加减)引起的越界、野指针、空指针都归为指针越界。SIGSEGV在很多时候是由于指针越界引起的,但并不是所有的指针越界都会引发SIGSEGV。一个越界的指针,如果不解引用它,是不会引起SIGSEGV的。而即使解引用了一个越界的指针,也不一定会引起SIGSEGV。这听上去让人发疯,而实际情况确实如此。SIGSEGV涉及到操作系统、C库、编译器、链接器各方面的内容,我们以一些具体的例子来说明。

2.1错误的访问类型引起的SIGSEGV

#include 
#include 
  
int main() {
    char* s = "hello world";
    s[1] = 'H';
}

这是最常见的一个例子。此例中,”hello world”作为一个常量字符串,在编译后会被放在.rodata节(GCC),最后链接生成目标程序时.rodata节会被合并到text segment与代码段放在一起,故其所处内存区域是只读的。这就是错误的访问类型引起的SIGSEGV。

其在图2中的顺序为:

1 -> 3 -> 4 -> 6 -> 8 -> 11 ->10

2.2访问了不属于进程地址空间的内存

#include 
#include 
int main() {
    int* p = (int*)0xC0000fff;
    *p = 10;
}

在这个例子中,我们访问了一个属于内核的地址(IA32,32bit)。当然,很少会有人这样写程序,但你的程序可能在不经意的情况下做出这样的行为(这个不经意的行为在后面讨论)。此例在图2的流程:

1 -> 2 -> 11 -> 10

2.3访问了不存在的内存

最常见的情况不外乎解引用空指针了,如:

#include 
#include 
   
int main () {
    int *a = NULL;
    *a = 1;
}

在实际情况中,此例中的空指针可能指向用户态地址空间,但其所指向的页面实际不存在。其产生SIGSEGV在图2中的流程为:

1 -> 3 -> 4 -> 5 -> 11 ->10

2.4栈溢出了,有时SIGSEGV,有时却啥都没发生

这也是CU常见的一个月经贴。大部分C语言教材都会告诉你,当从一个函数返回后,该函数栈上的内容会被自动“释放”。“释放”给大多数初学者的印象是free(),似乎这块内存不存在了,于是当他访问这块应该不存在的内存时,发现一切都好,便陷入了深深的疑惑。

#include 
#include 
int* foo() {
    int a = 10;
    return &a;
}
int main() {
    int* b;
    b = foo();
    printf ("%d\n", *b);
}

当你编译这个程序时,会看到“warning: function returns address of local variable”,GCC已经在警告你栈溢的可能了。实际运行结果一切正常。原因是操作系统通常以“页”的粒度来管理内存,Linux中典型的页大小为4K,内核为进程栈分配内存也是以4K为粒度的。故当栈溢的幅度小于页的大小时,不会产生SIGSEGV。那是否说栈溢出超过4K,就会产生SIGSEGV呢?看下面这个例子:

#include 
#include 

char* foo() {
    char buf[8192];
    memset (buf, 0x55, sizeof(buf));
    return buf;
}
int main() {
    char* c;
   c = foo();
    printf ("%#x\n", c[5000]);
}

虽然我们的栈溢已经超出了4K大小,可运行仍然正常。这是因为C教程中提到的“栈自动释放”实际上是改变栈指针,而其指向的内存,并不是在函数返回时就被回收了。在我们的例子中,所访问的栈溢处内存仍然存在。无效的栈内存(即栈指针范围外未被回收的栈内存)是由操作系统在需要时回收的,这是无法预测的,也就无法预测何时访问非法的栈内容会引发SIGSEGV。

好了,在上面的例子中,我们的栈溢例子,无论是大于一个页尺寸还是小于一个页尺寸,访问的都是已分配而未回收的栈内存。那么访问未分配的栈内存,是否就一定会引发SIGSEGV呢?答案是否定的。

#include 
#include 
int main() {
    char* c;
    c = (char*)&c – 8192 *2;
    *c = 'a';
    printf ("%c\n", *c);
}

在IA32平台上,栈默认是向下增长的,我们栈溢16K,访问一块未分配的栈区域(至少从我们的程序来看,此处是未分配的)。选用16K这个值,是要让我们的溢出范围足够大,大过内核为进程分配的初始栈大小(初始大小为4K或8K)。按理说,我们应该看到期望的SIGSEGV,但结果却非如此,一切正常。
答案藏在内核的page fault处理函数中:

if (error_code & PF_USER) {

              /*

               * Accessing the stack below %sp is always a bug.

               * The large cushion allows instructions like enter

               * and pusha to work.  ("enter $65535,$31" pushes

               * 32 pointers and then decrements %sp by 65535.)

               */

              if (address + 65536 + 32 * sizeof(unsigned long) < regs->sp)

                     goto bad_area;

       }

       if (expand_stack(vma, address))

              goto bad_area;

内核为enter[*]这样的指令留下了空间,从代码来看,理论上栈溢小于64K左右都是没问题的,栈会自动扩展。令人迷惑的是,笔者用下面这个例子来测试栈溢的阈值,得到的确是70K ~ 80K这个区间,而不是预料中的65K ~ 66K。

[*]关于enter指令的详细介绍,请参考《Intel(R) 64 and IA-32 Architectures Software Developer Manual Volume 1》6.5节“PROCEDURE CALLS FOR BLOCK-STRUCTURED LANGUAGES”

#include 
#include 

#define GET_ESP(esp) do {   \
    asm volatile ("movl %%esp, %0\n\t" : "=m" (esp));  \
}  while (0)
  
#define K 1024
int main() {
    char* c;
    int i = 0;
    unsigned long esp;
    GET_ESP (esp);
    printf ("Current stack pointer is %#x\n", esp);
    while (1) {
        c = (char*)esp -  i * K;
        *c = 'a';
        GET_ESP (esp);
        printf ("esp = %#x, overflow %dK\n", esp, i);
        i ++;
    }
}

笔者目前也不能解释其中的魔术,这神奇的程序啊!上例中发生SIGSEGV时,在图2中的流程是:

1 -> 3 -> 4 -> 5 -> 11 -> 10 (注意,发生SIGSEGV时,该地址已经不属于用户态栈了,所以是5 à 11 而不是 5 -à 6)

到这里,我们至少能够知道SIGSEGV和操作系统(栈的分配和回收),编译器(谁知道它会不会使用enter这样的指令呢)有着密切的联系,而不像教科书中“函数返回后其使用的栈自动回收”那样简单。

2.5 堆

#include 
#include 
#define K 1024
int main () {
    char* c;
    int i = 0;
    c = malloc (1);
    while (1) {
        c += i*K;
        *c = 'a';
        printf ("overflow %dK\n", i);
        i ++;
    }
}

看了栈的例子,举一反三就能知道,SIGSEGV和堆的关系取决于你的内存分配器,通常这意味着取决于C库的实现。

上面这个例子在笔者机器上于15K时产生SIGSEGV。让我们改变初次malloc的内存大小,当初次分配16M时,SIGSEGV推迟到了溢出180K;当初次分配160M时,SIGSEGV推迟到了溢出571K。我们知道内存分配器在分配不同大小的内存时通常有不同的机制,这个例子从某种角度证明了这点。此例SIGSEGV在图2中的流程为:

1 -> 3 -> 4 -> 5 -> 11 -> 10

用一个野指针在堆里胡乱访问很少见,更多被问起的是“为什么我访问一块free()后的内存却没发生SIGSEGV”,比如下面这个例子:

#include 
#include 
   
#define K 1024
int main () {
    int* a;
   
    a = malloc (sizeof(int));
    *a = 100;
    printf ("%d\n", *a);
    free (a);
    printf ("%d\n", *a);
}

SIGSEGV没有发生,但free()后a指向的内存被清零了,一个合理的解释是为了安全。相信不会再有人问SIGSEGV没发生的原因。是的,free()后的内存不一定就立即归还给了操作系统,在真正的归还发生前,它一直在那儿。

2.6如果是指向全局区的野指针呢?

看了上面两个例子,我觉得这实在没什么好讲的。

2.7 函数跳转到了一个非法的地址上执行

这也是产生SIGSEGV的常见原因,来看下面的例子:

#include 
#include 
#include 
void foo () {
    char c;
    memset (&c, 0x55, 128);
}
int main () {
    foo();
}

通过栈溢出,我们将函数foo的返回地址覆盖成了0x55555555,函数跳转到了一个非法地址执行,最终引发SIGSEGV。非法地址执行,在图2中的流程中的可能性就太多了,从1->3 ->4 -> … ->10,从4到10之间,几乎每条路径都可能出现。当然对于此例,0x55555555所指向的页面并不在内存之中,其在图2的流程为:

1->3 ->4 ->5-->11->10

如果非法地址对应的页面(页面属于用户态地址空间)存在于内存中,它又是可执行的[*],则程序会执行一大堆随机的指令。在这些指令执行过程中一旦访问内存,其产生SIGSEGV的流程几乎就无法追踪了(除非你用调试工具跟进)。看到这里,一个很合理的问题是:为什么程序在非法地址中执行的是随机指令,而不是非法指令呢?在一块未知的内存上执行,遇到非法指令可能性比较大吧,这样应该收到SIGILL信号啊?

[*]如果不用段寄存器的type checking,只用页表保护,传统32bit IA32可读即可执行。在NX技术出现后页级也可以控制是否可以执行。

事实并非如此,我们的IA32架构使用了如此复杂的指令集,以至于找到一条非法指令的编码还真不容易。在下例子中:

#include 
#include 
int main() {
    char buf[128] = "asdfaowerqoweurqwuroahfoasdbaoseur20   234123akfhasbfqower53453";
    sleep(1);
}

笔者在buf中随机的敲入了一些字符,反汇编其内容得到的结果是:

0xbffa9e00:     popa  
0xbffa9e01:     jae    0xbffa9e67
0xbffa9e03:     popaw 
0xbffa9e05:     outsl  %ds:(%esi),(%dx)
0xbffa9e06:     ja     0xbffa9e6d
0xbffa9e08:     jb     0xbffa9e7b
0xbffa9e0a:     outsl  %ds:(%esi),(%dx)
0xbffa9e0b:     ja     0xbffa9e72
0xbffa9e0d:     jne    0xbffa9e81
0xbffa9e0f:     jno    0xbffa9e88
0xbffa9e11:     jne    0xbffa9e85
0xbffa9e13:     outsl  %ds:(%esi),(%dx)
0xbffa9e14:     popa  
0xbffa9e15:     push   $0x73616f66
0xbffa9e1a:     bound  %esp,%fs:0x6f(%ecx)
0xbffa9e1e:     jae    0xbffa9e85
0xbffa9e20:     jne    0xbffa9e94
0xbffa9e22:     xor    (%eax),%dh
0xbffa9e24:     and    %ah,(%eax)
0xbffa9e26:     and    %dh,(%edx)
0xbffa9e28:     xor    (%ecx,%esi,1),%esi
0xbffa9e2b:     xor    (%ebx),%dh
0xbffa9e2d:     popa  
0xbffa9e2e:     imul   $0x61,0x68(%esi),%esp
0xbffa9e32:     jae    0xbffa9e96
0xbffa9e34:     data16
0xbffa9e35:     jno    0xbffa9ea6
0xbffa9e37:     ja     0xbffa9e9e
0xbffa9e39:     jb     0xbffa9e70
0xbffa9e3b:     xor    0x33(,%esi,1),%esi
0xbffa9e42:     add    %al,(%eax)
0xbffa9e44:     add    %al,(%eax)
0xbffa9e46:     add    %al,(%eax)
0xbffa9e48:     add    %al,(%eax)
0xbffa9e4a:     add    %al,(%eax)
0xbffa9e4c:     add    %al,(%eax)
0xbffa9e4e:     add    %al,(%eax)
0xbffa9e50:     add    %al,(%eax)
0xbffa9e52:     add    %al,(%eax)
0xbffa9e54:     add    %al,(%eax)
0xbffa9e56:     add    %al,(%eax)
0xbffa9e58:     add    %al,(%eax)
0xbffa9e5a:     add    %al,(%eax)
0xbffa9e5c:     add    %al,(%eax)
0xbffa9e5e:     add    %al,(%eax)

…………………………………………………………………………

一条非法指令都没有!大家也可以自己构造一些随机内容试试,看能得到多少非法指令。故在实际情况中,函数跳转到非法地址执行时,遇到SIGSEGV的概率是远远大于SIGILL的。

我们来构造一个遭遇SIGILL的情况,如下例:

#include 

#include 

#include 

 

#define GET_EBP(ebp)    \

    do {    \

        asm volatile ("movl %%ebp, %0\n\t" : "=m" (ebp));  \

    } while (0)
char buf[128];

void foo () {
    printf ("Hello world\n");
}
void build_ill_func() {
   int i = 0;
    memcpy (buf, foo, sizeof(buf));
    while (1) {
        /*
         * Find *call* instruction and replace it with
         * *ud2a* to generate a #UD exception
         */
        if ( buf[i] == 0xffffffe8 ) {
            buf[i] = 0x0f;
            buf[i+1] = 0x0b;
            break;
        }
        i ++;
    }
}
void overflow_ret_address () {
    unsigned long ebp;
    unsigned long addr = (unsigned long)buf;
    int i;
    GET_EBP (ebp);
    for ( i=0; i<16; i++ )
        memcpy ((void*)(ebp + i*sizeof(addr)), &addr, sizeof(addr));
    printf ("ebp = %#x\n", ebp);
}
int main() {
    printf ("%p\n", buf);
    build_ill_func ();
    overflow_ret_address ();
}

2.8非法的系统调用参数

我们在一块全局的buf里填充了一些指令,其中有一条是ud2a,它是IA32指令集中用来构造一个非法指令陷阱。在overflow_ret_address()中,我们通过栈溢出覆盖函数的返回地址,使得函数返回时跳转到buf执行,最终执行到ud2a指令产生一个SIGILL信号。注意此例使用了ebp框架指针寄存器,在编译时不能使用-fomit-frame-pointer参数,否则得不到期望的结果。
这是一种较为特殊的情况。特殊是指前面的例子访问非法内存都发生在用户态。而此例中,对非法内存的访问却发生在内核态。通常是执行copy_from_user()或copy_to_user()时。其流程在图2中为:
1 -> …. -> 11 -> 12 -> 13
内核使用fixup[*]的技巧来处理在处理此类错误。ULK说通常的处理是发送一个SIGSEGV信号,但实际大多数系统调用都可以返回EFAULT(bad address)码,从而避免用户态程序被终结。这种情况就不举例了,笔者一时间想不出哪个系统调用可以模拟此种情况而不返回EFAULT错误。

3.如何避免SIGSEGV

良好的编程习惯永远是最好的预防方法。良好的习惯包括:

尽量按照C标准写程序。之所以说是尽量,是因为C标准有太多平台相关和无定义的行为,而其中一些实际上已经有既成事实的标准了。例如C标准中,一个越界的指针导致的是无定义的行为,而在实际情况中,一个越界而未解引用的指针是不会带来灾难后果的。借用CU的一个例子,如下:

#include 
#include 
int main () {
    char a[] = "hello";
    char* p;
    for ( p = a+5; p>=a; p-- )
        printf ("%c\n", *p);
}

虽然循环结束后,p指向了数组a前一个元素,在C标准中这是一个无定义的行为,但实际上程序却是安全的,没有必要为了不让p成为一个野指针而把程序改写为:

#include 
#include 
int main () {
    char a[] = "hello";
    char* p;      
    for ( p = a+5; p!=a; p-- ) {
        printf ("%c\n", *p);
    }
    printf ("%c\n", *p);
}

当然,或许世界上真有编译器会对“越界但未解引用”的野指针进行处理,例如引发一个SIGSEGV。笔者无法100%保证,所以大家在实践中还是各自斟酌吧。

彻底的懂得你的程序。和其它程序员不同的是,C程序员需要对自己的程序完全了解,做到精确控制。尤其在内存的分配和释放方面。在操作每一个指针前,你都应该清楚它所指向内存的出处(栈、堆、全局区),并清楚此内存的生存周期。只有明白的使用内存,才能最大限度的避免SIGSEGV的产生。

大量使用assert。笔者偏好在程序中使用大量的assert,凡是有认为不该出现的情况,笔者就会加入一个assert做检查。虽然assert无法直接避免SIGSEGV,但它却能尽早的抛出错误。离错误越近,就越容易root cause。很多时候出现SIGSEGV时,程序已经跑飞很远了。

打开-Wall –Werror编译选项。如果程序是自己写的,0 warning应该始终是一项指标(0 warning不包括因为编译器版本不同而引起的warning)。一种常见的SIGSEGV来源于向函数传入了错误的参数类型。例如:

#include 
#include 
#include 
int main () {
    char buf[12];
    int buff;
    strcpy (buff, "hello");
}

这个例子中,本意是要向buf拷贝一个字符串,但由于有一个和buf名称很相近的buff变量,由于一个笔误(这个笔误很可能就来自你编辑器的自动补全,例如vim的ctrl – p, ctrl – n),strcpy如愿的引发了SIGSEGV。实际在编译期间,编译器就提示我们warning: passing argument 1 of strcpy makes pointer from integer without a cast,但我们忽略了。

这就进一步要求我们尽量使用编译器的类型检查功能,包括多用函数少用宏(特别是完成复杂功能的宏),函数参数多用带类型的指针,少用void*指针等。此例就是我们在2.2节提到的不经意的行为。

少用奇技淫巧,多用标准方法。好的程序应该逻辑清楚,干净整洁,像一篇朗朗上口的文章,让人一读就懂。那种充满晦涩语法、怪异招数的试验作品,是不受欢迎的。很多人喜欢把性能问题做为使用不标准方法的借口,实际上他们根本不知道对性能的影响如何,拿不出具体指标,全是想当然尔。笔者曾经在项目中,将一个执行频繁的异常处理函数用汇编重写,使该函数的执行周期从2000多个机器周期下降到40多个。满心欢喜的提交了一个patch给该项目的maintainer,得到的答复是:“张,你具体测试过你的patch能带来多大的性能提升吗?如果没有明显的数据,我是不愿意将优雅的C代码替换成这晦涩的汇编的。”于是我做了一个内核编译来测试patch,耗时15分钟,我的patch带来的整体性能提升大约为0.1%。所以,尽量写清楚明白的代码,不仅有利于避免SIGSEGV,也利于在出现SIGSEGV后进行调试。

当你的一个需求,标准的方法不能满足时,只有两种可能:1.从一开始的设计就错了,才会导致错误的需求;2.你读过的代码太少,不知道业界解决该问题的标准方法是什么。计算机已经发展了几十年,如果你不是在做前沿研究,遇到一定得用非标准方法解决的问题的机会实在太小了。正如我们经常用gdb跟踪发现SIGSEGV发生在C库里,不要嚷嚷说C库有bug,大部情况是一开始你传入的参数就错了。

主要内容来源

SEGMENTATION FAULT IN LINUX 原因与避免

2.SIGILL

1.illegal instruction,即SIGILL

是POSIX标准中提供的一类错误。 从名字上看,SIGILL是启动的某个进程中的某一句不能被CPU识别成正确的指令。 此类错误是由操作系统发送给进程的,在进程试图执行一些形式错误、未知或者特权指令时操作系统会使用SIGILL信号终止程序。 SIGILL对应的常数是4.

2.造成SIGILL的原因

错误代码示例

typedef void(*FUNC)(void);
int main(void)
{
    const static unsigned char insn[4] = { 0xff, 0xff, 0xff, 0xff };
    FUNC function = (FUNC) insn;
    function();
}

2.1 将不正确的数据段写入代码段

进程在代码段中的数据是要被作为一个指令执行的。 若不小心覆盖了已有的代码段,可能会得到错误格式的指令。 这种错误尤其在Just-In-Time即时编译器中最可能出现。
同样,如果不小心覆盖了栈上活跃记录中的返回地址,程序就可能根据这个错误地址,执行没有意义的内存中的数据,进而操作。
进一步可以认为,任何导致数据错误的问题都可能带来illegal instruction问题。比如硬盘发生故障。

2.2 指令集的演进

比如SIMD指令,自从奔腾4开始有MMX,X86的芯片就开始不停的增加和拓宽SIMD支持,SSE、SSE2、SSE3、SSE42、AVX、AVX2。 默认情况下,很多编译器都在O2或者O3中开了自动向量化,这就导致很多在新体系结构中编译的可执行程序,在老机器上运行时会有illegal instruction问题。

2.3 工具链BUG

对于普通C语言通过编译器生成的可执行程序。一般都已经通过严格的测试,不会随便发生这种问题。 所以如果你遇到这种错,并且试过了静态链,而且程序中没有嵌入式汇编,基本可以断定是工具链出了问题。 编译器?汇编器或者链接器。

2.4 访存对齐或浮点数格式问题

根据经验,出现错误的指令可能和访存地址指令有关。 另外,浮点数的格式是否符合IEEE的标准也可能会有影响。

3.错误排查方法

1.程序中有没有特权指令、或者访问特权寄存器
2.有没有将在较新CPU上编译得到的可执行文件拿到老CPU上运行
3.程序中有没有嵌入式汇编,先检查。
>1.一般编译器很少会生成有这种问题的代码
>2.X86平台上要尤其注意64位汇编指令和32位汇编指令的混用问题

4.程序有在进程代码段空间写数据的机会吗?
5.栈操作够安全吗?
6.注意程序的ABI是否正确,尤其是动态链和静态链是否处理的正确,尽量避免动态链的可执行文件调用错误库的问题(ARM的EABI,MIPS的N32/O32/N64都很可能出这种问题)
7.用的工具链靠谱吗?

4.参考文档

SIGILL定义
Illegal Instruction 错误初窥

3.SIGABRT

1.什么是SIGABRT

wiki上是这么定义的
The SIGABRT and SIGIOT signal is sent to a process to tell it to abort, i.e. to terminate. The signal is usually initiated by the process itself when it calls abort() function of the C Standard Library, but it can be sent to the process from outside like any other signal.
通俗的说就是由调用abort函数产生,进程非正常退出。

2.导致SIGABRT的原因

2.1.多次free导致SIGABRT

#include "stdlib.h"
#include "string.h"
#include "stdio.h
int main()
{
    void *pc = malloc(1024);
    free(pc);
    //free(pc);  //打开注释会导致错误
    printf("free ok!\n");
    return 0;
}

2.2执行abort()函数

#include "string.h"
#include "stdio.h"
#include "stdlib.h"
 
int main()
{
    printf("before run abort!\n");
    abort();
    printf("after run abort!\n");
     return 0;
}

2.3执行到assert函数

#include "string.h"
#include "stdio.h"
#include "assert.h"
#include "stdlib.h"
int main(){
    printf("before run assert!\n");
#if 0  //该值为0,则报错;为1,则正常
    void *pc = malloc(1024);
#else
    void *pc = NULL;
#endif
    assert( pc != NULL );
    printf("after run assert!\n");
    return 0;
}

3.参考文献

程序运行产生SIGABRT信号的原因
SIGABRT

4.SIGBUS

1.什么是SIGBUG

wiki是这么说的
The SIGBUS signal is sent to a process when it causes a bus error. The conditions that lead to the signal being sent are, for example, incorrect memory access alignment or non-existent physical address.

通常来说SIGBUS,是由于进程引起了一个总线错误(Bus error)。如下是wiki对总线错误的定义
In computing, a bus error is a fault raised by hardware, notifying an operating system (OS) that a process is trying to access memory that the CPU cannot physically address: an invalid address for the address bus, hence the name. In modern use on most architectures these are much rarer than segmentation faults, which occur primarily due to memory access violations: problems in the logical address or permissions. On POSIX-compliant platforms, bus errors usually result in the SIGBUS signal being sent to the process that caused the error. SIGBUS can also be caused by any general device fault that the computer detects, though a bus error rarely means that the computer hardware is physically broken—it is normally caused by a bug in software.[citation needed] Bus errors may also be raised for certain other paging errors;

2.导致SIGBUS的原因

2.1 未对齐的读或写

事实上,总线错误几乎都是由于未对齐的读或写引起的。它之所以称为总线错误,是因为出现未对齐的内存访问请求时,被阻塞(block)的组件就是地址总线。对齐(alignment)的意思就是数据项只能存储在地址是数据项大小的整数倍的内存上。在现代的计算机架构中,尤其是RISC架构,都需要数据对齐,因为与任意的对齐有关的有关的额外逻辑会使整个内存系统更大且更慢。通过迫使每个内存访问局限在一个cache行或一个单独的页面内,可以极大地简化并加速如cache控制器和内存管理单元(MMU)这样的硬件。
我们用地址对齐这个术语来陈述这个问题,而不是直截了当地说是禁止内存跨页访问,但它们说但是同一回事。例如,访问一个8字节的double数据时,地址只允许是8的整数倍。所以一个double数据可以存储于地址24,地址8008或32768,但不能存储于地址1006(因为它无法被8整除)。
页和cache的大小都是经过精心设计的,这样只要遵守对齐规则就可以保证一个原子数据项不会跨过一个页或cache块的边界。

下面是代码示例

#include
union {
    char a[10];
    int i;
} u;
int main(void){
    int *p = (int *) (&(u.a[1]));
    /**
     * p中未对齐的地址将会引起总线错误,
     * 因为数组和int的联合确保了a是按照int的4字节来对齐的,
     * 所以“a+1”肯定不是int对齐的
     */
    *p = 17; 
    printf("%d %p %p %p\n", *p, &(u.a[0]), &(u.a[1]), &(u.i));
    printf("%lu %lu\n", sizeof(char), sizeof(int));
    return 0;
}

5.SIGTRAP

1.什么是SIGTRAP?

The SIGTRAP signal is sent to a process when an exception (or trap) occurs: a condition that a debugger has requested to be informed of – for example, when a particular function is executed, or when a particular variable changes value.
通常来说SIGTRAP是由断点指令或其它trap指令产生. 由debugger使用。如果没有附加调试器,则该过程将终止并生成崩溃报告。 较低级的库(例如,libdispatch)会在遇到致命错误时捕获进程。

2.导致SIGTRAP被发送给进程的原因

2.1 断点指令触发

Debugger模式下,设置断点,当程序运行到断点时候,就会引发SIGTRAP。

2.2 其他trap指令触发

相关资料不多,待后续更新

6.SIGFPE

1.什么是SIGFPE?

SIG是信号名的通用前缀。FPE是floating-point exception(浮点异常)的首字母缩略字。在POSIX兼容的平台上,SIGFPE是当一个进程执行了一个错误的算术操作时发送给它的信号。SIGFPE的符号常量在头文件signal.h中定义。因为在不同平台上,信号数字可能变化,因此常使用信号名称。

2.导致SIGFPE被发送给进程的原因

1.FPE_INTDIV 整数除以零

2.FPE_INTOVF 整数上溢

3.FPE_FLTDIV 浮点除以零

4.FPE_FLTOVF 浮点上溢

5.FPE_FLTUND 浮点下溢

6.FPE_FLTRES 浮点结果不准

7.FPE_FLTINV 无效浮点操作

8.FPE_FLTSUB 浮点下标越界

这是一个尝试执行一个称为整数除以零,或FPE_INTDIV的错误算术运算的ANSI C程序的例子。

int main(){      
 int x = 42/0;      
}

3.参考文献

Documentation ArchiveDeveloperSearch
Exception Programming Topics

参考文献

POSIX signals

你可能感兴趣的:(信号量崩溃原因初探)