Android应用上的Crash主要分为两种:
1. Java Crash : java代码导致jvm退出,弹出“程序已经崩溃”的类似对话框,最终用户点击关闭后进程退出。Logcat会在"AndroiRuntime" 中输出崩溃的信息
2. Native Crash : 通过NDK,使用C/C++开发,导致进程收到错误信号,发生Crash,Android5.0之前进程会直接弹出(闪退),Android5.0之后会弹出“程序已崩溃”的对话框,最后Logcat会在"debug"中输出dump弹出的信息
什么示错误信号:
Android本质就是一个Linux,信号跟Linux信号示同一个东西,信号本身示用于进程间通信的没有正确错误之分,但是Android官方给信号赋予了特定的含义及处理动作
信号通常的来源:
1. 硬件发生异常,及硬件(通常示CPU)检测到一个错误条件并通知Linux内核,内核处理该异常,给相应的进程发送信号。硬件异常的情况包括执行一条异常的机器语言指令。比如被0除,或者引用了无法访问的内存区域。大部分信号如果没有被进程处理,默认的操作就是杀死进程。其中SIGSEGV(段错误)、SIGBUS(内存访问错误)、SIGFPE(算数错误)就属于这种信号(错误信号)
2. 进程调用的库发现错误,会给自己发送中止信号,默认情况下,该信号会终止进程。其中,SIGABRT(中止进程)属于这种信号
3. 用户或第三方恶意通过kill-信号 pid的方式给错误进程发送,这时signal中的si_code会小于0.
常见的崩溃:
1. 空指针
代码实例:
int * p = 0; //空指针
*p = 1; //写空指针指向的内存,产生SIGSEGV信号,造成Crash
原因分析: 在进程的地址空间中,从0开始的第一个页面的权限被设置为不可读也不可写,当进程的指令试图访问该页面中的地址时(如读取空指针指向的内存),处 理器就会产生一个异常,然后Linux内核会给进程发送一个段错误信号(SIGSEGV),默认的操作就是杀死进程,并产生core文件。
解决方法: 在使用指针前加以判断,如果为空,则是不可访问的。
Bug评述: 空指针时很容易出现的一种bug,在代码量大,快速迭代发布的今天,是很容易出现的错误。但是它也很容易被发现和修复。
2. 野指针
代码实例:
int* p; //野指针,未初始化,其指向的地址通常是随机的
*p = 1; //写野指针指向的内存,有可能不会马上Crash,而是破坏了别处的内存
原因分析: 野指针指向的是一个无效的地址,该地址如果是不可读不可写的,那么会马上Crash(Linux内核给进程发送段错误信号SIGSEGV),这是bug会很容易被发现。如果访问的地址为可写,而且通过野指针修改了该处的内存,那么很可能会等一段时间(其他的代码使用了该处的内存后)才会发生Crash。。这是查看Crash时显示的调用栈,和野指针所在的代码部分,有可能基本上没有任何关联。
解决方法: 1. 在指针变量定义时,一定要初始化,特别是在结构体或类中的成员指针变量
2. 在释放了该指针的内存后,要把该指针设置为NULL(但是如果在别的地方也有指针指向该内存,那这种方式就不好解决了)
3. 野指针造成的内存破坏的问题,有时候光看代码是很难查找的,通过代码分析工具也很难查找,只有通过专业的内存检测工具,才能发现这类bug
Bug评述: 野指针的bug,特别是内存被破坏的问题,有时候查起来毫无头绪,没有一点线索,让开发者感觉很迷茫(日志上报的信息查不出任何问题)。可以说内存破坏这类的bug是服务器稳定的最大杀手,也是开发者最应该注意的问题(c/c++开发尤其注意)
3. 数组越界
代码实例:
int arr[10]
arr[10] = 1; //数组越界,超过了数组长度的限制,有可能会马上Crash,也有可能会破坏别处的内存
原因分析: 数组越界和野指针类似,访问了无效的地址,如果该地址不可读写,则会马上Crash(Linux内核会给进程发送段错误信号SIGSEGV),如果修改了该处的内存,造成内存破坏,那么有可能要等一段时间才在别处发生Crash。
解决方法:1. 所有数组遍历的循环,都要加上越界判断
2. 用下标访问数组时,要判断是否越界(是否超出了数组的长度)
3. 通过代码分析工具可以发现绝大部分的数组越界问题
Bug评述: 数组越界也是一种内存破坏的bug,有时候与野指针一样也是很难查找的
4. 整数除以0
代码实例:
int a = 1;
int b = a/0; //整数除以0,产生SIGFPE信号,导致Crash
原因分析: 整数除以0总是产生SIGFPE(浮点异常,产生SIGFPE信号时并非一定要涉及浮点算数,整数运算异常也用浮点异常信号是为了保持向下兼容性)信号,默认的处理方式是终止进程,并生成core文件。
解决方法: 在做整数除法时,要判断被除数是否为0的情况。
Bug评述: 整数被0除的bug很容易被开发者忽视,因为被除数为0的情况在开发及测试环境下很难出现,但是到了生产应用环节,庞大的用户量和复杂的用户输入,就很容易导致被除数为0的情况出现了。
5. 格式化输出参数错误
代码实例:
char text[200]; //格式化参数错误,可能会导致非法的内存访问,从而造成宕机
snprintf(text,200,"Valid %u, Invalid %u %s", 1); //format格式不匹配
原因分析: 格式化参数和野指针也很类似,但是它只会读取无效地址的内存,而不会造成内存破坏。其结果是要么打印出错乱的数据,要么访问了无读写权限的内存(收到段错误信号SIGSEGV)而立即宕机。
解决方法:在书写输出格式和参数时,要做到参数个数和类型都要与输出格式一致。并且最好在GCC的编译选项中加入-wformat,让GCC在编译时检测此类错误。
6. 缓冲区溢出
代码实例:
char szBuffer[10];
sprintf(szBuffer,"Stack Buffer Overrun!888888888888888888888" "88888888888888888888888888888888")
原因分析: 通过往程序的缓冲区写超出其长度的内容,造成缓冲区溢出,函数的堆栈被破坏,修改函数调用的返回地址,在函数返回时会跳转到未知的地址上。如果不是黑客故意攻击,那么最终函数调用很可能会跳转到无法读写的内存区域,产生段错误信号SIGSEGV或SIGABRT,造成程序崩溃。
解决方法: 1. 检查所有容易产生漏洞的库调用,比如sprintf,strcpy等,它们都没有检查输入参数的长度。
2. 使用带有长度检查的库调用,如用snprintf来代替sprintf,或者自己在sprintf上封装一个带长度检查的函数。
3. 在GCC编译时,在-O1以上的优化行为下,使用-D_FORTIFY_SOURCE=level进行编译(level=1或2,代表的是检测级别不同,数值越大越严格),这样GCC会在编译时报告缓冲区溢出的错误。
4. 在GCC编译时加上-fstack-protector或-fstack-protector-all选项,使得堆栈保护功能生效。该功能会在编译后的汇编代码中插入堆栈检测的代码,并在运行时能够检测到栈破坏并输出报告。
Bug评述: 缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。黑客在进行攻击时,输入的字符串一般不会让程序崩溃,而是修改函数的返回地址,使程序跳转到别的地方,转而执行黑客安排好的指令,以此达到攻击的目的。缓冲区溢出后,调试生成的core,可以看见调用栈是混乱的,因为函数的返回地址已经被修改到随机的地址上去了。服务器宕机后,如果core文件和可执行文件是匹配的,但是调用栈时错乱的,那么很大的可能性是发生了缓冲区溢出。
以上就是关于应用程序常见的一些崩溃的总结,希望对测试及开发从业者有一些帮助。