JE触发原理及案例分析

目录

什么是Crash?

Java Crash

Native Crash

系统的Crash 处理过程

Java Crash 产生过程​编辑

发生Crash 时系统都做了什么?

AMS.handleApplicationCrash()

AMS.findAppProcess()

AMS.handleApplicationCrashInner()

AppErrors.crashApplication()

AppErrors.makeAppCrashingLocked()

ProcessRecord.startAppProblemLocked()

ProcessRecord.stopFreezingActivities()

AppErrors.handleAppCrashLocked()

WMS.stopFreezingScreenLocked()

AMS.UiHandler

总结

Crash 的基本原理

Crash 分析思路

Java Crash监控

Native crash异常处理

案例分析

预防措施

常见Android Native 崩溃及错误原因

什么是错误信号:

常见的崩溃:

空指针

野指针

数组越界

整数除以0

格式化输出参数错误

缓冲区溢出


  1. 什么是Crash?

App Crash全称Application crash, 对于Crash可分为Java Crash和Native Crash。

在应用启动及运行中,出现闪退(崩溃),屏幕提示当前程序停止运行的弹窗,类似于windows的应用程序崩溃。

小结:Crash是由于代码异常而导致App非正常退出现象,也就是我们常说的崩溃

为什么会出现崩溃?

概括来讲,就是程序运行中有未捕获的异常,未被 try-catch,导致进程被杀

  1. Java Crash

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

  1. Native Crash

No.&NameReasonResolutionComment
空指针试图对空指针进行操作时(如读取空指针指向的内存),处理器就会产生一个异常在使用指针前加以判断,如果为空,则是不可访问的。空指针目前是糯米app最多的一种引起crash的原因,但是它也很容易被发现和修复。
野指针指向的是一个无效的地址,该地址如果是不可读不可写的,那么会马上Crash;如果访问的地址为可写,而且通过野指针修改了该处的内存,那么很有可能会等一段时间(其它的代码使用了该处的内存后)才发生Crash。在指针变量定义时,一定要初始化,特别是在结构体或类中的成员指针变量。

在释放了指针指向的内存后,如该指针不再用应置为NULL

看代码很难查找,通过代码分析工具也很难找出,只有通过专业的内存检测工具,才能发现这类bug。
数组越界访问无效的地址。如果该地址不可读写,则会马上Crash;如果修改了该处的内存,造成内存破坏,那么有可能会等一段时间才在别处发生Crash。所有数组遍历的循环,都要加上越界判断。

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

通过代码分析工具可以发现绝大部分的数组越界问题。
破坏内存的bug,很难查找
整数除以零整数除以零默认的处理方式是终止进程在做整数除法时,要判断被除数是否为0的情况。改情况在开发环境下很难出现,但庞大的用户量和复杂的用户输入,就很容易导致被除数为0的情况出现。
格式化输出参数错误与野指针类似,但是只会读取无效地址的内存,而不会造成内存破坏。其结果是要么打印出错乱的数据,要么访问了无读写权限的内存而立即宕机。在书写输出格式和参数时,要做到参数个数和类型都要与输出格式一致
缓冲区溢出通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏函数调用的堆栈,修改函数调用的返回地址。如果不是黑客故意攻击,那么最终函数调用很可能会跳转到无法读写的内存区域,造成程序崩溃。检查所有容易产生漏洞的库调用,比如sprintf,strcpy等,它们都没有检查输入参数的长度。

使用带有长度检查的库调用,如用snprintf来代替sprintf,或者自己在sprintf上封装一个带长度检查的函数。
内存管理错误可用内存过低,app所需的内存超过设备的限制,app跑不起来导致App crash。

内存泄露,程序运行的时间越长,所占用的内存越大,最终用尽全部内存,导致整个系统崩溃。
imageview,图片占据太多内存,糯米app狂刷列表。
  1. 系统的Crash 处理过程

  1. Java Crash 产生过程

上图是Android App启动时序图,我们想要探知的Crash秘密就存在于ZygoteInit.zygoteInit()函数中。

它的内部实现:

    public static final Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
       //重定向的System.out和System.err到Android日志
        RuntimeInit.redirectLogStreams();
       // **重点关注** 用于初始化一些App运行期间需要用到的配置
        RuntimeInit.commonInit();
       // 调用Native方法进行初始化
        ZygoteInit.nativeZygoteInit();
              ...
    }

可以看到zygoteInit 是一个组装函数,下面让我们一探隐藏在内部的RuntimeInit.commonInit()函数。

protected static final void commonInit() {
        // LoggingHandler 用于组装异常信息并打印
        LoggingHandler loggingHandler = new LoggingHandler();
        // API 30 开始加入RuntimeHooks。RuntimeHooks.setUncaughtExceptionPreHandler()内部调用了Thread.setUncaughtExceptionPreHandler()
        RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
        // KillApplicationHandler 用于弹出Dialog并杀死进程
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
                ...
    }

所以当Android App发生未被捕获的异常发生时会终止线程,此时系统便会调用UncaughtExceptionHandler,告诉它被出错的线程以及对应的异常,然后便会调用uncaughtException函数.如果UncaughtExceptionHandler没有被设置,则会调用对应线程组的DefaultUncaughtExceptionHandler。

  • Thread.setUncaughtExceptionPreHandler覆盖所有线程,会在DefaultUncaughtExceptionHandler之前调用,只能在Android Framework内部调用该方法.
  • Thread.setDefaultUncaughtExceptionHandler在任意线程中设置即可作为所有线程的默认异常处理,可以在应用层调用,每次调用传入的Thread.UncaughtExceptionHandler都会替换上一次的.
  • new Thread().setUncaughtExceptionHandler()只可以处理当前线程的异常,如果有Thread设置了UncaughtExceptionHandler,则在当前线程不会再使用全局的DefaultUncaughtExceptionHandler.

LoggingHandler.Java

 private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
                 // 用于标识是否已触发处理逻辑
        public volatile boolean mTriggered = false;
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            mTriggered = true;
                  // 如果 KillApplicationHandler.uncaughtException()已经被触发了则不再继续执行
            if (mCrashing) return;
            // 判断是否为系统进程,mApplicationObject == null,一定不是普通的app进程. 但是除了system进程, 也有可能是shell进程, 即通过app_process + 命令参数 的方式创建的进程
            if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
                Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
            } else {
                StringBuilder message = new StringBuilder();
                message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
                final String processName = ActivityThread.currentProcessName();
                if (processName != null) {
                    message.append("Process: ").append(processName).append(", ");
                }
                message.append("PID: ").append(Process.myPid());
                Clog_e(TAG, message.toString(), e);
            }
        }
    }
  • 当system进程Crash的信息:

*** FATAL EXCEPTION IN SYSTEM PROCESS [线程名]为开头; 在第二行开始输出发生Crash时的调用栈信息;

  • 其他进程Crash时的信息:

FATAL EXCEPTION: [线程名] 为开头 在第二行输出 Process: [进程名], PID: [进程id]; 第三行开始输出发生Crash时的调用栈信息;

KillApplicationHandler.Java

 private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
        // 内部持有LoggingHandler用于输出异常信息。
        private final LoggingHandler mLoggingHandler;
        public KillApplicationHandler(LoggingHandler loggingHandler) {
            this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
        }
        
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
            // 触发异常信息记录
                ensureLogging(t, e);
                //避免在处理崩溃信息时发生异常导致无限重入
                if (mCrashing) return;
                mCrashing = true;
                                //进行意外停止分析
                if (ActivityThread.currentActivityThread() != null) {
                    ActivityThread.currentActivityThread().stopProfiling();
                }
                // 在此处弹出弹窗并杀死所在进程 **核心 见小节1.2** 
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } catch (Throwable t2) {
              ...
            } finally {
                // 通过finally语句块保证能执行并彻底杀掉Crash进程。当Crash进程被杀后,并没有完全结束,还有Binder死亡通知的流程还没有处理完成。
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
        }
        private void ensureLogging(Thread t, Throwable e) {
           // 调用mLoggingHandler组装并打印异常信息
        }
    }

Android Crash的特点:

  • 一般情况下程序出错时会弹出提示框;

  • 程序所在进程被杀死,JVM虚拟机退出;

  • 系统提供了捕获Crash的接口;

  • 由Java 层代码引发的Java Crash 较容易捕获分析;

  • 有C++ 层代码引发的Native Crash 一般的工具不能将其捕获;

  1. 发生Crash 时系统都做了什么?

在面,其中在KillApplicationHandler .uncaughtException()函数中我们看到了ActivityManager.getService().handleApplicationCrash()这样一个函数,其实这才是Android系统在发生Crash时所做事情的核心入口。

处理Crash的流程图如下:

ActivityManager.getService()返回的是ActivityManagerProxy实例(简称AMP),AMP经过binder调用最终交给ActivityManagerService(简称AMS)中相应的方法去处理,故接下来调用的是AMS.handleApplicationCrash()。

  1. AMS.handleApplicationCrash()
     public void handleApplicationCrash(IBinder app,
                ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
            // 获取进程对象,见1.2.2小节
            ProcessRecord r = findAppProcess(app, "Crash");
            final String processName = app == null ? "system_server"
                    : (r == null ? "unknown" : r.processName);
            // 见1.2.3小节        
            handleApplicationCrashInner("crash", r, processName, crashInfo);
        }

    关于进程名(processName):

  • 当远程IBinder对象为空时,则进程名为system_server;

  • 当远程IBinder对象不为空,且ProcessRecord为空时,则进程名为unknown;

  • 当远程IBinder对象不为空,且ProcessRecord不为空时,则进程名为ProcessRecord对象中相应进程名。

  1. AMS.findAppProcess()
        private ProcessRecord findAppProcess(IBinder app, String reason) {
            if (app == null) {
                return null;
            }
    
            synchronized (this) {
                return 找到app对应的进程信息,如果没找到会返回Null;
            }
        }
  2. AMS.handleApplicationCrashInner()
      void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
                ApplicationErrorReport.CrashInfo crashInfo) {
                     //将Crash信息写入到 Event log
                EventLog.writeEvent(EventLogTags.AM_CRASH,...);
                //将错误信息添加到 DropBox
                addErrorToDropBox(eventType, r, processName, null, null, null, null, null, crashInfo);
                // 见1.2.4
                mAppErrors.crashApplication(r, crashInfo);
        }

    addErrorToDropBox是将Crash的信息输出到目录/data/system/dropbox。例如system_server的dropbox文件名为system_server_crash@时间戳.txt

  1. AppErrors.crashApplication()
       void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
                   //清除远程调用者uid和pid信息,并保存到origId
                   final int callingPid = Binder.getCallingPid();
            final int callingUid = Binder.getCallingUid();
            final long origId = Binder.clearCallingIdentity();
            try {
                    if(存在ActivityController,比如monkey){
                            // 调用monkey的appCrashed
                            retutn;
                    }
                      // 见1.2.5
                    if (!makeAppCrashingLocked()) {
                        Binder.restoreCallingIdentity(origId);
                        return;
                    }
                    ...
                    //发送消息SHOW_ERROR_MSG,弹出提示crash的对话框,等待用户选择【见小节10】
                    mUiHandler.sendMessage(msg);
                    //进入阻塞等待,直到用户选择crash对话框
                        int res = result.get();
            } finally {
                    //恢复远程调用者uid和pid
                Binder.restoreCallingIdentity(origId);
            }
        }

    此方法主要做的两件事:

    1. 调用makeAppCrashingLocked,继续处理Crash流程;

    2. 发送消息,弹出提示Crash的对话框,等待用户选择;

  2. AppErrors.makeAppCrashingLocked()
        private boolean makeAppCrashingLocked(ProcessRecord app,
                String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) {
            app.setCrashing(true);
            //封装crash信息到crashingReport对象
            app.crashingReport = generateProcessError(app,
                    ActivityManager.ProcessErrorStateInfo.CRASHED, null, shortMsg, longMsg, stackTrace);
            //  见1.2.6
            app.startAppProblemLocked();
            // 停止屏幕冻结 见1.2.7 
            app.getWindowProcessController().stopFreezingActivities();
            // 见1.2.8 
            return handleAppCrashLocked(app, "force-crash" /*reason*/, shortMsg, longMsg, stackTrace,
                    data);
        }
  3. ProcessRecord.startAppProblemLocked()
     void startAppProblemLocked() {
           ComponentName errorReportReceiver = null;
            for (int userId : mService.mUserController.getCurrentProfileIds()) {
                if (this.userId == userId) {
                          // 获取获取当前用户下的Crash应用的 ErrorReceiver(需要在系统的设置中的错误报告功能开启时才会有值)
                    errorReportReceiver = ApplicationErrorReport.getErrorReportReceiver(
                            mService.mContext, info.packageName, info.flags);
                }
            }
            //调用AMS忽略当前app的广播接收
            mService.skipCurrentReceiverLocked(this);
        }

    此方法主要做的两件事:

    1. 如果开启了错误报告功能,获取广播接收器的组件名称;

    2. 忽略发生Crash应用的所有广播接收;

  4. ProcessRecord.stopFreezingActivities()
public void stopFreezingActivities(boolean force) {
  ...
  //其中activities类型为ArrayList<ActivityRecord>,停止进程里所有的Activity
  int i = activities.size();
    while (i > 0) {
        i--;
        activities.get(i).stopFreezingScreenLocked(true); 
    }
}

public void stopFreezingScreenLocked(boolean force) {
        // appToken是WindowManager的token。 见1.7.9
        WMS.stopAppFreezingScreen(appToken, force);
}
  1. AppErrors.handleAppCrashLocked()
boolean handleAppCrashLocked(ProcessRecord app, String reason, String shortMsg, String longMsg, String stackTrace) {
    if(同一进程在1分钟内连续两次crash){
            if(不是persistent进程){
                    // ActivityStackSupervisor 简称ASS
                    ASS.handleAppCrashLocked, 直接结束该应用所有activity
                    AMS.removeProcessLocked,杀死该进程以及同一个进程组下的所有进程
            }
            ASS.resumeTopActivitiesLocked,恢复栈顶第一个非finishing状态的activity
    }else{
            ASS.finishTopRunningActivityLocked,执行结束栈顶正在运行activity
    }
}
  1. WMS.stopFreezingScreenLocked()
    public void stopFreezingScreen() {
        if (权限检查) {
            throw new SecurityException("Requires FREEZE_SCREEN permission");
        }
     
        synchronized(mWindowMap) {
                 1. 处理屏幕旋转相关逻辑;
                     2. 移除冻屏的超时消息;
                     3. 屏幕旋转动画的相关操作;
                     4. 使能输入事件分发功能;
                     5. display冻结时,执行gc操作;
                     6. 更新当前的屏幕方向;
                     7. 发送configuraion改变的消息。
        }
    }
  2. AMS.UiHandler
    final class UiHandler extends Handler {
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SHOW_ERROR_MSG: 
                       1. 创建提示crash对话框,等待用户选择,5分钟操作等待。
                       2. 阻塞等待用户选择,当用户不做任何选择5分钟超时后,默认选择“确定”,当手机休眠时也默认选择“确定”
                     break;
            ...
        }
    }

  3. 总结

  • 首先发生Crash所在进程,在创建之初便准备好了DefaultUncaughtHandler,用来来处理Uncaught Exception,并输出当前Crash基本信息;

  • 调用当前进程中的AMP.handleApplicationCrash;经过binder ipc机制,传递到system_server进程;

  • 接下来,进入system_server进程,调用binder服务端执行AMS.handleApplicationCrash;

  • 从mProcessNames查找到目标进程的ProcessRecord对象;并将进程crash信息输出到目录/data/system/dropbox;

  • 执行makeAppCrashingLocked

  • 执行handleAppCrashLocked方法,

  • 通过UiHandler发送消息SHOW_ERROR_MSG,弹出Crash对话框;

  • system_server进程执行完成。回到Crash进程开始执行杀掉当前进程的操作;

  • 当Crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked();

  • 最后,执行清理应用相关的activity/service/ContentProvider/receiver组件信息。

  1. Crash 的基本原理

  • Android 应用运行在 Dalvik/ART 虚拟机中,当应用中出现未处理的异常时,虚拟机会终止应用的进程,导致应用崩溃。通常, Android Crash 可以分为以下几种类型:

  • 空指针异常(NullPointerException) :当代码尝试访问一个空对象引用时,会抛出该异常。

  • 数组越界异常(ArrayIndexOutOfBoundsException) :当代码尝试访问超过数组边界范围的元素时,会抛出该异常。

  • 类转换异常(ClassCastException) :当代码尝试将一个对象转换成不兼容的类型时,会抛出该异常。

  • 内存泄漏(Memory Leaks) :当应用程序持有对某个对象的引用,但该对象已经不再需要并且无法被释放回收时,就发生了内存泄漏。

  • 资源未释放(Resource Not Released) :在使用一些需要手动管理资源(如数据库连接、文件流等)的情况下,如果没有正确释放这些资源,在长时间运行后可能导致系统资源耗尽并触发崩溃

  1. Crash 分析思路

  2. 收集 Crash 信息

    要分析 Crash,首先需要收集相关的信息。我们可以通过以下途径获取 Crash 信息:

    • Android Studio 的 Logcat

    • 第三方 Crash 收集工具(如: Firebase Crashlytics, Bugly 等)

    • 操作系统提供的日志工具(如: adb logcat)

2.分析 Crash 日志

分析 Crash 日志是定位问题的关键。我们需要关注以下几个方面的信息:

• 异常类型: Java 层异常、 Native 层异常或 ANR

• 异常原因:例如空指针异常、数组越界等

• 堆栈信息:从堆栈信息中,我们可以定位到具体的代码行数

3.重现崩溃

为了更好地理解问题,我们需要尝试重现崩溃。这可以帮助我们确定崩溃发生的条件和场

景。重现崩溃还可以帮助我们验证解决方案是否有效。

4.调试和测试

在确定崩溃原因后,我们需要进行调试和测试。这可以帮助我们验证解决方案是否有效,

并确保它不会引入新的问题。一般 crash 的问题比较明显,容易找到问题。

  1. Java Crash监控
  • 如何收集 Java_Crash 日志

自定义java异常处理,记录当前设备的基础信息,及线程错误信息存储到本地文件中。在需要的时候上传到服务器

CrashHandler

internal object CrashHandler {
    var CRASH_DIR = "crash_dir"
    fun init(crashDir: String) {
        Thread.setDefaultUncaughtExceptionHandler(CaughtExceptionHandler())
        this.CRASH_DIR = crashDir
    }

    private class CaughtExceptionHandler : Thread.UncaughtExceptionHandler {
        private val context = AppGlobals.get()!!
        private val formatter = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA)
        private val LAUNCH_TIME = formatter.format(Date())
        private val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        override fun uncaughtException(t: Thread, e: Throwable) {
            if (!handleException(e) && defaultExceptionHandler != null) {
                defaultExceptionHandler.uncaughtException(t, e)
            }
            restartApp()
        }

        private fun restartApp() {
            val intent: Intent? =
                context.packageManager?.getLaunchIntentForPackage(context.packageName)
            intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
            context.startActivity(intent)

            Process.killProcess(Process.myPid())
            exitProcess(10)
        }


        private fun handleException(e: Throwable?): Boolean {
            if (e == null) return false
            val log = collectDeviceInfo(e)
            if (BuildConfig.DEBUG) {
                HiLog.e(log)
            }

            saveCrashInfo2File(log)
            return true
        }

        private fun saveCrashInfo2File(log: String) {
            val crashDir = File(CRASH_DIR)
            if (!crashDir.exists()) {
                crashDir.mkdirs()
            }
            val crashFile = File(crashDir, formatter.format(Date()) + "-crash.txt")
            crashFile.createNewFile()
            val fos = FileOutputStream(crashFile)

            try {
                fos.write(log.toByteArray())
                fos.flush()
            } catch (ex: Exception) {
                ex.printStackTrace()
            } finally {
                fos.close()
            }
        }


        /**
         * 设备类型、OS本版、线程名、前后台、使用时长、App版本、升级渠道

        CPU架构、内存信息、存储信息、permission权限
         */
        private fun collectDeviceInfo(e: Throwable): String {
            val sb = StringBuilder()
            sb.append("brand=${Build.BRAND}\n")// huawei,xiaomi
            sb.append("rom=${Build.MODEL}\n") //sm-G9550
            sb.append("os=${Build.VERSION.RELEASE}\n")//9.0
            sb.append("sdk=${Build.VERSION.SDK_INT}\n")//28
            sb.append("launch_time=${LAUNCH_TIME}\n")//启动APP的时间
            sb.append("crash_time=${formatter.format(Date())}\n")//crash发生的时间
            sb.append("forground=${ActivityManager.instance.front}\n")//应用处于前后台
            sb.append("thread=${Thread.currentThread().name}\n")//异常线程名
            sb.append("cpu_arch=${Build.CPU_ABI}\n")//armv7 armv8

            //app 信息
            val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
            sb.append("version_code=${packageInfo.versionCode}\n")
            sb.append("version_name=${packageInfo.versionName}\n")
            sb.append("package_name=${packageInfo.packageName}\n")
            sb.append("requested_permission=${Arrays.toString(packageInfo.requestedPermissions)}\n")//已申请到那些权限


            //统计一波 存储空间的信息,
            val memInfo = android.app.ActivityManager.MemoryInfo()
            val ams =
                context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
            ams.getMemoryInfo(memInfo)

            sb.append("availMem=${Formatter.formatFileSize(context, memInfo.availMem)}\n")//可用内存
            sb.append("totalMem=${Formatter.formatFileSize(context, memInfo.totalMem)}\n")//设备总内存

            val file = Environment.getExternalStorageDirectory()
            val statFs = StatFs(file.path)
            val availableSize = statFs.availableBlocks * statFs.blockSize
            sb.append(
                "availStorage=${Formatter.formatFileSize(
                    context,
                    availableSize.toLong()
                )}\n"
            )//存储空间


            val write: Writer = StringWriter()
            val printWriter = PrintWriter(write)
            e.printStackTrace(printWriter)
            var cause = e.cause
            while (cause != null) {
                cause.printStackTrace(printWriter)
                cause = cause.cause
            }

            printWriter.close()
            sb.append(write.toString())
            return sb.toString()
        }
    }

    fun crashFiles(): Array<File> {
        return File(
            AppGlobals.get()?.cacheDir,
            CRASH_DIR
        ).listFiles()
    }
}
  • 重启应用

注意事项:如果进入应用就崩溃,就会进入重复重启的死循环。所以在做重启时候,需要做一些判断:例如1分钟内系统出现两次 crash 就不再重启了,否则这个应用被卸载无疑

  1. Native crash异常处理

native_crash,就是 native 曾发生了 crash。内部是通过一个 NativeCrashListener 线程去监控的。

SystemServer --> ActivityManagerService --> startObservingNativeCrashes();

直接引入第三方库来处理 native 异常

class ActivityManagerService{
    public void startObservingNativeCrashes() {
        final NativeCrashListener ncl = new NativeCrashListener(this);
        ncl.start();
    }
}
  1. 案例分析

空指针异常

09-22 07:16:41.848 +0000 17248 17248 D AndroidRuntime: Shutting down VM
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: *** FATAL EXCEPTION INSYSTEM PROCESS: main
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: java.lang.NullPointerException: Attempt to read from field 'java.lang.String com.android.server.pm.Settings$VersionInfo.fingerprint' on a null object reference
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.pm.PackageManagerService.<init>(PackageManagerService.java:7613)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.pm.PackageManagerService.main(PackageManagerService.java:7121)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.SystemServer.startBootstrapServices(SystemServer.java:1166)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.SystemServer.run(SystemServer.java:880)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.SystemServer.main(SystemServer.java:613)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:987)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: Error reporting crash
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'void android.app.IActivityManager.handleApplicationCrash(android.os.IBinder,android.app.ApplicationErrorReport$ParcelableCrashInfo)' on a null object reference
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at com.android.internal.os.RuntimeInit$KillApplicationHandler.uncaughtException(RuntimeInit.java:156)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1073)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at java.lang.Thread.dispatchUncaughtException(Thread.java:2200)

分析过程:

(1) Settings$VersionInfo.fingerprint 为空,找到出现问题的代码:

(2)自己对源码进行分析,发现一个方法,该方法能创建或者找到版本的信息,如果为空则添加到集合中。而报错的地方,是调用 getInternalVersion(),没有做判空的处理。

(3)继续跟踪代码, mVersion 是一个集合,通过键值对来获取那个值

(4)UUID_PRIVATE_INTERNAL 是 null

(5)猜想空指针异常可能是没有调用 findOrCreateVersion(),没有创建对应的值并且没有put 到集合中。并且打印了正常能开机的 mVersion 的大小,都是 2。

修改方案如下:

  1. 预防措施

为了减少应用程序崩溃的风险,可以采取以下预防措施:

  • 良好的异常处理:在关键代码块中使用 try-catch 语句来捕获和处理可能抛出的异常。这样可以避免应用程序因为未处理的异常而崩溃,并提供更友好的用户体验。

  • 合理地使用空值检查:确保在访问对象之前进行适当的空值检查,以避免触发空指针异常。

  • 正确释放资源:对于需要手动管理资源(如数据库连接、文件流等),始终确保及时释放这些资源,以避免资源泄漏和系统负载过高导致崩溃。

  • 谨慎使用第三方库:选择可靠且经过广泛测试的第三方库,并仔细阅读其文档和源代码。不推荐使用已知存在问题或长期无更新支持的库。

  • 定期进行性能优化和内存管理:通过减少内存占用、优化算法、限制并发线程数等方法,改善应用程序性能并减少崩溃风险。

  • 多渠道测试与发布前验证:在发布前进行全面测试,并覆盖各种设备、操作系统版本和网络条件下运行应用程序的场景。这有助于发现并修复潜在的崩溃问题。

  1. 常见Android Native 崩溃及错误原因

  1. 什么是错误信号:

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

信号通常的来源:

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

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

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

  1. 常见的崩溃:

  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")

  7. 原因分析: 通过往程序的缓冲区写超出其长度的内容,造成缓冲区溢出,函数的堆栈被破坏,修改函数调用的返回地址,在函数返回时会跳转到未知的地址上。如果不是黑客故意攻击,那么最终函数调用很可能会跳转到无法读写的内存区域,产生段错误信号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文件和可执行文件是匹配的,但是调用栈时错乱的,那么很大的可能性是发生了缓冲区溢出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值