一、信号机制
函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换。
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@[email protected] (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
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进程的地址分布空间
用户进程部分分段存储内容如下表所示(按地址递减顺序):
名称 | 存储内容 |
---|---|
栈 | 局部变量、函数参数、返回地址等 |
堆 | 动态分配的内存 |
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
#include
#include
#include
#include
#include
#include
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
编译指令:
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