常见Android Native崩溃及错误原因

2019-03-26 19:26:36

一、什么是Android的C/C++ NativeCrash

Android上的Crash可以分两种:

1、Java Crash
java代码导致jvm退出,弹出“程序已经崩溃”的对话框,最终用户点击关闭后进程退出。
Logcat 会在“AndroidRuntime”tag下输出Java的调用栈。

2、
通过NDK,使用C/C++开发,导致进程收到错误信号,发生Crash,Android 5.0之前进程直接退出(闪退) , Android 5.0之后会弹“程序已崩溃”的对话框。

Logcat 会在“debug”tag下输出dump信息:

  • 错误信号:11是信号量sigNum,SIGSEGV是信号的名字,SEGV_MAPERR是SIGSEGV下的一种类型。

  • 寄存器快照:进程收到错误信号时保存下来的寄存器快照,其中PC寄存器存储的就是下个要运行的指令(出错的位置)。

  • 调用栈:#00是栈顶,#02是栈底,#02调用#01调用#00方法,#00的方法时libspirit.so中的Spirit类下的testCrash方法,出错的地方是testCrash方法内汇编偏移17(不是行号哦!)

UKYUQA}TYQ(3SPXORCQAU$4

二、什么是错误信号

Android本质就是一个Linux,信号跟Linux信号是同一个东西,信号本身是用于进程间通信的没有正确错误之分,但官方给一些信号赋予了特定的含义及特定处理动作,

通常我们说的错误信号有5个(Bugly全部都能上报),系统默认处理就是dump出堆栈,并退出进程:

}RLH@[`~UMNVFJU`(~3NTVN

通常的来源有三个:
1、硬件发生异常,即硬件(通常是CPU)检测到一个错误条件并通知Linux内核,内核处理该异常,给相应的进程发送信号。硬件异常的例子包括执行一条异常的机器语言指令,诸如,被0除,或者引用了无法访问的内存区域。大部分信号如果没有被进程处理,默认的操作就是杀死进程。在本文中,SIGSEGV(段错误),SIGBUS(内存访问错误),SIGFPE(算数异常)属于这种信号。

2、进程调用的库发现错误,给自己发送中止信号,默认情况下,该信号会终止进程。在本文中,SIGABRT(中止进程)属于这种信号。

3、用户(手贱)或第三方App(恶意)通过kill-信号 pid的方式给错误进程发送,这时signal中的si_code会小于0。

三、抖几个常见错误

1. 空指针

代码示例

1

2

int* p = 0; //空指针

*p = 1; //写空指针指向的内存,产生SIGSEGV信号,造成Crash

原因分析

在进程的地址空间中,从0开始的第一个页面的权限被设置为不可读也不可写,当进程的指令试图访问该页面中的地址时(如读取空指针指向的内存),处理器就会产生一个异常,然后Linux内核会给该进程发送一个段错误信号(SIGSEGV),默认的操作就是杀死进程,并产生core文件。

解决方法

在使用指针前加以判断,如果为空,则是不可访问的。

Bug评述

空指针是很容易出现的一种bug,在代码量大,赶开发进度时很容易出现,但是它也很容易被发现和修复。

2. 野指针

代码示例

1

2

int* p; //野指针,未初始化,其指向的地址通常是随机的

*p = 1; //写野指针指向的内存,有可能不会马上Crash,而是破坏了别处的内存

原因分析

野指针指向的是一个无效的地址,该地址如果是不可读不可写的,那么会马上Crash(内核给进程发送段错误信号SIGSEGV),这时bug会很快被发现。
如果访问的地址为可写,而且通过野指针修改了该处的内存,那么很有可能会等一段时间(其它的代码使用了该处的内存后)才发生Crash。这时查看Crash时显示的调用栈,和野指针所在的代码部分,有可能基本上没有任何关联。

解决方法

  1. 在指针变量定义时,一定要初始化,特别是在结构体或类中的成员指针变量。

  2. 在释放了指针指向的内存后,要把该指针置为NULL(但是如果在别的地方也有指针指向该处内存的话,这种方式就不好解决了)。

  3. 野指针造成的内存破坏的问题,有时候光看代码很难查找,通过代码分析工具也很难找出,只有通过专业的内存检测工具,才能发现这类bug。

Bug评述

野指针的bug,特别是内存破坏的问题,有时候查起来毫无头绪,没有一点线索,让开发者感觉到很茫然和无助( Bugly上报的堆栈看不出任何问题)。可以说内存破坏bug是服务器稳定性最大的杀手,也是C/C++在开发应用方面相比于其它语言(如Java, C#)的最大劣势之一。

3. 数组越界

代码示例

1

2

int arr[10];

arr[10] = 1; //数组越界,有可能不会马上Crash,而是破坏了别处的内存

原因分析

数组越界和野指针类似,访问了无效的地址,如果该地址不可读写,则会马上Crash(内核给进程发送段错误信号SIGSEGV),如果修改了该处的内存,造成内存破坏,那么有可能会等一段时间才在别处发生Crash。

解决方法

  1. 所有数组遍历的循环,都要加上越界判断。

  2. 用下标访问数组时,要判断是否越界。

  3. 通过代码分析工具可以发现绝大部分的数组越界问题。

Bug评述

数组越界也是一种内存破坏的bug,有时候与野指针一样也是很难查找的。

4. 整数除以零

代码示例

1

2

int a = 1;

int b = a / 0; //整数除以0,产生SIGFPE信号,导致Crash

原因分析

整数除以零总是产生SIGFPE(浮点异常,产生SIGFPE信号时并非一定要涉及浮点算术,整数运算异常也用浮点异常信号是为了保持向下兼容性)信号,默认的处理方式是终止进程,并生成core文件。

解决方法

在做整数除法时,要判断被除数是否为0的情况。

Bug评述

整数被0除的bug很容易被开发者忽视,因为通常被除数为0的情况在开发环境下很难出现,但是到了生产环境,庞大的用户量和复杂的用户输入,就很容易导致被除数为0的情况出现了。

5. 格式化输出参数错误

代码示例

1

2

3

//格式化参数错误,可能会导致非法的内存访问,从而造成宕机

char text[200];

snprintf(text,200,"Valid %u, Invalid %u %s", 1);//format格式不匹配

原因分析

格式化参数错误也和野指针类似,但是只会读取无效地址的内存,而不会造成内存破坏,因此其结果是要么打印出错乱的数据,要么访问了无读写权限的内存(收到段错误信号SIGSEGV)而立即宕机。

解决方法

  1. 在书写输出格式和参数时,要做到参数个数和类型都要与输出格式一致。

  2. 在GCC的编译选项中加入-wformat,让GCC在编译时检测出此类错误。

6、缓冲区溢出

代码示例

1

2

3

4

5

6

char szBuffer[10];

//由于函数栈是从高地址往低地址创建,而sprintf是从低地址往高地址打印字符,

//如果超出了缓冲区的大小,函数的栈帧会被破坏,在函数返回时会跳转到未知的地址上,

//基本上都会造成访问异常,从而产生SIGABRT或SIGSEGV,造成Crash

 

sprintf(szBuffer, "Stack Buffer Overrun!111111111111111"  "111111111111111111111");

原因分析

通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏函数调用的堆栈,修改函数调用的返回地址。如果不是黑客故意攻击,那么最终函数调用很可能会跳转到无法读写的内存区域,产生段错误信号SIGSEGV或SIGABRT,造成程序崩溃,并生成core文件。

解决方法

  1. 检查所有容易产生漏洞的库调用,比如sprintf,strcpy等,它们都没有检查输入参数的长度。

  2. 使用带有长度检查的库调用,如用snprintf来代替sprintf,或者自己在sprintf上封装一个带长度检查的函数。

  3. 在GCC编译时,在-O1以上的优化行为下,使用-D_FORTIFY_SOURCE=level进行编译(其中level=1或2,level代表的是检测级别的不同,数值越大越严格)。这样GCC会在编译时报告缓冲区溢出的错误。

  4. 在GCC编译时加上-fstack-protector或-fstack-protector-all选项,使得堆栈保护(stack-smashingprotector, SSP)功能生效。该功能会在编译后的汇编代码中插入堆栈检测的代码,并在运行时能够检测到栈破坏并输出报告。

Bug评述

缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。黑客在进行攻击时,输入的字符串一般不会让程序崩溃,而是修改函数的返回地址,使程序跳转到别的地方,转而执行黑客安排好的指令,以达到攻击的目的。
缓冲区溢出后,调试生成的core,可以看见调用栈是混乱的,因为函数的返回地址已经被修改到随机的地址上去了。
服务器宕机后,如果core文件和可执行文件是匹配的,但是调用栈是错乱的,那么很大的可能性是发生了缓冲区溢出。

7、主动抛出异常

代码示例

1

2

3

4

  if ((*env)->ExceptionOccurred(env) != 0) {

         //动态库在内部运行出现错误时,大都会主动abort,终止运行

         abort(); //给当前进程发送信号SIGABRT

  }

解决方法

查看堆栈找出abort的原因

Bug评述

如果是程序主动abort的,通过堆栈加源码还是很好定位的,但往往abort的位置是在系统库中,就不好定位了,需要多查看系统API的使用方法,检查是否使用不当。

四、小编有话说

Java异常已经搞得大家焦头烂额了,Native异常更是恐怖,数量比Java异常多得多,只是看堆栈还不好定位(画小圈圈诅咒万恶的指针)。非常感谢王竞原童鞋能在日常开发遇到的崩溃中总结出这一篇宝贵的文章!

转载自:http://bugly.qq.com/blog/?p=131  作者:王竞原

One thought on “常见Android Native崩溃及错误原因


  • 2019-12-05 17:01:36

    Vue 结合 Axios 接口超时统一处理

    当网路慢的时候。又或者公司服务器不在内地的时候,接口数据请求不回来超时报错的情况相信大家肯定遇到过的,这里我把我公司项目请求超时的处理方法分享下,希望看过后有帮助。

  • 2019-12-05 17:13:40

    JS模板工具lodash.template的简单用法

    lodash是从underscore分支的一个项目,之前我写了一篇JS模板工具underscore.template的简单用法,lodash跟underscore很相似,这也简单介绍一下lodash的template方法。 先把underscore的文章中用过的代码贴过来,把underscore的js文件换成lodash的js,其他一字不改,然后我们试试:

  • 2019-12-06 10:47:29

    date-fns日期工具的使用方法详解

    isToday() 判断传入日期是否为今天 isYesterday() 判断传入日期是否为昨天 isTomorrow() 判断传入日期是否为 format() 日期格式化 addDays() 获得当前日期之后的日期 addHours() 获得当前时间n小时之后的时间点 addMinutes() 获得当前时间n分钟之后的时间 addMonths() 获得当前月之后n个月的月份 subDays() 获得当前时间之前n天的时间 subHours() 获得当前时间之前n小时的时间 subMinutes() 获得当前时间之前n分钟的时间 subMonths() 获得当前时间之前n个月的时间 differenceInYears() 获得两个时间相差的年份 differenceInWeeks() 获得两个时间相差的周数 differenceInDays() 获得两个时间相差的天数 differenceInHours() 获得两个时间相差的小时数 differenceInMinutes() 获得两个时间相差的分钟数

  • 2019-12-06 10:49:39

    npm 查看源 换源

    npm,cnpm,查看源,切换源,npm config set registry https://registry.npmjs.org

  • 2019-12-06 11:01:31

    npm发布包流程详解 有demo

    npm发布包步骤,以及踩过的坑(见红颜色标准): 1.注册npm账号,并完成Email认证(否则最后一步提交会报Email错误) 2.npm添加用户或登陆:npm adduser 或 npm login

  • 2019-12-06 13:16:18

    vue mixins组件复用的几种方式

    最近在做项目的时候,研究了mixins,此功能有妙处。用的时候有这样一个场景,页面的风格不同,但是执行的方法,和需要的数据非常的相似。我们是否要写两种组件呢?还是保留一个并且然后另个一并兼容另一个呢? 不管以上那种方式都不是很合理,因为组件写成2个,不仅麻烦而且维护麻烦;第二种虽然做了兼容但是页面逻辑造成混乱,必然不清晰;有没有好的方法,有那就是用vue的混合插件mixins。混合在Vue是为了提出相似的数据和功能,使代码易懂,简单、清晰。

  • 2019-12-06 13:26:30

    vue的mixins混入合并规则

    混入minxins:分发vue组件中可复用功能的灵活方式。混入对象可以包含任意组件选项。组件使用混入对象时,所有混入对象的选项将混入该组件本身的选项。

  • 2019-12-06 16:50:34

    Intellij idea 如何关闭无用的提示

    Linux:Settings —> Editor —> Inspections —> General —> Duplicated Code Mac:Preferences --> Editor —> Inspections —> General —> Duplicated Code fragment 将对应的勾去掉。