1. 问题背景与aee dump机制的价值
前阵子接手一个项目,Android Q的user版本,在量产测试阶段出现了偶发重启。因为抓不到现场日志,测试端反馈了三四次都定位不了问题。后来把版本切换到userdebug,问题复现了一次,一查aee_exp目录,kernel panic信息和db信息都攒在那里,五分钟左右就锁定了方向。
这就是aee dump机制的价值。AEE(Android Exception Engine)是MTK平台的一套异常捕获引擎,在系统发生kernel panic、watchdog timeout、native crash、system_server异常等关键故障时,它会把现场的寄存器状态、内核日志、进程栈帧、内存快照打包成db文件,统一存放在/data/aee_exp目录下。有了这份现场数据,才能做后续的堆栈解析和根因定位。
但问题在于,Android Q/R之后,user版本和userdebug版本的默认配置差异很大,很多aee相关的功能开关在user版本里是被关闭的。原厂ROM一般没事,但做定制项目的朋友如果没提前打开这些开关,出了问题就会发现连aee_exp目录都是空的。这篇就把这件事讲透,包括原理、配置开关、实操命令和踩坑记录。
这篇文章适合谁看?做MTK平台驱动、系统稳定性、性能优化的工程师,尤其是手里有user版本量产项目的团队,提前把本文收藏着,出问题的时候能少走弯路。
2. 为什么user版本和userdebug版本行为完全不同
2.1 系统类型的本质差异
Android系统从构建类型上分为user、userdebug和eng三种。eng完全开放,主要用于开发调试;userdebug在user基础上开放了adb root、部分调试属性,方便开发人员验证功能;user是最终交付版本,需要满足安全性和性能要求,所以默认会把调试通道都收掉。
关键差异集中在这几个方面:
- adb root权限:userdebug可通过adb root获取root,user版本即使做了adb root,因为ro.debuggable=0的原因,shell权限仍受限。
- ro.build.type属性值不同,这个属性直接决定了系统代码在运行时的行为分支。
- 部分系统属性在user版本中被框架层屏蔽,即使setprop也会被忽略。
- SELinux策略在user版本中更严格,很多调试接口被直接deny。
对于aee来说,MTK的代码里通过Build.TYPE和几个关键property来控制aee的行为。比如在AEE相关的初始化代码中,会做类似这样的检查:
if (Build.TYPE.equals("user")) { // 简化dump内容、关闭部分触发源 } else { // 完整dump }这就是为什么同一个异常,在userdebug上能抓到完整db,在user上可能什么都不留,或者只留一条不完整的log。所谓“开启aee dump机制”,本质上要做的事情就是绕过或者覆盖掉这些基于构建类型的默认分支,让user版本也能完整地抓取异常现场。
2.2 aee在Android Q/R中的架构演变
Android Q/R里,aee的架构也发生了调整。老版本中aee主要依赖aee_core和aee_dumpstate两个进程,新版本中逐渐将部分能力整合到system_server的异常处理流程中,同时属性的命名也做了梳理。
这里要特别注意,MTK平台不同软件分支上的属性名差异挺大的。比如有的分支用ro.aee.mode,有的分支用persist.sys.aee.mode,还有的在mk文件里直接配置宏。给大家列一下Android Q/R上比较常见的属性对照:
| 属性名 | 作用 | user默认 | userdebug默认 |
|---|---|---|---|
| ro.build.type | 系统类型 | user | userdebug |
| persist.sys.aee.mode | aee运行模式 | normal或off | normal |
| persist.sys.aee_extra_enable | 扩展dump开关 | false | true |
| debug.aee.dump.enable | dump总开关 | 部分为false | true |
| persist.sys.mtk.aee.debug | aee调试级别 | 较低 | 较高 |
这个表格是经验值,不是绝对。真正做项目的时候,一定要以你自己手里的代码为准,去代码里搜属性名,看默认值怎么定义的,再对照操作。我见过有人按网上文档操作了一通,结果属性名根本对不上,白白浪费了时间。
3. 开启aee dump的完整实操步骤
3.1 检查当前配置状态
拿到一台设备,先确认基本状态,这一步很重要,不要跳过。用下面的命令把当前配置全摸一遍:
# 确认构建类型 adb shell getprop ro.build.type # 查看aee相关属性 adb shell getprop | grep -i aee # 确认aee_exp目录是否存在 adb shell ls -l /data/aee_exp # 看aee进程状态 adb shell ps -A | grep aee如果是userdebug版本,下列属性一般应该是正常的。如果某个属性缺失或者值为off,就需要手动打开。如果是user版本,很可能所有aee相关属性都处于关闭状态。
3.2 userdebug版本快速开启方式
userdebug版本支持adb root,操作起来相对简单:
adb root adb remount adb shell setprop persist.sys.aee.mode normal adb shell setprop persist.sys.aee_extra_enable true adb shell setprop debug.aee.dump.enable true adb shell setprop persist.sys.mtk.aee.debug true # 重启生效,部分属性必须重启后才能生效 adb reboot为什么有些属性必须重启后才生效?因为persist.*开头的属性会写入/data/property目录下的持久化文件,理论上设置完后下次开机自动生效。但aee的初始化进程在system_server启动阶段就会读取这些属性来决定行为模式,如果只setprop不重启,aee模块已经按旧配置初始化完成了,新值不会作用于当前运行实例。所以设置完之后务必重启,重启后再getprop确认一遍。
3.3 user版本开启方式
user版本比较麻烦。由于user版本默认ro.debuggable=0,adb root无法直接使用,所以不能简单通过setprop来修改系统属性。
常见的做法有这几种:
方式一:临时打开ro.debuggable,只适合临场调试,量产版本不要这样干。
# 需要通过fastboot解锁后刷入boot镜像 # 修改boot.img中的ro.debuggable=1 # 重新刷入后即可adb root方式二:通过uboot或kernel cmdline加入aee相关开关。MTK平台在lk(U-Boot)阶段可以读取某些cmdline参数来配置aee行为。如果你有平台源码,可以在device/mediatek/xxx/BoardConfig.mk或者lk配置里加上aee的默认开关。
方式三:最稳妥的方式,编译期直接改配置。在编译user版本时,直接把aee的默认配置改成完整模式:
# 修改的宏或mk文件配置 MTK_AEE_SUPPORT := yes MTK_AEE_MODE := normal MTK_AEE_EXTRA_ENABLE := yes然后重新编译user版本。这是量产阶段的正确做法,避免线上出问题时无法抓取现场。编译期修改的好处在于,所有配置在系统启动时就已经是完整的,不依赖运行时权限,也不存在SELinux拦截的问题。
3.4 重启后验证是否真的生效
配置完成后,重启设备,再验证一遍:
# 构建类型 adb shell getprop ro.build.type # aee属性 adb shell getprop persist.sys.aee.mode adb shell getprop persist.sys.aee_extra_enable adb shell getprop debug.aee.dump.enable # 检查aee_exp目录是否自动创建 adb shell ls -ld /data/aee_exp # 检查aee相关进程是否运行 adb shell ps -A | grep -i aee如果aee_exp目录存在,说明aee框架已经初始化。如果该目录还不存在,可能system_server还没走到创建目录的逻辑,也可能是aee进程没有正确启动。这种情况下可以触发一次测试异常来验证,下面第5章会讲具体方法。
4. 核心参数背后的设计逻辑
4.1 persist.sys.aee.mode的三种模式
persist.sys.aee.mode是aee的核心模式开关,常见的取值有off、normal、power三种。
- off:完全不启用aee,任何异常都不抓取
- normal:标准模式,异常发生时抓取基本的log和db
- power:低功耗模式,在normal基础上减少部分高频日志采集,用于功耗敏感场景
为什么这个属性设计成persist开头,而不是ro开头?因为persist属性允许运行时修改并持久化保存,而ro.*属性开机时设定一次之后就不能再改了。aee的模式在开发阶段经常需要切换,用persist设计更灵活。但在user版本中由于安全限制,即使persist属性也需要root权限才能修改,所以这个设计主要是方便开发调试用的。
还有一个容易混淆的点:有些分支上这个属性叫ro.aee.mode,含义其实是一样的。如果同时存在persist和ro两个属性,一般以生效的那个为准,需要通过实际测试确认,也不要死记硬背。
4.2 dump完整度配置决定能抓到什么
dump完整度决定了抓取的日志范围。aee在抓取db时,会执行类似dumpsys、logcat -b all、cat /proc/last_kmsg等命令,并把输出整理到db文件中。不同版本对user和userdebug定义了不同的完整度集合:
| 日志类型 | user默认 | userdebug默认 |
|---|---|---|
| last_kmsg / panic日志 | 有限 | 完整 |
| main log(logcat) | 只保留最近一段 | 全量 |
| system log | 有限 | 全量 |
| events log | 有限 | 全量 |
| dumpsys关键服务 | 仅系统核心 | 全部 |
| /data/tombstones | 不收集 | 收集 |
| 内存、CPU状态 | 不收集 | 收集 |
有一个实际案例:一台user版本设备偶发无响应,打开aee后发现db里没有dumpsys信息,只能看到kernel侧的数据。原因就是user模式下AEE的dumpstate收集逻辑被裁剪,只执行了最低限度的命令集合。这种情况下就需要针对性地提升dump level。
提升dump level的方式,在MTK平台上一般通过persist.sys.aee_extra_enable=true来打开扩展选项。打开后,aee会额外执行system_server所有关键服务的dumpsys,这不仅对问题定位有帮助,也会延长dump时间,可能从几十秒增加到几分钟。所以量产版本打开之前,需要先评估是否接受这个耗时开销。
4.3 触发源与频控模块
aee并不是所有的异常都会一视同仁地抓取。它有触发源配置和频控模块。触发源包括kernel panic、hardware watchdog、framework watchdog、native crash、java crash等,每个触发源都可以单独配置是否启用。
频控模块用来抑制反复崩溃时的重复抓取。比如一个进程崩溃后重启、再崩溃、再重启,如果没有频控,aee会把存储空间写爆。所以在开启aee dump时,要注意确认频控参数是否合理,否则会遇到dump目录不断增大、系统频繁重启的问题。
常见频控相关配置有:
- persist.sys.aee.dump.count:单位时间内的最大dump次数
- persist.sys.aee.dump.interval:两次dump之间的最小间隔(秒)
- persist.sys.aee.clean.enable:磁盘空间不足时,是否自动清理旧db
在userdebug版本中这些参数可能没有默认限制,需要自行设置。在user版本中,系统会根据构建类型自动应用一套保守的配置,这也是我们常说的“user版本默认不开全量dump”的原因之一。
5. 实操过程中遇到的坑与排查思路
5.1 aee_exp目录不存在
这是最常遇到的问题。开启aee dump后,重启完发现/data/aee_exp目录还是不存在。
排查步骤按这个顺序来:
- 确认aee相关属性已经设置成功:getprop逐个确认,别只看一个值。
- 确认aee进程是否存在:ps -A | grep aee,如果进程都没有,说明初始化就失败了。
- 查看logcat中是否有AEE相关的报错:logcat -b system | grep -i aee。
- 确认SELinux是否阻止了aee创建目录:dmesg | grep avc。
我遇到过一次情况,属性值被框架层强制覆盖。AEE的初始化逻辑在system_server启动时,会根据Build.TYPE强制把某些属性重置为安全值,导致手动setprop的值无效。这种需要阅读代码,在对应的初始化逻辑里添加白名单或修改判断条件,光靠命令行是没办法绕过去的。
还有一种情况是/data分区满了。aee在启动时会检查/data目录的可用空间,如果可用空间过小,会拒绝创建aee_exp目录。排查时顺手用df -h /data确认一下,不用绕弯路。
5.2 user版本设置属性提示Permission denied
user版本默认adb shell下没有写系统属性的权限,命令会直接报Permission denied:
adb shell setprop persist.sys.aee.mode normal # 输出: setprop: failed to set property 'persist.sys.aee.mode' to 'normal' # 或者直接Permission denied这是正常的。因为setprop写系统属性需要root或者相应的SELinux权限。解决办法就是前面说的编译期修改,或者通过临时的root调试手段。
但要注意一个细节:即使修改ro.debuggable=1,让user版本能adb root,部分属性依然受SELinux限制。MTK平台在user版本中会收紧aee相关属性的访问策略,即使拿到了root权限也未必能写入。这种情况下最彻底的办法还是重新编译boot image或者system image,把aee的默认配置直接内嵌进去。
5.3 dump文件很大导致/data空间被写满
AEE完整dump的db文件,尤其是开启了extra_enable的,单个文件可以达到几百MB甚至超过1GB。在存储空间有限的设备上,连续几次异常就会导致/data分区被写满,进而引发其他问题。
对策是这几个:
- 设置磁盘空间阈值,在aee配置中指定可用空间低于某个值时自动清理旧db
- 合理配置频控参数,避免反复抓取
- 定期导出并清理aee_exp目录,不要让日志在设备上无限堆积
我在一个项目中就吃过这个亏。开了完整dump后又遇到一个反复重启的bug,一晚上aee_exp写了8个多GB,第二天整机无法启动。后来我强行从user版本切回userdebug才把系统恢复过来,把aee_exp清掉后重新配了频控参数,才恢复正常。
5.4 dump的db文件如何解析
开启aee dump并抓到db之后,接下来是解析问题。db文件是目录形式,里面通常包含:
db ├── 00_xxx │ ├── BIN │ ├── LAST_KMSG │ ├── MAIN_LOG │ ├── SYSTEM_LOG │ ├── EVENT_LOG │ ├── DS_xxx │ └── ... ├── 01_yyy └── ...每个子目录代表一次异常。解析时的重点:
- LAST_KMSG:kernel panic或watchdog的原始信息,从里面找PC地址和调用栈
- MAIN_LOG/SYSTEM_LOG:上层日志,看异常发生前的关键打印
- DS_xxx:dumpsys输出,看系统服务状态
- 如果是native crash,一般还会有tombstone或backtrace
内核panic的问题,通常用addr2line结合vmlinux解析PC地址。用户态crash,用ndk-stack或者gdb分析。这些是另一个深挖的话题了,这里先不做展开。
6. 一些实际的工程建议
写到最后,结合我的经验给几点建议,希望对做MTK平台项目的朋友有用。
第一,user版本的aee配置一定要在项目启动阶段就评估好,而不是等到bug出现了再回头折腾。用户反馈问题是最难定位的,没有现场dump,定位成本会成倍增加。等出了问题再去加配置、重新编译、刷机,一来一回就是几天的时间。
第二,userdebug版本平时可以开完整dump,但发布前一定要检查一遍配置,确认没有把冗余的调试开关带到user版本里。aee的完整dump在真实用户环境中会带来明显的性能开销和存储压力,不是所有场景都需要开全量。
第三,开启aee后,建议做一次异常注入测试,确认确实能抓取db文件。常见的方法有这几种:
# 方法和注意事项说明 # 1. 通过sysrq触发kernel panic(需要内核配置了sysrq) adb shell "echo c > /proc/sysrq-trigger" # 2. 杀掉system_server触发watchdog adb shell "kill -9 $(pidof system_server)"注意,这两种测试方法都会导致系统重启,请在测试机上操作,不要在存有重要数据的设备上乱试。
第四,如果把aee关闭了,比如persist.sys.aee.mode=off,记得把关闭前产生的db文件导出,避免误删导致问题现场丢失。检查一次aee_exp目录,把有价值的db同步到电脑上,这是花不了几分钟但能救命的工作。
最后再说一点:MTK平台各分支的属性名、默认行为经常有差异,本文写的是Android Q/R上比较通用的实践。如果你在自己项目上操作时发现属性名对不上,建议先到代码里搜一下aee相关的属性定义,确认你所在分支的实际命名和含义,再照方抓药。
这篇FAQ就是想帮大家少走一些弯路。有问题欢迎在评论区交流,尤其是不同分支上的差异,我看到了会补充更新。