信号机制和Android natvie crash捕捉

一、信号机制

image.png

函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换。

1、信号的接收

接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。

此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。

2、信号的检测

进程陷入内核态后,有两种场景会对信号进行检测:

  • 进程从内核态返回到用户态前进行信号检测
  • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
    当发现有新信号时,便会进入下一步,信号的处理。

3、信号的处理

信号处理函数是运行在用户态的,调用信号处理函数前,内核会将当前的内核栈的内容备份拷贝到用户栈,然后修改命令寄存器(eip)指向信号处理函数。
接下来进程返回到用户态中,执行相应的信号处理函数。
信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。
如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。

一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第2步和第3步骤间重复进行。

二、信号定义和行为

1、信号的定义

所有的符合Unix规范(如POSIX)的系统都统一定义了SIGNAL的数量、含义和行为。
Android代码中,signal的定义一般在 signum.h(Android代码中,signal的定义一般在 signum.h)中。

一共有31个信号,其中1~15号信号为常用信号

/* Signals.  */  
#define SIGHUP      1   /* Hangup (POSIX).  */   
#define SIGINT      2   /* Interrupt (ANSI).  */   
#define SIGQUIT     3   /* Quit (POSIX).  */   
#define SIGILL      4   /* Illegal instruction (ANSI).  */   
#define SIGTRAP     5   /* Trace trap (POSIX).  */   
#define SIGABRT     6   /* Abort (ANSI).  */   
#define SIGIOT      6   /* IOT trap (4.2 BSD).  */   
#define SIGBUS      7   /* BUS error (4.2 BSD).  */   
#define SIGFPE      8   /* Floating-point exception (ANSI).  */   
#define SIGKILL     9   /* Kill, unblockable (POSIX).  */   
#define SIGUSR1     10  /* User-defined signal 1 (POSIX).  */   
#define SIGSEGV     11  /* Segmentation violation (ANSI).  */   
#define SIGUSR2     12  /* User-defined signal 2 (POSIX).  */   
#define SIGPIPE     13  /* Broken pipe (POSIX).  */   
#define SIGALRM     14  /* Alarm clock (POSIX).  */   
#define SIGTERM     15  /* Termination (ANSI).  */   
#define SIGSTKFLT   16  /* Stack fault.  */   
#define SIGCLD      SIGCHLD /* Same as SIGCHLD (System V).  */   
#define SIGCHLD     17  /* Child status has changed (POSIX).  */   
#define SIGCONT     18  /* Continue (POSIX).  */   
#define SIGSTOP     19  /* Stop, unblockable (POSIX).  */   
#define SIGTSTP     20  /* Keyboard stop (POSIX).  */   
#define SIGTTIN     21  /* Background read from tty (POSIX).  */   
#define SIGTTOU     22  /* Background write to tty (POSIX).  */   
#define SIGURG      23  /* Urgent condition on socket (4.2 BSD).  */   
#define SIGXCPU     24  /* CPU limit exceeded (4.2 BSD).  */   
#define SIGXFSZ     25  /* File size limit exceeded (4.2 BSD).  */   
#define SIGVTALRM   26  /* Virtual alarm clock (4.2 BSD).  */   
#define SIGPROF     27  /* Profiling alarm clock (4.2 BSD).  */   
#define SIGWINCH    28  /* Window size change (4.3 BSD, Sun).  */   
#define SIGPOLL     SIGIO   /* Pollable event occurred (System V).  */   
#define SIGIO       29  /* I/O now possible (4.2 BSD).  */   
#define SIGPWR      30  /* Power failure restart (System V).  */   
#define SIGSYS      31  /* Bad system call.  */   
#define SIGUNUSED   31  
插曲:什么是POSIX
POSIX表示可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准。
POSIX标准意在期望获得源代码级别的软件可移植性。换句话说,为一个POSIX兼容的操作系统编写的程序,应该可以在任何其它的POSIX操作系统(即使是来自另一个厂商)上编译执行。

简单来说:
完成同一功能,不同内核提供的系统调用(也就是一个函数)是不同的。例如创建进程,linux下是fork函数,windows下是creatprocess函数。
POSIX 要求 linux和windows都要实现基本的posix标准,linux把fork函数封装成posix_fork(随便说的),windows把creatprocess函数也封装成posix_fork,都声明在unistd.h里。这样,程序员编写普通应用时候,只用包含unistd.h,调用posix_fork函数,程序就在源代码级别可移植了。

2、常见信号的含义

SIGHUP - 1

本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

SIGINT - 2

程序终止(interrupt)信号,通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

SIGQUI - 3

和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制.进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

SIGILL - 4

执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

SIGTRAP - 5

由断点指令或其它trap指令产生. 由debugger使用。

SIGABRT - 6

调用abort函数生成的信号。

SIGBUS - 7

非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。
它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

SIGFPE - 8

算术运算错误。不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

SIGKILL - 9

用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

SIGUSR1 - 10

用户自定义的信号1

SIGSEGV - 11

访问非法地址。试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

SIGUSR2 -12

用户自定义的信号1

SIGPIPE - 13

管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

SIGALRM -14

时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

SIGTERM - 15

程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出

3、如何产生信号

  • 在native应用中 使用 kill() 或者raise()
raise(SIGILL);
kill(SIGILL);
  • java 应用中使用 Procees.sendSignal()等
  • adb shell kill 命令向其他进程发送singal
adb root  
adb shell ps  
adb shell kill -3 513  
首先是切换到root用户 (普通进程只能发个自己或者同组进程,而root可以发送signal给任何进程)。然后用 ps命令查看当前系统中所有的进程信息。最后用kill命令发送SIGQUIT给进程号为513的进程。

4、信号处理行为

4.1 信号处理的方式一般有三种:

  • 忽略 接收到信号后不做任何反应
  • 自定义 用自定义的信号处理函数来执行特定的动作
  • 默认 收到信号后按默认得行为处理该信号。 这是多数应用采取的处理方式。
信号处理的行为是以进程级的。就是说不同的进程可以分别设置不同的信号处理方式而互不干扰。同一进程中的不同线程虽然可以设置不同的信号屏蔽字,但是却共享相同的信号处理方式 。

4.2 针对Android系统,信号的特殊行为。

Android也是Linux系统。为了开发和调试的需要,android对一些信号的处理定义了额外的行为。

1. SIGQUIT ( 整型值为 3) 打印trace信息

传统UNIX系统应用,对SIGQUIT信号的默认行为是 "终止 + CORE",也就是产生core dump文件后,立即终于运行。

Android Dalvik应用收到该信号后,会 打印改应用中所有线程的当前状态,并且并不是强制退出。
这些状态通常保存在一个特定的叫做trace的文件中。一般的路径是/data/anr/trace.txt

feifeideMacBook-Pro:s1 feifei$ adb shell ps | grep com.sogou.translate.example
u0_a70       18621  2972 4466496 109576 SyS_epoll_wait 7aced42ec0 S com.sogou.translate.example
u0_a70       18640  2972 4364644  69144 SyS_epoll_wait 7aced42ec0 S com.sogou.translate.example:logservice
feifeideMacBook-Pro:s1 feifei$ adb shell kill -3 18621
S1 AI Recorder:/data/anr # cd /data/anr/                                                                          
S1 AI Recorder:/data/anr # ls
trace_01 

----- pid 18621 at 2019-12-04 10:27:19 -----
Cmd line: com.sogou.translate.example
Build fingerprint: 'Sogou/sl8541e_1h10_oversea/sl8541e_1h10:8.1.0/OPM2.171019.012/02421:userdebug/test-keys'
ABI: 'arm64'


"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x7241fed0 self=0x7a4e8bea00
  | sysTid=18621 nice=-10 cgrp=default sched=0/0 handle=0x7ad35279a8
  | state=S schedstat=( 786261704 135017223 573 ) utm=71 stm=7 core=2 HZ=100
  | stack=0x7ff1799000-0x7ff179b000 stackSize=8MB
  | held mutexes=
  kernel: __switch_to+0xb0/0xbc
  kernel: SyS_epoll_wait+0x29c/0x378
  kernel: SyS_epoll_pwait+0xbc/0x130
  kernel: el0_svc_naked+0x24/0x28
  native: #00 pc 0000000000069ec0  /system/lib64/libc.so (__epoll_pwait+8)
  native: #01 pc 000000000001f734  /system/lib64/libc.so (epoll_pwait+52)
  native: #02 pc 0000000000015dcc  /system/lib64/libutils.so (android::Looper::pollInner(int)+144)
  native: #03 pc 0000000000015cb4  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+108)
  native: #04 pc 000000000010f75c  /system/lib64/libandroid_runtime.so (???)
  native: #05 pc 0000000000bd096c  /data/dalvik-cache/arm64/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+140)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:325)
  at android.os.Looper.loop(Looper.java:142)
  at android.app.ActivityThread.main(ActivityThread.java:6789)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:449)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
2、对于很多其他的异常信号 (SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT ), Android进程 在退出前,会在/data/tombstones,生成 tombstone文件。
 adb shell ps | grep com.sogou.translate.example
 adb shell kill -11 18621
 adb pull /data/tombstones
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Native Crash TIME: 19573247
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Sogou/sl8541e_1h10_oversea/sl8541e_1h10:8.1.0/OPM2.171019.012/02421:userdebug/test-keys'
Revision: '0'
ABI: 'arm64'
pid: 22302, tid: 22319, name: Binder:22302_4  >>> com.sogou.iot.b1pro.launcher:ttsservice <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x7b29df9480
    x0   00000000ffffffff  x1   0000000000000081  x2   000000007fffffff  x3   0000000000000000
    x4   0000000000000000  x5   0000000000000000  x6   0000000000000000  x7   0000007a45ff5000
    x8   000000003f4faf20  x9   0000000000000002  x10  0000007a2ca0d800  x11  0000000000000002
    x12  0000000000000001  x13  0000007a2c5ca800  x14  0000000000000000  x15  0000000000000001
    x16  0000007aceda4240  x17  0000007acecf67d0  x18  0000000000000004  x19  0000007a32ef9278
    x20  0000007a34d6bd98  x21  0000000000000000  x22  0000007a32ef9310  x23  00000000ffffffff
    x24  0000007a45e85000  x25  0000007a34d6b910  x26  000000000000013c  x27  0000000000000000
    x28  0000000000000050  x29  0000007a34d6bd80  x30  0000007a3244d5b4
    sp   0000007a34d6b4c0  pc   0000007a3244d728  pstate 00000000a0000000
    v0   00000000000000000000000000000000  v1   00000000211712c600000000211712c6
    v2   0000000000ffff5a0000000000000000  v3   00000000030000000000000000000000
    v4   00000000000000010000000000000000  v5   00000000000000030000000000000000
    v6   00000000000000000000000043420c74  v7   0000000000000000000000003ba3d70a
    v8   0000000000000000401921fb54442d18  v9   00000000000000000000000000000000
    v10  00000000000000000000000000000000  v11  00000000000000000000000000000000
    v12  00000000000000000000000000000000  v13  00000000000000000000000000000000
    v14  00000000000000000000000000000000  v15  00000000000000000000000000000000
    v16  000000000000000000000000433d0bdb  v17  0000000000000000000000004300f791
    v18  ffffffffffffffffffffffffffffffff  v19  00000000000000000000000000000000
    v20  000000000000000000000000429b0da8  v21  0000007a3244bad40000000040066666
    v22  3d9a026fbe6e77413de4d4f6bde37556  v23  be46eb12bab5227e3e42ccdd3ddc0298
    v24  bd81aadc3baa94113d553e38bd7e0cfa  v25  3d973dfebe6f2a7a3df6b7edbdeade23
    v26  be3550e13b58e3993e4a9fc03dce668e  v27  bd667f7d3c8acb883d67db0dbd3870f1
    v28  3d95a576be6d76b93e067243bdeb7287  v29  be2c0caf3b2d9cfa3e47bc863da722ea
    v30  bd2c2f113d0578df3d823731bcdbef07  v31  3d93ca5dbe68d8923e133c79bde8fffc
    fpsr 00000017  fpcr 00000000

backtrace:
    #00 pc 0000000000241728  /data/app/com.sogou.iot.b1pro.launcher-fmvk7osx2wimDuqXl484ZQ==/lib/arm64/libttsoff.so
    #01 pc 00000000002436dc  /data/app/com.sogou.iot.b1pro.launcher-fmvk7osx2wimDuqXl484ZQ==/lib/arm64/libttsoff.so
    #02 pc 0000000000253a38  /data/app/com.sogou.iot.b1pro.launcher-fmvk7osx2wimDuqXl484ZQ==/lib/arm64/libttsoff.so
    #03 pc 0000000000252308  /data/app/com.sogou.iot.b1pro.launcher-fmvk7osx2wimDuqXl484ZQ==/lib/arm64/libttsoff.so
    #04 pc 00000000002545fc  /data/app/com.sogou.iot.b1pro.launcher-fmvk7osx2wimDuqXl484ZQ==/lib/arm64/libttsoff.so (Java_com_sogou_speech_tts_TTSOffline_nativeSynthesize+124)
    #05 pc 000000000005d9b4  /data/app/com.sogou.iot.b1pro.launcher-fmvk7osx2wimDuqXl484ZQ==/oat/arm64/base.odex (offset 0x5a000)

stack:
         0000007a34d6b440  0000007a34d6b4b0
         0000007a34d6b448  f1ecdfe5bc67f748
         0000007a34d6b450  0000000000000000
         0000007a34d6b458  401921fb54442d18
         0000007a34d6b460  0000000000000050
         0000007a34d6b468  0000000000000000
         0000007a34d6b470  000000000000013c

4.3 android 捕捉native crash 需要注册下面6个信号

Android 平台 捕捉natvie crash 一般只需要安装6个信号处理函数即可。

信号 信号值 含义 备注 在Android中默认行为
SIGSEGV 11 访问无效地址 如试图访问未分配给自己的内存 生成tombstone文件,然后退出
SIGBUS 7 非法地址 包括内存地址对齐(alignment)出错。 生成tombstone文件,然后退出
SIGABRT 6 调用abort函数生成的信号。 生成tombstone文件,然后退出
SIGFPE 8 浮点计算错误。 包括浮点运算错误, 还包括溢出及除数为0等算数运算错误 生成tombstone文件,然后退出
SIGILL 4 非法指令错误。 非法指令错误。 生成tombstone文件,然后退出
SIGTRAP 5 硬件错误(通常为断点指令) 生成tombstone文件,然后退出

三、信号处理函数的使用

3.1、安装信号处理函数

(1)sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。

(2)struct sigaction结构体介绍

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}

sa_handler代表新的信号处理函数,仅接受一个参数.
如:
void show_handler(int signo)
{
    printf("I got signal %d\n", signo);
}
sa_sigaction 同样为信号处理函数,相对于sa_handler可以获得更多的信息:
void handler(int signo, siginfo_t *info, void *context);

第二个参数为一个siginfo_t结构的指针,该结构描述了信号产生的原因

struct siginfo_t
{
    int     si_signo;       // signal number
    int     si_errno;       // if nonzero, errno value from <errno.h>
    int     si_code;        // additional info (depends on signal)
    pid_t   si_pid;         // sending process ID
    uid_t   si_uid;         // sending process real user ID
    void    *si_addr;       // address that cased the fault
    int     si_status;      // exit value or signal number
    long    si_band;        // band number for SIGPOLL
    
    /* possibly other fileds also */

}

一般siginfo_t结构至少包含si_signo和si_code成员。第三个参数context是一个无类型的指针,它可以被强制转换为ucntext_t结构类型,用于标识信号传递时进程的上下文。

当sig_action.sa_flags = SA_SIGINFO 时,会使用sa_sigaction作为信号处理函数;否则使用 sa_handler 作为信号处理函数。


备注:
sa_sigaction和sa_handler字段,其实现可能使用同一存储区,所以应用程序只能一次使用这两个字段中的一个。
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用:
  • SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL(默认处理方式)
  • SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
  • SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
  • SA_SIGINFO:设置选择sa_sigaction 作为信号处理函数,否则选择sa_handler作为信号处理函数。
  • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
选项 含义
SA_INTERRUPT 由此信号中断的系统调用不会自动重启
SA_NOCLDSTOP 若signo是SIGCHLD,当子进程停止(作业控制)时,不产生此信号。当子进程终止时,仍产生此信号(参加SA_NOCLDWAIT说明)。若已设置此标志,则当停止的进程继续运行时,作为XSI扩展,不发送SIGCHLD信号。
SA_NOCLDWAIT 若signo是SIGCHLD,则当调用进程的子进程终止时,不创建僵尸进程。若调用进程在后面调用wait,则调用进程阻塞,直到其所有子进程都终止,此时返回-1,并将errno设置为ECHILD。
SA_NODEFER 当捕捉到此信号时,在执行其信号处理函数时,系统不自动阻塞此信号(除非sa_mask包括了此信号)。
SA_ONSTACK 若用sigaltstack声明了替换栈,则将此信号递送给替换栈上的进程。
SA_RESETHAND 在此信号处理函数的入口处,将此信号的处理方式复位为SIG_DEF,并清除SA_SIGINFO标志。但是,不能自动复位SIGILL和SIGTRAP这两个信号的配置。设置此标志是sigaction的行为如同SA_NODEFER标志也设置了一样。
SA_RESTART 由此信号中断的系统调用会自动重启动。
SA_SIGINFO 设置了该标志后,会选择sa_sigaction 作为信号处理函数,否则选择sa_handler作为信号处理函数。

(3)要用信号处理函数捕获到native crash(SIGSEGV, SIGBUS等) 可以使用sig_action ,如下:

信号处理函数

void posix_signal_handler(int sig, siginfo_t *siginfo, void *context)
{
    (void)context;
    switch(sig)
    {
        case SIGSEGV:
            fputs("Caught SIGSEGV: Segmentation Fault\n", stderr);
            break;
        case SIGINT:
            fputs("Caught SIGINT: Interactive attention signal, (usually ctrl+c)\n",
                  stderr);
            break;
        case SIGFPE:
            switch(siginfo->si_code)
            {
                case FPE_INTDIV:
                    fputs("Caught SIGFPE: (integer divide by zero)\n", stderr);
                    break;
                case FPE_INTOVF:
                    fputs("Caught SIGFPE: (integer overflow)\n", stderr);
                    break;
                case FPE_FLTDIV:
                    fputs("Caught SIGFPE: (floating-point divide by zero)\n", stderr);
                    break;
                case FPE_FLTOVF:
                    fputs("Caught SIGFPE: (floating-point overflow)\n", stderr);
                    break;
                case FPE_FLTUND:
                    fputs("Caught SIGFPE: (floating-point underflow)\n", stderr);
                    break;
                case FPE_FLTRES:
                    fputs("Caught SIGFPE: (floating-point inexact result)\n", stderr);
                    break;
                case FPE_FLTINV:
                    fputs("Caught SIGFPE: (floating-point invalid operation)\n", stderr);
                    break;
                case FPE_FLTSUB:
                    fputs("Caught SIGFPE: (subscript out of range)\n", stderr);
                    break;
                default:
                    fputs("Caught SIGFPE: Arithmetic Exception\n", stderr);
                    break;
            }
        case SIGILL:
            switch(siginfo->si_code)
            {
                case ILL_ILLOPC:
                    fputs("Caught SIGILL: (illegal opcode)\n", stderr);
                    break;
                case ILL_ILLOPN:
                    fputs("Caught SIGILL: (illegal operand)\n", stderr);
                    break;
                case ILL_ILLADR:
                    fputs("Caught SIGILL: (illegal addressing mode)\n", stderr);
                    break;
                case ILL_ILLTRP:
                    fputs("Caught SIGILL: (illegal trap)\n", stderr);
                    break;
                case ILL_PRVOPC:
                    fputs("Caught SIGILL: (privileged opcode)\n", stderr);
                    break;
                case ILL_PRVREG:
                    fputs("Caught SIGILL: (privileged register)\n", stderr);
                    break;
                case ILL_COPROC:
                    fputs("Caught SIGILL: (coprocessor error)\n", stderr);
                    break;
                case ILL_BADSTK:
                    fputs("Caught SIGILL: (internal stack error)\n", stderr);
                    break;
                default:
                    fputs("Caught SIGILL: Illegal Instruction\n", stderr);
                    break;
            }
            break;
        case SIGTERM:
            fputs("Caught SIGTERM: a termination request was sent to the program\n",
                  stderr);
            break;
        case SIGABRT:
            fputs("Caught SIGABRT: usually caused by an abort() or assert()\n", stderr);
            break;
        default:
            break;
    }

    _Exit(1);
}

安装信号处理函数


{
        struct sigaction sig_action = {};
        sig_action.sa_sigaction = posix_signal_handler;
        sigemptyset(&sig_action.sa_mask);

#ifdef __APPLE__
        /* for some reason we backtrace() doesn't work on osx
           when we use an alternate stack */
        sig_action.sa_flags = SA_SIGINFO;
#else
        sig_action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#endif

        if (sigaction(SIGSEGV, &sig_action, NULL) != 0) { err(1, "sigaction"); }
}

3.2 处理栈溢出 - sigaltstack()

Native crash 中有一种是堆栈溢出错误。调用函数时会将被调用函数入栈,并保存该函数中的局部变量等信息。当栈满了(太多次递归,栈上太多对象)时,系统会在同一个已经满了的栈上调用SIGSEGV的信号处理函数,又再一次引起同样的信号。

sigaltstack() 允许进程创建一个备用的栈,供信号处理函数使用。

int sigaltstack(const stack_t *ss, stack_t *oss);

该函数两个个参数为均为stack_t类型的结构体

typedef struct {
   void  *ss_sp;     /* Base address of stack */
   int    ss_flags;  /* Flags */
   size_t ss_size;   /* Number of bytes in stack */
}

要想创建一个新的可替换信号栈,ss_flags必须设置为0,ss_sp和ss_size分别指明可替换信号栈的起始地址和栈大小。

sigaltstack第一个参数为创建的新的可替换信号栈,第二个参数可以设置为NULL,如果不为NULL的话,将会将旧的可替换信号栈的信息保存在里面。函数成功返回0,失败返回-1.

使用可替换信号栈的步骤如下:

  • 在内存中分配一块区域作为可替换信号栈
  • 使用sinalstack()函数通知系统 存在一个可以替换的信号栈。
  • 使用sigaction()函数建立信号处理函数的时候,通过将sa_flags设置为SA_ONSTACK来告诉系统信号处理函数将在可替换信号栈上面运行。

示例代码:


stack_t stack;
memset(&stack, 0, sizeof(stack));
/* Reserver the system default stack size. We don't need that much by the way. */
stack.ss_size = SIGSTKSZ;
stack.ss_sp = malloc(stack.ss_size);
stack.ss_flags = 0;
/* Install alternate stack size. Be sure the memory region is valid until you revert it. */
if (stack.ss_sp != NULL && sigaltstack(&stack, NULL) == 0) {
  ...
}

3.3 兼容旧的信号处理函数

我们在java虚拟机上运行,某些信号可能在之前已经被安装过信号处理函数。例如,SIGSEGV经常用于处理NullPointerException.
所以,你必须先调用旧的信号处理函数,以防把上下文环境搞乱。旧的信号处理函数要么不进行处理直接返回,要么调用abort()(这样我们就有最后一次机会通过SIGABRT的信号处理函数处理这个信号,所以在捕获SIGABRT的信号处理函数里边,我们是最后才调用旧的信号处理函数)

static void my_handler(const int code, siginfo_t *const si, void *const sc) {
  /* Call previous handler. */
  old_handler.sa_sigaction(code, si, sc);
...

}

3.4 提取崩溃信息

信号处理函数中有丰富的信息,供我们分析crash发生的原因

/*信号处理函数*/
void (*sa_sigaction)(const int code, siginfo_t *const si, void * const sc) 

siginfo_t {
   int      si_signo;     /* Signal number 信号量 */
   int      si_errno;     /* An errno value */
   int      si_code;      /* Signal code 错误码 */
   }

1、 signo 和code

发生native crash之后,logcat中会打出如下一句信息:
signal 11 (SIGSEGV), code 0 (SI_USER), fault addr 0x0
sigaction 第二个参数为siginfo_t结构体,其中的 si_signo和si_code 可以得出crash的大致原因

2、 收集发生crash的地址(pc):

sigaction回调函数的第三个参数是一个指向ucontext_t的指针,ucontext_t收集了寄存器数值(还有各种处理器特定的信息)。
在x86-64架构,pc值是存在uc_mcontext.gregs[REG_RIP];
在arm架构,则是uc_mcontext.arm_pc。不过在Android上,ucontext_t结构体没有在任何系统头文件定义,所以要自己去引入一份定义。

3、找出crash的pc,属于哪个二进制文件或so。分析是哪个so库出现了奔溃,奔溃在哪个函数。

(1) dladdr() 根据pc值获得共享库名字和相对偏移地址

dladdr 可以将pc程序计数器指向的决定地址 转换成Dl_info 对象。从而计算出pc指向地址 所在的动态库名字,该动态库的基地址,pc地址最近的符号名等。

Dl_info 结构体个字段的意义

typedef struct {
  /* Pathname of shared object that contains address. */
  const char* dli_fname; //共享库的完全路径名
  /* Address at which shared object is loaded. */
  void* dli_fbase;     //该共享库的基地址
  /* Name of nearest symbol with address lower than addr. */
  const char* dli_sname; //和指定pc值最近的符号名
  /* Exact address of symbol named in dli_sname. */
  void* dli_saddr;      //dli_name 符号名的绝对地址
} Dl_info;

dladdr()用法:

    Dl_info info;  
if (dladdr(addr, &info) != 0 && info.dli_fname != NULL) {  
  void * const nearest = info.dli_saddr;  
  //相对偏移地址
  const uintptr_t addr_relative =  
    ((uintptr_t) addr - (uintptr_t) info.dli_fbase);  
  ...  
}

4、如何根据pc 手动分析出 动态库的名字和相对地址

(1)linux进程的地址分布空间
image.png

用户进程部分分段存储内容如下表所示(按地址递减顺序):

名称 存储内容
局部变量、函数参数、返回地址等
动态分配的内存
BSS段 未初始化或初值为0的全局变量和静态局部变量
数据段 已初始化且初值非0的全局变量和静态局部变量
代码段 执行代码、字符串字面值、只读变量

任何一个程序通常都包括代码段和数据段,这些代码和数据本身都是静态的。程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。光有代码段和数据段是不够的,进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。

上图中Random stack offset和Random mmap offset等随机值意在防止恶意程序。Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等地址。

栈(stack),作为进程的临时数据区,增长方向是从高地址到低地址。

(2) /proc/self/maps: 查看各模块加载在内存中的地址范围(绝对地址范围)

在Linux系统中,/proc/self/maps保存了各个程序段在内存中的加载地址范围,grep出共享库的名字,就可以知道共享库的加载基值是多少。

feifeideMacBook-Pro:s1 feifei$ adb shell ps | grep com.sogou.translate.example
u0_a83       24460  2977 4455820 111148 0                   0 S com.sogou.translate.example
u0_a83       24478  2977 4362888  71852 0                   0 S com.sogou.translate.example:logservice
查看24460 com.sogou.translate.example 进程对应的map文件
adb shell cat /proc/24460/maps

6fb7751000-6fb77e6000 r-xp 00000000 103:19 61608                         /data/app/com.sogou.translate.example-xMTUqjEw8dDDCbBi01nLgg==/lib/arm64/libbreakpad-core.so
6fb77e6000-6fb77f6000 ---p 00000000 00:00 0 
6fb77f6000-6fb77fb000 r--p 00095000 103:19 61608                         /data/app/com.sogou.translate.example-xMTUqjEw8dDDCbBi01nLgg==/lib/arm64/libbreakpad-core.so
6fb77fb000-6fb77fc000 rw-p 0009a000 103:19 61608                         /data/app/com.sogou.translate.example-xMTUqjEw8dDDCbBi01nLgg==/lib/arm64/libbreakpad-core.so
6fb77fc000-6fb77fd000 rw-p 00000000 00:00 0                              [anon:.bss]
6fb7800000-6fb7a00000 rw-p 00000000 00:00 0                              [anon:libc_malloc]
6fb7a45000-6fb7a47000 r-xp 00000000 103:14 1811                          /system/lib64/vndk-sp/libion.so
6fb7a47000-6fb7a48000 r--p 00001000 103:14 1811                          /system/lib64/vndk-sp/libion.so
6fb7a48000-6fb7a49000 rw-p 00002000 103:14 1811                          /system/lib64/vndk-sp/libion.so
6fb7ab5000-6fb7ab6000 ---p 00000000 00:00 0                              [anon:thread stack guard page]

可见libbreakpad-core.so的地址范围是6fb7751000-6fb77e6000,基地址为6fb7751000

(3) 利用readelf查看共享库的符号表
安装

在linux下,用readelf来看ELF头部或者其它各section的内容,用objdump来对指定的内容(.text, .data等)进行反汇编。

但是mac os X下没有这两个命令,可以用brew来安装

brew update && brew install binutils

将libnutils 安装路径加入到环境变量

vim ~/.bash_profile

添加一行:
PATH=${PATH}:/usr/local/Cellar/binutils/2.33.1/bin

然后执行:
source ~/.bash_profile
readelf -s 查看so库的符号表

参数 -s,symbols 显示符号表段中的项(如果有数据的话)

 readelf -s libbreakpad-core.so 
 

部分结果如下:

 354: 000000000007d794     3 OBJECT  GLOBAL DEFAULT   12 _ZTSDu
   355: 000000000004a180   176 FUNC    WEAK   DEFAULT   11 _ZNSt6__ndk116allocator_t
   356: 00000000000298a0   812 FUNC    GLOBAL DEFAULT   11 _Z7tryDumpv
   357: 00000000000385e4    88 FUNC    WEAK   DEFAULT   11 _ZNSt6__ndk19allocatorINS
   358: 0000000000041920    52 FUNC    WEAK   DEFAULT   11 _ZNSt6__ndk116allocator_t
   359: 00000000000a5940    24 OBJECT  GLOBAL DEFAULT   18 _ZTIN10__cxxabiv116__shim
   360: 000000000002a89c    44 FUNC    GLOBAL DEFAULT   11 _Z23call_dangerous_functi
   361: 0000000000061f78    84 FUNC    GLOBAL DEFAULT   11 _ZNSt12length_errorD2Ev
   362: 0000000000038f88   316 FUNC    WEAK   DEFAULT   11 _ZNSt6__ndk112basic_strin
   363: 000000000004f6f8   704 FUNC    WEAK   DEFAULT   11 _ZN15google_breakpad6CpuS
   364: 0000000000045500    52 FUNC    WEAK   DEFAULT   11 _ZNSt6__ndk116allocator_t
   365: 0000000000045b2c   140 FUNC    WEAK   DEFAULT   11 _ZNKSt6__ndk16vectorIPN15
   366: 0000000000057890    32 FUNC    WEAK   DEFAULT   11 _ZN15google_breakpad9GetO
   367: 00000000000367cc   132 FUNC    WEAK   DEFAULT   11 _ZNSt6__ndk117__compresse
   368: 0000000000049290   168 FUNC    GLOBAL DEFAULT   11 _ZN15google_breakpad13Wri
   369: 000000000003e018   180 FUNC    WEAK   DEFAULT   11 _ZN15google_breakpad15was
   370: 0000000000047ca0    96 FUNC    WEAK   DEFAULT   11 _ZNSt6__ndk116allocator_t
   371: 00000000000aa048     1 OBJECT  GLOBAL DEFAULT   22 global_interruptSystemCra

dangerous_function 和
global_interruptSystemCrash 都是so库中定义的函数,可以看到这两个函数符号加载到内存中的偏移量。

四 演示demo 代码

下面是一个mac 或linux平台上运行的sigaction()的小demo,用来熟悉sinal处理函数的特性。

TestSignalPosix.c

//
// Created by 飞飞 on 2020-01-22.
//

#include <stdio.h>
#include <signal.h>
#include <assert.h>
#include <stdlib.h>
#include <err.h>
#include <execinfo.h>
#include <string.h>

int  divide_by_zero();
void cause_segfault();
void stack_overflow();
void infinite_loop();
void illegal_instruction();
void cause_calamity();

void set_signal_handler_4_posix();
void posix_signal_handler(int sig, siginfo_t *siginfo, void *context);

static char const * icky_global_program_name;
int addr2line(char const * const program_name, void const * const addr);
void posix_print_stack_trace();


const int kExceptionSignals[] = {
        SIGSEGV, SIGABRT, SIGFPE, SIGILL, SIGBUS, SIGTRAP
};

//--- 信号量个数
const int kNumHandledSignals =
        sizeof(kExceptionSignals) / sizeof(kExceptionSignals[0]);

//--- 每个信号的原有信号处理函数
struct sigaction old_handlers[kNumHandledSignals];

int main(int argc, char * argv[])
{
    (void)argc;

    /* store off program path so we can use it later */
    icky_global_program_name = argv[0];
    char* exceptionType = argv[1];

    set_signal_handler_4_posix();

    cause_calamity(exceptionType);


    printf("OMG! Nothing bad happend!");
    return 0;
}

void cause_calamity(char* type)
{
    /* uncomment one of the following error conditions to cause a calamity of
     your choosing! */

    if(strcmp(type,"1") == 0){
        fputs("cause_calamity type = 1 ,create a  divide_by_zero error \n", stderr);
        (void)divide_by_zero();
    } else if(strcmp(type,"2") == 0){
        fputs("cause_calamity type = 2 ,create a  cause_segfault error \n", stderr);
         cause_segfault();
    } else if(strcmp(type,"3") == 0){
        fputs("cause_calamity type = 3 ,create a assert(false)  error\n", stderr);
//         assert(false);
    } else if(strcmp(type,"4") == 0){
        fputs("cause_calamity type = 4 ,,create a infinite_loop \n", stderr);
        infinite_loop();
    } else if(strcmp(type,"5") == 0){
        fputs("cause_calamity type = 5 ,,create a illegal_instruction \n", stderr);
        illegal_instruction();
    } else if(strcmp(type,"6") == 0){
        fputs("cause_calamity type = 6 ,,create a stack_overflow\n", stderr);
        stack_overflow();
    }



    // infinite_loop();
    // illegal_instruction();
    // stack_overflow();
}

int divide_by_zero()
{
    int a = 1;
    int b = 0;
    return a / b;
}

void cause_segfault()
{
    int * p = (int*)0x12345678;
    *p = 0;
}

void stack_overflow();
void stack_overflow()
{
    int foo[1000]; //allocate something big on the stack
    (void)foo;
    stack_overflow();
}

/* break out with ctrl+c to test SIGINT handling */
void infinite_loop()
{
    while(1) {};
}

void illegal_instruction()
{
    /* I couldn't find an easy way to cause this one, so I'm cheating */
    raise(SIGILL);
}



void posix_signal_handler(int sig, siginfo_t *siginfo, void *context)
{

    fputs("\n\n",stderr);

    printf("received signal no:%d,code:%d\n",sig,siginfo->si_code);


    fputs("\n\n",stderr);
    (void)context;
    switch(sig)
    {
        case SIGSEGV:
            fputs("Caught SIGSEGV: Segmentation Fault\n", stderr);
            break;
        case SIGINT:
            fputs("Caught SIGINT: Interactive attention signal, (usually ctrl+c)\n",
                  stderr);
            break;
        case SIGFPE:
            switch(siginfo->si_code)
            {
                case FPE_INTDIV:
                    fputs("Caught SIGFPE: (integer divide by zero)\n", stderr);
                    break;
                case FPE_INTOVF:
                    fputs("Caught SIGFPE: (integer overflow)\n", stderr);
                    break;
                case FPE_FLTDIV:
                    fputs("Caught SIGFPE: (floating-point divide by zero)\n", stderr);
                    break;
                case FPE_FLTOVF:
                    fputs("Caught SIGFPE: (floating-point overflow)\n", stderr);
                    break;
                case FPE_FLTUND:
                    fputs("Caught SIGFPE: (floating-point underflow)\n", stderr);
                    break;
                case FPE_FLTRES:
                    fputs("Caught SIGFPE: (floating-point inexact result)\n", stderr);
                    break;
                case FPE_FLTINV:
                    fputs("Caught SIGFPE: (floating-point invalid operation)\n", stderr);
                    break;
                case FPE_FLTSUB:
                    fputs("Caught SIGFPE: (subscript out of range)\n", stderr);
                    break;
                default:
                    fputs("Caught SIGFPE: Arithmetic Exception\n", stderr);
                    break;
            }
        case SIGILL:
            switch(siginfo->si_code)
            {
                case ILL_ILLOPC:
                    fputs("Caught SIGILL: (illegal opcode)\n", stderr);
                    break;
                case ILL_ILLOPN:
                    fputs("Caught SIGILL: (illegal operand)\n", stderr);
                    break;
                case ILL_ILLADR:
                    fputs("Caught SIGILL: (illegal addressing mode)\n", stderr);
                    break;
                case ILL_ILLTRP:
                    fputs("Caught SIGILL: (illegal trap)\n", stderr);
                    break;
                case ILL_PRVOPC:
                    fputs("Caught SIGILL: (privileged opcode)\n", stderr);
                    break;
                case ILL_PRVREG:
                    fputs("Caught SIGILL: (privileged register)\n", stderr);
                    break;
                case ILL_COPROC:
                    fputs("Caught SIGILL: (coprocessor error)\n", stderr);
                    break;
                case ILL_BADSTK:
                    fputs("Caught SIGILL: (internal stack error)\n", stderr);
                    break;
                default:
                    fputs("Caught SIGILL: Illegal Instruction\n", stderr);
                    break;
            }
            break;
        case SIGTERM:
            fputs("Caught SIGTERM: a termination request was sent to the program\n",
                  stderr);
            break;
        case SIGABRT:
            fputs("Caught SIGABRT: usually caused by an abort() or assert()\n", stderr);
            break;
        default:
            break;
    }
    posix_print_stack_trace();

    fputs("\n\n",stderr);

    _Exit(1);

}

static uint8_t alternate_stack[SIGSTKSZ];
void set_signal_handler_4_posix()
{
    /* setup alternate stack */
    {
        stack_t ss = {};
        /* malloc is usually used here, I'm not 100% sure my static allocation
           is valid but it seems to work just fine. */
        ss.ss_sp = (void*)alternate_stack;
        ss.ss_size = SIGSTKSZ;
        ss.ss_flags = 0;

        if (sigaltstack(&ss, NULL) != 0) { err(1, "sigaltstack"); }
    }

    /* register our signal handlers */
    {
        struct sigaction sig_action = {};
        sig_action.sa_sigaction = posix_signal_handler;
        sigemptyset(&sig_action.sa_mask);

#ifdef __APPLE__
        /* for some reason we backtrace() doesn't work on osx
           when we use an alternate stack */
        sig_action.sa_flags = SA_SIGINFO;
#else
        sig_action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#endif

        for(int i = 0;i<kNumHandledSignals;i++ ){
            printf("install sigaction for signal  %d\n",kExceptionSignals[i]);
            if(sigaction(kExceptionSignals[i], &sig_action, &old_handlers[i])==-1){
                printf("set sinalaction failed,just return");
            }
        }
    }
}


#define MAX_STACK_FRAMES 64
static void *stack_traces[MAX_STACK_FRAMES];
void posix_print_stack_trace()
{

    fputs("\n\n",stderr);
    int i, trace_size = 0;
    char **messages = (char **)NULL;

    trace_size = backtrace(stack_traces, MAX_STACK_FRAMES);
    messages = backtrace_symbols(stack_traces, trace_size);

    /* skip the first couple stack frames (as they are this function and
       our handler) and also skip the last frame as it's (always?) junk. */
    // for (i = 3; i < (trace_size - 1); ++i)
    // we'll use this for now so you can see what's going on
    for (i = 0; i < trace_size; ++i)
    {
        if (addr2line(icky_global_program_name, stack_traces[i]) != 0)
        {
            printf("  error determining line # for: %s\n", messages[i]);
        }

    }
    if (messages) { free(messages); }
}


/* Resolve symbol name and source location given the path to the executable
   and an address */
int addr2line(char const * const program_name, void const * const addr)
{
    char addr2line_cmd[512] = {0};

    /* have addr2line map the address to the relent line in the code */
#ifdef __APPLE__
    /* apple does things differently... */
    sprintf(addr2line_cmd,"atos -o %.256s %p", program_name, addr);
#else
    sprintf(addr2line_cmd,"addr2line -f -p -e %.256s %p", program_name, addr);
#endif

    /* This will print a nicely formatted string specifying the
       function and source line of the address */
//    sprintf("addr2line: %s",addr2line_cmd);
    return system(addr2line_cmd);
}

编译指令:

mac上:
gcc -lpthread  -g -fno-pie TestSignalPosix.c -o TestSignalPosix

linux上:
gcc -lpthread  -g  TestSignalPosix.c -o TestSignalPosix

gcc 参数

  • -l 指定gcc 需要加载的动态链接库。
  • -g 为gdb调试用,会做两件事情:创建符号表,符号表包含了程序中使用的变量名称的列表;关闭所有的优化机制,以便程序执行过程中严格按照原来的C代码进行。

测试结果:

feifeideMacBook-Pro:cpp feifei$ ./TestSignalPosix 2
install sigaction for signal  11
install sigaction for signal  6
install sigaction for signal  8
install sigaction for signal  4
install sigaction for signal  10
install sigaction for signal  5
cause_calamity type = 2 ,create a  cause_segfault error 


received signal no:11,code:1


Caught SIGSEGV: Segmentation Fault



posix_print_stack_trace (in TestSignalPosix) (TestSignalPosix.c:271)
posix_signal_handler (in TestSignalPosix) (TestSignalPosix.c:217)
0x7fff7024cb5d
0x0000ffff (in TestSignalPosix)
cause_calamity (in TestSignalPosix) (TestSignalPosix.c:67)
main (in TestSignalPosix) (TestSignalPosix.c:52)
0x7fff700673d5


五、 参考文献:

https://www.jianshu.com/p/78a363ea48df

sigaction demo:https://spin.atomicobject.com/2013/01/13/exceptions-stack-traces-c/

信号定义和行为:https://blog.csdn.net/yeyuwuhen1203/article/details/78031391

信号处理函数:https://blog.csdn.net/weibo1230123/article/details/81411827

natvie crash 捕获:https://blog.csdn.net/mba16c35/article/details/54178067

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • 一、信号及信号来源 信号本质 信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一...
    丶Em1tu0F阅读 1,383评论 0 1
  • 信号本质 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,...
    飞扬code阅读 723评论 0 2
  • 文/tangsl(简书作者) 原文链接:http://www.jianshu.com/p/2b993a4b913e...
    西葫芦炒胖子阅读 3,684评论 0 5
  • 对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件...
    故事狗阅读 83,421评论 2 61
  • 计算机系统漫游 代码从文本到可执行文件的过程(c语言示例):预处理阶段,处理 #inlcude , #defin...
    willdimagine阅读 3,506评论 0 5