ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32MP257双核RTC踩坑:A35绕过资源锁改写时间寄存器

STM32MP257双核RTC踩坑:A35绕过资源锁改写时间寄存器 1. 现象M33 握着锁A35 却把时间改了1.1 一个能让人原地宕机的现场记录我们项目的主控是 STM32MP257一颗双核异构芯片Cortex-A35 跑 Linux负责业务逻辑和网络服务Cortex-M33 跑 FreeRTOS负责电源状态管理和 RTC 维护。上电后的分工很清晰M33 第一个拿到 RESMGR_RTC_INIT_RSC 资源锁把 RTC 初始化、校准、时间维护全部接管A35 侧如果需要读时间走 RPMsg 向 M33 发请求。这套设计在文档上看起来很完美外设所有权清晰不会有人抢。但测试跑到一半M33 串口打印的时间突然往前跳了一天。一开始我怀疑是项目管理代码里的 RTC 读取出错或者是日历换算有边界 bugreview 了好几遍都没找到问题。后来又想是不是低功耗唤醒的中断路径触发了某段意外代码在里面加了断点、加了变量跟踪依旧毫无收获。直到我随手在 A35 的 shell 里敲了一句date -s 2025-06-01 12:00:00再看 M33 的串口时间当场就跳到了这个值。那一刻我才反应过来问题根本不在 M33 怎么读时间而在 A35 居然还能写时间。顺着这个方向查下去真相慢慢浮出水面。M33 确实持有着 RESMGR_RTC_INIT_RSC但 A35 侧的 Linux RTC 驱动在写时间时根本没有经过任何与这把锁相关的检查。锁是锁在了初始化这个动作上可 A35 改动的是 RTC 的时间寄存器——两个层面从一开始就没对齐。1.2 时间被改后受牵连的不只是显示值RTC 时间跳变表面上看起来只是显示的时间不对了但在真实产品里它引发的连锁反应相当麻烦。M33 侧通常用 RTC 做低功耗唤醒唤醒时刻是提前设定好的闹钟值时间一旦被改走设备可能在错误的时间点被唤醒或者永远等不到闹钟。日志系统的时间戳会全部错乱排查问题时分不清哪条日志在前哪条在后。更敏感的是安全相关功能证书有效期校验、会话过期时间、文件时间戳这些一旦依赖了被污染的时间基准轻则业务异常重则直接拒绝服务。还有一点容易被忽略RTC 单元通常有独立的备份电源域VBAT许多产品会使用可充电的电池或超级电容维持掉电后的计时。双核系统里任何一个核心的复位、电源域切换、备份域读写都可能影响备份域的供电状态而异常写入时间寄存器相当于在电源域不稳定的时候对备份域做了写操作这比单纯改错时间更让硬件工程师头疼。一旦遇到掉电时序和 RTC 写操作同时发生寄存器内容可能就是不确定的复位后恢复出来的时间完全不可信。所以RTC 时间被异常写入不是小事它属于那种平时看着没事出问题就是大事故的共享外设。搞清楚锁的边界在哪里是解决这个问题的第一步。2. STM32MP257 的资源锁到底锁了什么2.1 资源锁是会议室预约本不是门禁闸机很多做单片机出身的人第一次接触 STM32MP2 的资源管理器时容易把它理解成一个硬件权限闸门只要我持有了某个外设的锁其他内核访问这个外设就应该被物理挡住。我一开始也是这么想的但实际查资料、看 BSP 代码之后才意识到这个理解偏差很大。RESMGRResource Manager在 STM32MP2 系列里的职责是管理外设的时钟门控、复位释放、初始化序列并通过一套资源锁协议来协调不同执行上下文Cortex-A35、Cortex-M33、安全/非安全世界对外设的所有权。这套协议解决的核心问题是谁来初始化和初始化顺序是什么它并不负责在每一个总线事务上做权限审查。用一个类比资源锁更像会议室门口的预约本谁在黑板写上已约其他人在正常流程里会先去翻一下预约本看到已被预约就不去抢会议室了。但这个机制要求所有人都遵守同一个约定如果有人根本不看预约本、直接从后门闯进会议室开会预约本拦不住他。RTC 的场景就是这样M33 遵守了预约规则在预约本上写了名字但 A35 侧的驱动压根没翻过这本预约本它直接操作了 RTC 寄存器。2.2 RESMGR_RTC_INIT_RSC 只保护初始化这个动作把 RESMGR_RTC_INIT_RSC 这个名字拆开看含义其实很直白RTC 是外设对象INIT 是资源类型RSC 是 Resource 的缩写。它要协调的是谁有资格对 RTC 做初始化级操作。初始化级操作通常包括配置 RTC 的预分频系数、选择时钟源、设置闹钟/唤醒功能、配置时间/日期格式二进制还是 BCD、校准晶振频率、使能备份寄存器访问以及设置中断/触发相关配置。这些操作直接决定 RTC 能不能正常工作、走时准不准所以双核系统里确实需要一把锁避免两个核同时去做这些配置把寄存器状态改乱。但是时间寄存器RTC_TR、RTC_DR不在这个初始化配置的范畴里。它们只是 RTC 对外暴露的当前时间窗口任何一个能访问到 RTC 地址空间的内核都可以直接写这两个寄存器。资源锁不负责拦截这种写入因为从它的设计初衷来看设置时间是一个运行时操作不属于初始化配置。问题恰恰出在这里。M33 觉得我持有初始化锁RTC 应该归我管A35 觉得我只是写了个时间没碰初始化配置。两边对RTC 归谁管的理解不一致而锁的粒度正好把初始化和时间写入切成了两个世界。2.3 从 RTC 寄存器操作流程看冲突的必然性如果你去看 STM32 系列 RTC 的编程手册会发现写时间这个操作并不像表面看起来那么单纯。它通常有一个固定流程先设置控制寄存器里的某个初始化标志让 RTC 进入可写入状态然后修改时间和日期寄存器最后退出这个状态。换句话说A35 为了改时间必须在 RTC 控制寄存器上做一次完整的状态翻转这个动作跟初始化 RTC在寄存器层面高度重合。这就带来一个很尴尬的事实即使 A35 没有动任何预分频和校准配置它操作控制寄存器的时机如果和 M33 的初始化动作撞在一起就有可能导致 M33 正在做的配置序列被中断或者配置值被覆盖。M33 这边还认为自己握着锁、没人会来碰寄存器实际上 A35 已经偷偷把 RTC 的状态机推了一把。锁的语义边界和硬件操作的真实需求之间裂开了一条缝这条缝就是本次问题的根源。所以从一开始就应当意识到RESMGR_RTC_INIT_RSC 锁住的只是初始化配置这个动作而不是RTC 外设的所有访问。如果产品需求要求 RTC 完全由 M33 独占那就必须靠别的机制来保证不能让这把锁承担它本来就不具备的职责。3. A35 侧 RTC 时间更新的完整路径追踪3.1 从 date -s 到寄存器写入Linux 的调用链路为了确认 A35 是怎么把时间写进去的我把完整的调用路径跟踪了一遍。执行date -s 2025-06-01 12:00:00后实际发生的事比我想象中多得多。第一步date 工具会调用 glibc 的 settimeofday()把字符串形式的时间解析成内核时间结构体写入 Linux 的 system time。这一步只是改了内核软件计时器跟 RTC 还没有关系。第二步内核时间更新完成后timekeeping 模块会察觉到系统时间发生变化。如果内核配置了CONFIG_RTC_HCTOSYS且策略允许或者系统里有服务执行了hwclock --systohcRTC 框架就会被触发调用 rtc_set_time()。第三步rtc_set_time() 通过 RTC 子系统找到注册到系统里的 RTC 设备调用设备驱动结构体里的 set_time 回调。在 STM32MP2 的 BSP 中这个回调对应 stm32 系列的 RTC 驱动函数它可以拿到一个指向当前 RTC 设备的数据结构里面有寄存器映射地址、时钟信息、锁信息等。第四步也是问题的核心驱动函数直接操作映射到内存的 RTC 寄存器先把 RTC 的控制寄存器切换成可写模式再把年、月、日、时、分、秒的计算结果换算成 BCD 码写入 RTC_TR 和 RTC_DR最后退出可写模式。整个过程中驱动不会去查询 RESMGR 里 RTC 资源锁的状态也不会试图获取这把锁。它只是按手册描述机械地完成一次寄存器写入。这条链路在单核系统里没有任何问题因为 CPU 天然就是唯一的外设访问者。但在双核异构系统里这条链路上的每一步都没有考虑一个前提另一个内核可能正在管理这个外设。3.2 为什么这条路径没有经过锁一句话总结Linux RTC 驱动模型在设计时默认 RTC 是一个本机独占设备完全没有外设可能被另一个核心管理的假设。STM32MP2 的 BSP 确实在系统启动早期通过资源管理器完成了大量外设的资源分配但驱动代码层面的资源和 RESMGR 锁是两套体系。像 rtc-stm32 这类驱动本身是基于普通 I2C/MMIO 设备模型写的它关心的只是时钟有没有打开、中断有没有配置好、设备树里有没有给自己的寄存器地址留映射。只要 DTS 里 RTC 节点的 status 是 enabledLinux 就会把它当作真实可用的设备注册到内核进而允许系统时间同步时回写 RTC。还有一个关键细节Linux 系统每次启动、每次网络对时、每次系统时间发生明显跳变时都可能触发 RTC 回写。即使没有任何用户手动写时间光靠 NTP 服务同步就能让内核在后台调用一次 rtc_set_time()。这种悄无声息的写入比用户手动执行命令更危险因为它在日志里几乎不留痕迹。如果你想在 A35 的 Linux 侧快速验证有一个很直接的办法用 devmem2 直接读取 RTC 寄存器当前值再用同样的方式写入一个新值看会不会得到硬件层面的响应。如果 devmem2 能写成功说明硬件层面没有任何保护在挡锁只是软件协议层面的一纸约定而已。3.3 高频触发源与隐蔽触发源在排查过程中我总结了一份触发源清单方便下次直接对照。高频触发源包括用户手动执行date -s、hwclock -w或hwclock --systohcsystemd 的 systemd-timedated 服务在系统时间发生较大跳变时自动同步 RTCNTP 服务同步后自动把系统时间写回 RTC以及系统启动阶段如果内核认为 RTC 时间比系统时间更旧会在系统时间更新后立即执行一次 SYSTOHC。隐蔽触发源更要留心。有次测试发现时间在凌晨三点多被改了查了所有调度任务都没找到可疑项最后翻/root/.bash_history才发现是之前一个小伙伴手动执行过一次校时脚本脚本里残留了一段自动执行hwclock -w的逻辑。还有一次是 OTA 脚本在升级完成后主动校正系统时间这个时间同步回写了 RTC日志里完全看不出来。这类问题一旦隐藏在产品代码里往往要等很久才会被现场数据暴露出来。这些触发源有一个共同特点它们最终都会路由到 Linux RTC 框架的 rtc_set_time()。只要 RTC 驱动还在设备树里正常注册这条路径就永远存在。4. 从日志、断点到复现把谁在写钉死4.1 取证先抓住时间跳变的案发时刻排查这种双核资源冲突第一步不是去改代码而是把谁在什么时间做了什么钉死。我们当时的做法是先在 M33 侧所有读取 RTC 的地方加上带 tick 的日志把每次读到的绝对时间打出来。跳变一旦发生就能从日志里拿到一个精确到毫秒的时间点。有了这个时间点再去查 A35 侧的系统日志。重点看三样东西syslog 里有没有 NTP 同步记录journal 里有没有 systemd-timedated 的 RTC 同步记录以及/root/.bash_history和 cron 列表里有没有手动校时的痕迹。大多数情况下这个时间点能对上某个服务的行为冲突源基本就锁定了一半。如果日志定位不到就用 strace 实时跟踪一条校时命令看它到底调用了哪些系统调用strace -f -e tracesettimeofday,clock_settime,ioctl date -s 2025-06-01 12:00:00在输出里能看到 date 命令调用 settimeofday 之后RTC 框架最终通过 ioctl 把新的时间传给了 RTC 设备。这能帮你确认这条校时路径确实走到了 RTC 驱动而不是只停留在内核的软件时间层。4.2 用内核跟踪把调用栈抓出来有时候光靠用户态日志不够因为时间写入可能是内核自己触发的比如 NTP 同步引起的内核时间跳变后自动回写 RTC。这时可以在 A35 的内核里挂一个函数跟踪直接看是谁在调用 RTC 驱动的 set_time 回调。以 ftrace 为例可以把 RTC 驱动的 set_time 函数放到跟踪列表里然后触发一次校时操作看函数调用栈echo stm32_rtc_set_time /sys/kernel/tracing/set_ftrace_filter echo function /sys/kernel/tracing/current_tracer echo 1 /sys/kernel/tracing/tracing_on date -s 2025-06-01 12:00:00 cat /sys/kernel/tracing/trace如果你的内核里这个函数名不是stm32_rtc_set_time可以先在/proc/kallsyms里搜一下 stm32_rtc 相关的符号再放到 trace 列表里。跟踪结果里会显示调用者的完整调用链比如是rtc_set_time被hwclock的 ioctl 触发还是被内核的sync_hw_clock工作队列触发。这一步能把到底是谁发起的写时间彻底锁死。4.3 硬件断点级别的验证如果想要更实锤的证据可以用调试器在 RTC 寄存器的写地址上打数据断点watchpoint。Cortex-A35 和 Cortex-M33 都有硬件调试单元理论上只要你让调试器同时连接到两个核把 RTC_TR 地址设为写入断点一旦有代码写这个寄存器断点就会触发你就能看到执行这条写的 PC 落在哪个内核、哪段代码里。但在实际项目中同时调试两个核的难度比想象中大。很多调试 IDE 对异构双核同时调试的支持并不完善一个调试会话只能挂一个核两个核之间的调试切换会导致另一个核继续跑很容易错过触发时机。如果你们团队有条件用好这种多核调试方式它当然是最直接的证据如果没有条件也不必强求用 4.1 和 4.2 的组合已经能拿到足够可靠的结论。还有一个简单粗暴但有效的方法直接改 RTC 的值看 M33 那边的反应。在 A35 的 Linux 上执行date -s同时观察 M33 串口打印如果时间立刻跟着变那么在物理层面已经证实了 A35 对 RTC 的写操作是完全无防护
返回列表