资讯详情

Android 10开机前状态重置:启动落点、misc控制块与SELinux排错

📅 2026/10/31 9:54:52 | 华诺云谱 👁 阅读
Android 10开机前状态重置:启动落点、misc控制块与SELinux排错
开机前重置一些状态这八个字第一次看到的时候我以为是半小时能收工的活在 init.rc 里加两行删除命令重启验证提交。真动起手来才发现卡住整个事情的从来不是命令怎么写而是那几行代码究竟跑在启动链路的哪一格、跑的时候目标路径到底存不存在、跑完之后 SELinux 认不认这个新上下文。写在 early-init 里文件系统还没挂载想清 persist 属性属性服务在更早的时候就把旧值读进了内存想清某个应用的数据又撞上文件级加密用户没解锁之前那个目录根本不可见。这篇文章就围绕 sp 平台项目标题里的 sprd10.0 是团队内部沿用的版本串写法下文统称 sp 平台具体厂商和型号不展开Android 10 基线下的开机前重置状态这件事把启动窗口选择、状态分类、分区控制块写入、init.rc 落地、幂等设计和排错链路完整拆一遍。适合正在做定制固件、产线模式、返修整机处理或者升级兼容适配的同行看也适合刚接手系统层、对 init 流程还比较陌生的同学按图索骥。1. 先确定四个关键点命令该落在启动链路的哪一格1.1 从 Preloader 到 post-fs-data每一格能碰到什么Android 的启动不是一条直线而是一串接力。粗粒度地拆会经过 Preloader、二级引导常见是 U-Boot 或 LK、Kernel、init 第一阶段、init 第二阶段这几棒。每一棒手里能操作的东西完全不同这也是重置状态这件事最容易翻车的地方——不是逻辑错是放错了棒次。Preloader 跑在 DRAM 刚初始化完的时候能干的只有最基础的硬件初始化和加载下一级引导这一层基本不适合做任何业务性质的清理。二级引导能读分区、能改分区内容misc 分区里的控制块通常就是在这一层被解析的所以想通过写一条指令让下次开机进恢复模式执行清除这条路落点就在这里。Kernel 起来之后init 作为第一个用户态进程启动它按 trigger 顺序推进early-init→init→late-init而late-init里又会依次触发early-fs→fs→post-fs→late-fs→post-fs-data→zygote-start→early-boot→boot。这个顺序不是随便排的它精确对应文件系统挂到哪一步了。early-init阶段/data还没挂上你在里面写rm /data/xxx只会得到一个文件不存在的静默失败等到post-fs-data/data可用了但属性服务早在 init 启动初期就已经把persist.*的值从磁盘读进共享内存了这时候你再去删属性文件本次开机是无效的。这里有个很实用的判断办法在设备的 init.rc 里搜mount_all出现的位置把自己要执行的命令放在它之后的第一个 trigger 里。mount_all在哪文件系统就在哪可用这比死记阶段名字靠谱得多。1.2 判断你自己的设备该放在哪一格一个三步的判定法我一般用三个问题来定落点顺序不能反。第一个问题要清的东西是分区里的字节还是文件系统里的文件如果是分区字节比如某块保留区里的标志位那落点在二级引导或者一个能拿到块设备节点的早期阶段如果是文件系统里的文件往下走。第二个问题这个文件在不在/data下、需不需要用户解锁才能看见不需要解锁的device encrypted 那部分以及/cache、/metadata这类落在post-fs-data之前或之中都可以需要用户解锁才可见的credential encrypted 那部分也就是绝大多数应用的数据目录必须等到用户解锁之后通常是on user-unlocked或者在系统服务里做init.rc 阶段想都别想。第三个问题这次清理要不要只发生一次如果要就得引入标志位标志位本身又需要一个不会被本次清理顺手删掉的存放位置——这是个递归问题很多人就是在这里绕不出来最后把标志和状态放在了同一个目录清状态的时候把标志一起清了于是每次开机都清一遍。把这三个问题的答案写在一张纸上落点基本就唯一了。我见过的大部分返工都是跳过这三个问题直接开写。2. 分清状态的四种形态别把不可逆的东西当成可清项2.1 属性、文件、分区字段、硬件寄存器四类状态的重置代价状态这个词太笼统了实际项目里它至少包含四种形态重置代价差着数量级。形态典型例子重置方式生效时机风险等级属性persist.sys.*、自定义persist.vendor.*setprop 覆盖或删持久化文件本次或下次开机低文件目录下的标志文件、缓存、日志、小型配置rm / 重写视挂载阶段低到中分区字段misc 控制块、引导计数、槽位元数据dd 写入指定偏移下次开机中到高硬件相关校准数据、射频参数、唯一标识专用工具或产线流程需重新校准极高属性和文件这两类出问题最多是没生效可以反复试分区字段出问题可能是开不了机但通常还能救回来而校准数据、射频参数、设备唯一标识这一类一旦覆盖整机基本只能返产线重烧普通开发者手上没有对应工装是做不回来的。所以第一件事永远是分类而不是动手。我在项目里习惯先把待清项列成表逐条标注形态和风险等级风险等级为极高的项直接画红线不管需求方怎么催都不碰。2.2 一眼看上去能清、实际上碰了会出事的几类东西有几类东西特别容易被误判成普通状态这里单独点出来。第一类是设备唯一标识相关的存储。它们的名字往往很朴素看起来就是个普通的分区或文件但里面存的是整机出厂时写入的唯一值覆盖之后同一批机器会出现标识重复后续所有依赖唯一标识的功能都会出问题。第二类是传感器校准。有些方案会把校准结果放在一个可写分区里测试的时候为了方便会去动它动完之后指南针、重力感应就开始飘而现场根本判断不出是软件问题还是硬件问题。第三类是加密相关的元数据。文件级加密的密钥材料、密钥派生相关的文件看起来只是/data下的一堆目录但删错一个用户数据就永久解不开了。清理加密元数据只有一种合法姿势就是走完整的恢复出厂流程让系统自己去处理密钥轮换。第四类是引导计数和回滚保护相关的字段。有些平台用这些字段做启动失败自动回滚你把它清了等于把安全网拆了某次刷了坏镜像之后设备就再也起不来。提示任何清完之后没法靠重刷固件恢复的项都应当默认排除在自动清理流程之外需要清的时候走人工工装流程并留记录。3. misc 分区里的那块控制块开机流程真正读的第一个状态3.1 2048 字节的布局与各字段偏移misc 分区是引导流程和 Android 之间的一条便签纸。引导程序每次启动都会读它看里面有没有指令Android 侧写完指令重启引导程序读到就照做。Android 10 基线上常见的结构体是这样struct bootloader_message { char command[32]; /* 偏移 0指令比如 boot-recovery */ char status[32]; /* 偏移 32状态回写 */ char recovery[768]; /* 偏移 64传给恢复模式的参数 */ char stage[32]; /* 偏移 832 */ char reserved[1184]; /* 偏移 864 */ };整块 2048 字节。command是要做的事recovery是做的细节两者配合。想触发一次带数据清除的恢复流程需要把command写成boot-recovery把recovery写成以recovery\n开头、后面跟具体参数的多行文本。清除数据用的是--wipe_data清缓存是--wipe_cache这两个参数的组合顺序会影响恢复模式里的实际行为通常--wipe_data在前。要注意的是不同平台对这块区域的用法会有增减有的方案会额外用stage字段做槽位相关的事情有的会把控制块搬到别的位置。所以动手之前先在设备上确认块设备节点ls -l /dev/block/by-name/看有没有misc有的话记下它的实际指向。分区名在不同平台上可能叫misc、miscdata或者别的以实际列表为准。3.2 用 dd 手写一条指令并在下次开机验证确认了节点之后写入本身很简单关键是偏移和convnotrunc# 只写 command 字段不动其它区域 printf boot-recovery | dd of/dev/block/by-name/misc bs1 seek0 convnotrunc # 写 recovery 字段偏移 64 printf recovery\n--wipe_data\n--wipe_cache\n \ | dd of/dev/block/by-name/misc bs1 seek64 convnotrunc # 回读确认 dd if/dev/block/by-name/misc bs1 count128 2/dev/null | hexdump -Cconvnotrunc不能省。省掉的话 dd 会先把文件截断成零长度对整个块设备来说这意味着你后面一段数据全被清掉了这时候设备还能不能起来就只能看运气。回读那一步也别省。我在一个项目里遇到过写完立刻重启、结果什么也没发生的情况回读才发现写进去的是boot-recover——少了一个y因为当时用的是printf但字符串被 shell 变量截断了。这种低级错误靠肉眼核对代码是看不出来的只能回读比对。写完之后触发重启引导程序读到boot-recovery就会进恢复模式并执行清除。清除完成后恢复模式会把command字段清掉避免下次开机又进一遍。如果清除完command还在那就是恢复流程没有正常走到收尾这时候开机就会陷入每次都进恢复模式的循环需要手动把command字段写成全零救回来。3.3 写完之后没生效通常是哪三处出了问题第一种写错了块设备。有些设备上/dev/block/by-name/misc是个符号链接指向/dev/block/mmcblk0pX之类如果链接本身被改过或者指向了错误的槽位你写的就不是引导读的那块。这种情况回读是正常的但行为不对非常迷惑。第二种槽位不对。A/B 方案的设备上有些元数据是按槽位分开的你当前跑在 A 槽写进去的却是 B 槽的位置重启后从 A 槽引导自然读不到。判断当前槽位可以看ro.boot.slot_suffix这个属性。第三种写对了但时机不对。misc 里的指令是在引导阶段被读的如果写入动作发生在引导已经开始之后比如你写完之后做了一次软重启某些方案下软重启不完整走引导流程指令就不会被消费。稳妥的做法是写完做一次完整的断电重启而不是reboot命令。4. init.rc 里的清理动作write、exec 与 restorecon 的正确组合4.1 early-init 能写的节点与不能动的路径early-init这个阶段最容易被误用因为它名字里带early看起来什么都能干。实际上这个时候/data、/cache都还没挂上你能操作的有效范围主要是两类一类是 proc 和 sys 下的节点另一类是那些在 fstab 里标记为 early mount 的分区。写 sys 节点是early-init最正当的用法。比如某些硬件状态需要在系统起来之前复位就往对应的 sysfs 节点写值on early-init write /sys/class/xxx/reset 1 write /sys/class/xxx/enable 0这种写法干净、无副作用也不会牵扯 SELinux 太多问题前提是节点本身的 type 允许 init 域写入。而清理/data下的文件无论怎么写都不会成功。我见过有人在early-init里写rm /data/system/xxx.xml日志里连报错都没有因为 init 的rm遇到不存在的路径是静默返回的。找这个问题的正确姿势不是看日志而是看mount的输出确认/data到底在哪个 trigger 之后才出现。4.2 exec 一个脚本怎么让 init 真的把它跑起来当清理逻辑复杂到几行 rc 写不下的时候就需要exec一个脚本。Android 10 上 init 的exec语法大致是这样on post-fs-data exec - root system -- /system/bin/sh /system/bin/reset_state.sh四个关键点逐一说。-之后的root system是用户和组--之后才是真正的命令和参数。脚本本身必须放在一个 init 能访问、并且在系统分区里的位置放在/data下是不行的因为执行时机可能早于挂载。脚本第一行要写#!/system/bin/sh权限要设成可执行。更关键的是 SELinux 上下文脚本文件自己的 type 必须是 init 域能够执行的类型否则会得到一条avc: denied { execute }然后在 logcat 里一闪而过很容易漏看。脚本内部执行的动作同样受 SELinux 约束。在 init 域里 fork 出来的 shell 进程其域可能是init本身也可能是某个shell域取决于策略配置。所以脚本里每一条对文件的写操作都要确认目标路径的 type 对当前域开放写权限。我的习惯是脚本第一版只做一件事就是把要清的目标路径挨个ls一遍并把结果打到内核日志里。先确认路径可见、权限够、上下文对再往里加真正的删除动作。这个笨办法能省下大量来回刷机的时间。4.3 文件创建后忘了 restorecon后面全是 avc denied清理完之后如果需要重建某些文件或目录比如重建一个空的状态文件一定要紧接着做restorecon。因为通过 init 或 shell 创建出来的文件默认继承的是创建者的上下文而不是该路径在策略文件里声明的预期上下文。on post-fs-data mkdir /data/vendor/mystate 0770 system system restorecon_recursive /data/vendor/mystaterestorecon_recursive是 init 内建的命令会按 file_contexts 里的规则把整个目录的上下文刷一遍。不做这一步后续任何访问这个目录的进程都会撞上avc: denied而这条拒绝日志往往在开机几万行日志的中间位置第一次排查的人基本找不到。排查 avc 拒绝有一个简单办法adb shell dmesg | grep avc或者adb logcat -b all | grep avc。dmesg 里的记录不受 logcat 缓冲区大小限制更适合开机阶段的问题。如果嫌日志太多可以临时把 SELinux 设成宽容模式setenforce 0对比一次行为行为正常了才能确认问题出在策略上而不是逻辑上。5. 让清理只发生一次幂等标志与触发条件的几种设计5.1 用版本号 标志文件做双保险只清一次这件事最朴素的做法是留个标志文件但标志文件本身也会遇到被清掉的风险。我的做法是双保险标志文件 版本号两者都要满足条件才执行清理。on post-fs-data exec - root system -- /system/bin/sh /system/bin/reset_state.sh#!/system/bin/sh FLAG/data/vendor/.state_reset_done VER_FILE/data/vendor/.state_reset_ver CUR_VER1 # 已经执行过且版本匹配直接退出 if [ -f $FLAG ] [ -f $VER_FILE ]; then LAST_VER$(cat $VER_FILE) if [ $LAST_VER $CUR_VER ]; then exit 0 fi fi # 执行清理动作 rm -rf /data/vendor/mystate/* mkdir -p /data/vendor/mystate chown system:system /data/vendor/mystate restorecon -R /data/vendor/mystate # 写入标志 echo $CUR_VER $VER_FILE touch $FLAG restorecon $VER_FILE $FLAG注意标志文件和目标状态必须放在不同目录。这条建议看起来很傻但我至少见过三次把它们放在同一个目录里、然后清理动作把标志一起删掉、于是每次开机都清一遍的案例。表现出来就是用户设置的东西每次重启都丢失而开发者在本地反复测试都复现不了因为本地的目录结构跟定制版本不一样。版本号这一层是给后续留的活口将来如果又要清一次把CUR_VER改成 2 就行不需要新增文件也不需要依赖用户手动触发。5.2 把异常重启计数作为触发条件有些场景下清一次的语义不是新版本第一次开机清而是连续异常重启 N 次之后清。这种需求在稳定性兜底里很常见某个状态被写坏导致系统起不来、无限重启希望系统能自己恢复。实现上需要在引导阶段维护一个计数每次正常启动完成后清零每次启动时加一超过阈值就触发清理。计数放在哪里是个设计点放在/data下不行因为系统起不来的时候/data可能还没挂载放在 misc 的reserved区域或者单独一块保留区比较合适但要注意兼顾前面说的风险等级评估。/* 思路示意从 misc 的保留区读计数 */ struct boot_count_meta { uint32_t magic; /* 用于识别这块区域是否已被初始化 */ uint32_t count; uint32_t reserved[2]; };magic字段很重要。首次上电时保留区大概率是全0xFF或者全0如果不做魔数校验你会把一片随机值当成计数读进来导致第一次开机就误触发。这是分区字段类操作的通病凡是从裸分区读结构化数据第一件事都是校验魔数。5.3 开关要不要留出厂固件里的应急后门设计量产出货的固件里我倾向于给清理逻辑留一个属性开关比如persist.vendor.state.reset默认为 0设为 1 的时候强制执行一次。理由很实际设备到用户手里之后出现状态相关的疑难问题现场支持人员没有工程机、也没法刷机但能通过某种受控方式改一个属性重启一次比寄回来省事得多。这个开关的设计有几条约束。属性名要足够独特不要跟已有命名撞车开关只对白名单内的清理项生效不能一个开关把所有东西都清了触发之后要自动复位成 0避免残留最重要的是对所有高风险项校准、唯一标识必须硬编码排除不能因为一个属性就放行。6. 一次完整的排错复盘改了 init.rc 但开机后状态还在6.1 现象与第一层排除某次做定制版本需求是在出厂前清掉测试阶段留下的一组状态文件。改完 init.rc编译刷机重启进系统一看——状态文件原封不动。整个改动不超过十行理论上没有任何出错空间。第一层排除做的是确认改动真的进了镜像。这一步听起来多余实际上很有必要因为定制版本的编译产物路径经常有多套一不小心刷的是旧的。验证方法很直接adb shell cat /system/etc/init/hw/init.rc | grep reset_state或者看我加的那条exec在不在。确认在。第二层排除做的是确认脚本被执行。这一步我用了一个取巧的办法在脚本第一行加一句写内核日志的动作重启后dmesg | grep reset_state看有没有打出来。结果没有。到这里问题范围就从清理逻辑不对缩小到了脚本压根没跑。6.2 用日志把执行链一环一环接回去脚本没跑可能的原因有三层rc 里的exec语法有问题、脚本文件不存在或不可执行、SELinux 拦住了执行。逐层验证。先看 rc 语法。把exec那一行从post-fs-data挪到别的 trigger 下试一次还是没跑说明不是 trigger 名字写错。再看文件adb shell ls -lZ /system/bin/reset_state.shls -lZ会带出 SELinux 上下文。这一看就发现了问题脚本文件的上下文是u:object_r:system_file:s0而我期望的是u:object_r:init_exec:s0或者该平台对应的可执行类型。system_file这个 type 对 init 域是没有执行权限的。到这里其实还没完因为还要确认脚本是怎么被编进镜像的。检查编译配置发现脚本被放进了/system/bin目录但没有在对应的文件上下文配置里声明所以默认继承了目录的上下文。6.3 定位结论与修复动作修复分两步。第一步在文件上下文的配置里给这个脚本单独声明一个 type让它跟着 init 相关域走第二步在策略里补上 init 域对该 type 的执行权限。策略文件里大致是这么几条# file_contexts 片段 /system/bin/reset_state\.sh u:object_r:state_reset_exec:s0 # 类型定义 type state_reset_exec, file_type, exec_type, vendor_file_type; # 域转换与权限 init_domain(state_reset_exec) allow init state_reset_exec:file { execute execute_no_trans };补完之后重新编译刷机脚本跑起来了但清理还是没成功——这是第三层脚本跑起来了里面的rm却因为目标路径的 type 对 init 域没有写权限而被拒。这次的日志在dmesg里找到了一条avc: denied { write }目标正是/data/vendor/mystate。最后把这条写权限也补上整套流程才通。整个过程从十行改动的简单需求变成改了三个配置文件、跨了两轮刷机这就是为什么我在第 1 节里反复强调落点和上下文要先想清楚——真正耗时间的从来不是业务逻辑而是这条链路上的每一道门。7. 出货前的那张自检表验证、灰度与回滚7.1 验证清单清理类改动最容易出现的问题是看起来生效了实际只生效了一半。我一般用下面这张表逐项过检查项验证方法关注点清理动作是否执行脚本内打日志 dmesg比对时序是否早于目标路径可用清理是否只执行一次连续重启三次观察标志文件标志与状态是否放在同目录状态是否真的消失逐项读取不能只看表面目录有些状态有内存缓存重启后是否恢复正常完整断电重启而非软重启软重启可能跳过引导阶段是否有 avc 拒绝dmesg | grep avc全量扫描开机阶段日志容易被刷掉加密场景是否正常未解锁状态下观察行为CE 存储路径在解锁前不可见高风险项是否被排除逐条核对白名单校准、唯一标识不能进自动流程其中连续重启三次这一项我建议一定要做成本很低但能抓住绝大多数幂等设计错误。很多问题在第一次开机时完全看不出来第二次开机才暴露因为第一次的开机流程和后续开机走的路径不完全一样。还有一个细节验证状态是否真的消失的时候不要只看文件在不在有些状态会被加载进共享内存文件删了但内存里的值还在表现出来就是文件没了但行为没变。这种情况需要同时确认重启后属性值和实际行为。7.2 回滚路径怎么提前埋好清理类改动一旦出问题往往表现为设备能开机但某些功能异常比完全开不了机更难定位。所以在提交这种改动之前我要求自己能答出三个问题怎么关掉它、怎么恢复被清掉的东西、怎么在不刷机的前提下验证。第一个问题的答案通常是属性开关。前面说的persist.vendor.state.reset那种设计反过来也能当总闸用设成 0 就彻底不执行清理。这个属性本身不能放在被清理的范围内否则开关自己会丢。第二个问题最容易被忽略。如果清理动作删掉的东西里有系统后续需要重建的光删不建会让系统进入一个半残状态。所以清理脚本里每删一项都要想清楚系统会不会自己重建不会的话就必须在脚本里显式重建包括目录、权限和 SELinux 上下文三件套。第三个问题的答案是日志。在脚本里保留一份轻量的执行记录写到一个不会被清掉的位置出问题时能一眼看出清理是在哪一步停住的。这份日志不需要复杂几个时间戳加几行结果就够了但它能省下的排查时间远超写它的成本。我在实际项目里踩过最深的一个坑是把清理逻辑和标志位放在了同一个/data/vendor子目录里本地测试永远是清一次就停到了产线批量刷机却变成每次开机都清最后追下来是产线流程里有一个环节会额外重建这个目录把标志一起清掉了。从那之后我给自己定了条死规矩标志位永远单独放且永远不放在任何会被清理脚本扫到的路径下。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑