reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
154
vendor-new/rev-skills/.claude/skills/re-rtos/references/ecos.md
Normal file
154
vendor-new/rev-skills/.claude/skills/re-rtos/references/ecos.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# eCos:ISR / DSR / thread、CDL 配置系统、HAL 与启动
|
||||
|
||||
<CORE RULE>
|
||||
eCos 有两条同样重要的东西,**只掌握其中一条都会误判**:
|
||||
|
||||
```
|
||||
运行期:ISR → DSR → thread 三层(中断模型)
|
||||
构建期:CDL 配置系统决定编进什么、向量在哪、谁先跑
|
||||
```
|
||||
|
||||
**同一份源码在不同配置下的产物差异极大**——所以顺序是:
|
||||
|
||||
```
|
||||
先确认配置(CDL),再谈代码
|
||||
```
|
||||
</CORE RULE>
|
||||
|
||||
## 一、ISR 与 DSR 的分工
|
||||
|
||||
- ISR 返回时若带"需要调用 DSR"的位,系统会调用关联的 **DSR(Deferred Service Routine)**
|
||||
- **若调度器未被锁定,DSR 立即运行**;**若被中断的线程恰在内核调用里锁了调度器,DSR 会被推迟到解锁之后**
|
||||
- 这个推迟正是 DSR 能安全做某些内核调用的原因——**例如 post 一个信号量、signal 一个条件变量**
|
||||
|
||||
## 二、同步级别是 API 的属性
|
||||
|
||||
| 级别 | 谁可以调用 |
|
||||
|---|---|
|
||||
| **Thread** | 仅线程代码 |
|
||||
| **DSR** | **DSR 或线程**代码 |
|
||||
| **ISR** | **ISR、DSR 或线程**代码 |
|
||||
|
||||
**调用点落在哪一级,决定了哪些 API 是合法的。**
|
||||
|
||||
## 三、三种同步机制
|
||||
|
||||
| 机制 | 作用 | 谁能用 |
|
||||
|---|---|---|
|
||||
| **与 ISR 同步**(禁中断 + SMP 下自旋锁) | 阻止临界区内 ISR 运行 | 必须**短**且少用 |
|
||||
| **与 DSR 同步** | 阻止临界区内 DSR 运行(内核里等价于取调度器锁) | **DSR 与线程可用;ISR 不允许使用** |
|
||||
| **与线程同步**(互斥量、条件变量) | 线程级阻塞与唤醒 | **只有线程可以 lock 互斥量与 wait 条件变量;DSR 可以 signal 条件变量** |
|
||||
|
||||
**数据保护按"谁会碰它"分层**:被 ISR 访问的数据在任何层级(**包括 ISR 内部**)都要用 ISR 锁/自旋锁保护;DSR 与线程共享的用 DSR 锁;只有线程访问的用互斥量。
|
||||
|
||||
## 四、三种典型驱动模型
|
||||
|
||||
1. **全部处理放在 ISR**(DSR 只 signal 条件变量唤醒线程)
|
||||
2. **处理推迟到 DSR**(DSR 可选地 signal 条件变量并解屏蔽中断)
|
||||
3. **处理推迟到线程**(DSR 只 signal,线程随后获得完整内核设施)
|
||||
|
||||
## 五、CDL:配置系统决定一切
|
||||
|
||||
配置由 **CDL 定义**驱动,选项决定"编进什么"。几个有逆向价值的类别:
|
||||
|
||||
- **通信通道选择**(通道数量、调试通道、控制台通道及其波特率、控制台默认通道)
|
||||
- **使用 ROM 监视器时这些通道选项通常失效**——由监视器选择通道;应用可通过配置项声明**自己接管通信**
|
||||
- **调试/GDB 桩的包含与否**直接影响镜像内容与异常处理去向
|
||||
- **构建目标**(ROM / ROMRAM / RAM 等)决定镜像是否需要在启动时重定位
|
||||
|
||||
**逆向含义**:拿到一个 eCos 镜像,"里面有什么"是配置的产物,不是源码的自然结果。**先把配置项与编入项对上,再解释代码。**
|
||||
|
||||
## 六、HAL:向量表与 VSR
|
||||
|
||||
- **向量表的位置完全由内存映射决定**:由内存布局工具决定,并体现在布局描述文件里
|
||||
- 启动时**初始化向量表**
|
||||
- 同步异常的向量项**大多指向一个默认异常向量**:它保存 CPU 状态并调用异常处理器,后者按配置**路由到调试桩或路由到 eCos**
|
||||
- **由 ROM 监视器加载的应用,其向量项指向监视器自己的异常向量**——所以异常会进监视器(例如调试器),**除非应用显式替换该项**
|
||||
- 异常向量的偏移由 HAL 头文件里的编号定义
|
||||
|
||||
## 七、虚拟向量:ROM 监视器给 RAM 应用提供的服务表
|
||||
|
||||
- 在**静态地址**处有一张表(几十个向量),指向服务函数或数据,让 ROM 配置(如 RedBoot)**为单独链接的 RAM 应用**提供服务
|
||||
- 服务含:通信通道(控制台/调试)、微秒延时、复位、缓存刷新、**调试/控制台通道切换**、**调试系统调用(线程信息)**、flash 配置访问
|
||||
- 由 ROM 监视器或按策略由独立/RAM 配置初始化;**无论用不用都保留固定的字节数**
|
||||
- **代价是间接层**(体积/性能开销,并多一个失败点);收益是**应用可以不含调试桩也能被调试**
|
||||
|
||||
## 八、启动顺序(RedBoot / 典型 ROM 配置)
|
||||
|
||||
```
|
||||
复位入口
|
||||
→ 平台设置宏:禁中断与缓存、初始化内存控制器与时钟、建立并开启 MMU
|
||||
(ROMRAM 情形还要重定位镜像)
|
||||
→ HAL 启动:在 0 号位置放置向量指令、复制 .data、设栈、清 .bss
|
||||
→ 硬件初始化
|
||||
→ (含调试桩则)初始化调试桩
|
||||
→ 中断(ctrl-C 类)初始化
|
||||
→ 【调用构造函数】
|
||||
→ 进入 cyg_start
|
||||
```
|
||||
|
||||
**构造函数在硬件/桩/中断初始化之后、进入主启动之前运行。**
|
||||
|
||||
## 九、构造函数与内存布局的一个真实坑
|
||||
|
||||
**向量表所在区域如果与构造函数区域重叠,初始化会把构造函数冲掉**——曾出现过"预期的构造函数在某个地址上,结果被初始化向量表时覆盖"的实例。
|
||||
|
||||
**所以**:向量表位置必须按目标内存映射决定,**且如果需要被初始化,就不能放在 flash 里**。
|
||||
|
||||
## 十、调试与 GDB 桩
|
||||
|
||||
- **调试桩在 HAL 启动阶段被初始化**;异常处理器在**含桩时进 GDB**
|
||||
- ROM 监视器通常包含 GDB 协议支持,**可以关掉以节省代码与内存**
|
||||
- 虚拟向量让"不含桩的应用"也能被调试;但**某些较老的 ROM 监视器在异步调试中断上会失败**,除非应用里也配了调试支持
|
||||
- 相关配置项控制"是否构建带 GDB 的监视器"以及"是否包含桩"
|
||||
|
||||
## 十一、平台差异与可配置性
|
||||
|
||||
- eCos 覆盖多种架构的 HAL,**不同架构的 HAL 在向量表、启动序列、缓存与 MMU 处理上不同**
|
||||
- 调度器、定时器、同步原语都受配置影响;**"这版 eCos 的线程模型是怎样的"要由配置回答**
|
||||
- 内存池与堆的配置同样属于构建期选择
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 eCos 目标
|
||||
├─ 先确认配置(CDL)
|
||||
│ ├─ 构建目标(ROM/ROMRAM/RAM)→ 是否启动时重定位
|
||||
│ ├─ 是否含调试桩 → 异常去向
|
||||
│ └─ 通道/控制台/调度器等选项 → 编入了什么
|
||||
├─ 看到一个很短的中断处理函数
|
||||
│ └─ 真正工作可能在 DSR(或第三个线程)
|
||||
├─ 看到一个没有线程入口却反复执行的函数
|
||||
│ └─ 可能是 DSR
|
||||
├─ 判断某个内核调用是否合法
|
||||
│ └─ 查它的同步级别(Thread / DSR / ISR)
|
||||
├─ 异常去向与预期不符
|
||||
│ ├─ 是否含调试桩?
|
||||
│ ├─ 向量项指向 eCos 还是 ROM 监视器?
|
||||
│ └─ 应用是否替换了向量项
|
||||
├─ 构造函数"没运行"
|
||||
│ └─ 查向量表所在区域是否与构造函数区域重叠(会被初始化冲掉)
|
||||
└─ 找不到某个功能
|
||||
└─ 可能是配置里就没编进去
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 配置:CDL 选项与编入项、构建目标、调试桩是否包含
|
||||
- 分层:ISR 返回值(是否调度 DSR)、DSR 注册与调用点、各调用的同步级别
|
||||
- HAL:向量表位置(布局文件)、向量项指向、异常处理器路由
|
||||
- 启动:复位入口 → 平台设置 → HAL 启动 → 构造函数 → 主启动
|
||||
- 验证:能同时说清「这份镜像是哪套配置构建的 + 中断处理分布在哪几层 + 异常会去哪里」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把 ISR 当完整的中断处理**:工作可能在 DSR,甚至在第三个线程里
|
||||
- **把 DSR 当普通线程回调**:它有同步级别与调度约束,且会被调度器锁推迟
|
||||
- **在 ISR 里套用 DSR 级同步的假设**:ISR 不允许使用 DSR 锁
|
||||
- **以为 DSR 可以像线程一样等待**:能 signal 条件变量,不能 wait
|
||||
- **按"中断发生时刻"推断时序**:DSR 的运行时机受调度器锁影响
|
||||
- **不看配置就解释代码**:CDL 决定编入项、通道、调试桩、构建目标
|
||||
- **把异常去向当固定**:取决于是否含桩、向量项指向谁、应用是否替换
|
||||
- **忽略向量表与构造函数区域冲突**:构造函数会被初始化覆盖
|
||||
- **以为 ROM 监视器加载的应用异常会进 eCos**:默认进监视器
|
||||
- **把虚拟向量当纯开销**:它让无桩应用也能被调试,是有意的权衡
|
||||
@@ -0,0 +1,109 @@
|
||||
# FreeRTOS:ISR 上下文 / FromISR API / MPU 受限任务
|
||||
|
||||
<CORE RULE>
|
||||
FreeRTOS 表面很小,**最大的坑不是"简单",而是上下文**:
|
||||
|
||||
```
|
||||
xQueueSend() vs xQueueSendFromISR()
|
||||
xTaskNotifyGive() vs vTaskNotifyGiveFromISR()
|
||||
taskENTER_CRITICAL() vs taskENTER_CRITICAL_FROM_ISR()
|
||||
```
|
||||
|
||||
**这些不是可随意互换的别名。** 在 ISR 里看到 task 版 API,不能顺着"正常业务逻辑"继续分析——先判断**它到底在不在 ISR 上下文**、以及是不是符号/包装器识别错了。
|
||||
</CORE RULE>
|
||||
|
||||
## 一、为什么要有 FromISR 变体
|
||||
|
||||
在 ISR 里,**调度器实际上是冻结的**:ISR 不能阻塞,内核也不能在 ISR 内做任务切换。普通 API 可能阻塞(例如等队列空位)或在内部触发调度——**在中断上下文里都是禁止的**。
|
||||
|
||||
`...FromISR()` 变体的共同点:**不阻塞、使用更轻量的临界区、把调度决定交回给应用**。
|
||||
|
||||
## 二、`pxHigherPriorityTaskWoken` 与 `portYIELD_FROM_ISR`
|
||||
|
||||
- 以 `xQueueSendFromISR(queue, item, pxHigherPriorityTaskWoken)` 为例:若这次发送解除了某个任务的阻塞**且该任务优先级高于当前运行任务**,则把 `*pxHigherPriorityTaskWoken` 置为 `pdTRUE`,表示**退出中断前应请求一次上下文切换**
|
||||
- **自 V7.3.0 起该参数可选(可传 NULL)**。取舍要清楚:
|
||||
- 传 NULL 后若调用 `portYIELD_FROM_ISR(pdTRUE)` → **总是强制让出**(可能是不必要的切换,甚至切到同优先级轮转的对等任务)
|
||||
- 传 NULL 后不 yield → **更高优先级的就绪任务可能一直等到后续事件才被调度**
|
||||
- 典型形态:
|
||||
|
||||
```c
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
xQueueSendFromISR(xQueue, &item, &xHigherPriorityTaskWoken);
|
||||
if (xHigherPriorityTaskWoken) { portYIELD_FROM_ISR(); }
|
||||
```
|
||||
|
||||
- **`portYIELD_FROM_ISR()` 并不在调用点立即切换**:在 Cortex-M 上通常是 **pend PendSV**,真正的切换发生在中断退出之后(tail-chaining)——这对时序分析很重要
|
||||
|
||||
**所以**:"函数尾部突然出现调度/yield 相关代码"的 CFG,**可能只是 ISR 正常触发高优先级任务切换**。
|
||||
|
||||
## 三、`configMAX_SYSCALL_INTERRUPT_PRIORITY`:越界的中断不能碰任何 API
|
||||
|
||||
- 该配置定义**允许调用 FreeRTOS API 的最高(数值最低的)中断优先级**
|
||||
- **数值上比它更低(更紧急)的 ISR 不得调用任何 FreeRTOS API(包括 FromISR 版本),也不能使用 `portYIELD_FROM_ISR`**——这类 ISR 只能做纯硬件操作或置标志
|
||||
- Cortex-M 的临界区通过写 **BASEPRI** 实现:只屏蔽该优先级及以下的中断,让最紧急的中断仍能通过
|
||||
- **注意优先级数值方向**:Cortex-M/STM32 上数值越小优先级越高,**与任务优先级的方向相反**——这一条经常导致误判
|
||||
- 典型故障:ISR 调用了 `xQueueSendFromISR` 但**没有设置 NVIC 优先级**(默认 0 = 最高),触发端口层的优先级校验断言(`vPortValidateInterruptPriority` 一类)
|
||||
|
||||
## 四、FreeRTOS-MPU:受限任务与运行期改区
|
||||
|
||||
**受限任务**通过 `xTaskCreateRestricted(TaskParameters_t *, TaskHandle_t *)` 创建。参数是静态配置的结构体:任务函数、名字、栈深度、参数、优先级、**栈缓冲**,以及 **`xRegions[portNUM_CONFIGURABLE_REGIONS]`**(Cortex-M3 上为 3 个)。
|
||||
|
||||
- **`uxPriority` 与 `portPRIVILEGE_BIT` 相或** → 特权模式任务;否则任务以**用户模式**运行
|
||||
- 栈缓冲须满足 MPU 约束,常用编译期对齐(如 `__attribute__((aligned(...)))`);传 NULL 时使用端口提供的对齐分配函数
|
||||
- **`xTaskCreateRestrictedStatic()`**:完全不动态分配(需开启静态分配支持),还要求应用提供 `StaticTask_t` 存储——可把它放进内核特权数据区
|
||||
- **`MemoryRegion_t { pvBaseAddress, ulLengthInBytes, ulParameters }`**:**区域的大小与对齐必须是同一个 2 的幂**(硬约束)
|
||||
- `ulParameters` 可取(可相或):读写、特权只读、只读、特权读写、可缓存/可缓冲、**永不执行** 等标志
|
||||
- **每次任务切换时 MPU 会被动态重配**,为进入的任务授予其自身栈的读写权限
|
||||
- **运行期可以改区**:`vTaskAllocateMPURegions(handle, regions)`(handle 传 NULL 表示修改当前任务),底层经端口函数写入硬件
|
||||
|
||||
**于是**:
|
||||
|
||||
```
|
||||
任务 A 能访问 buffer X,任务 B 访问同一地址 fault
|
||||
→ 不一定是内存损坏,可能是 MPU 策略
|
||||
|
||||
同一任务昨天能访问,运行到某阶段后不能了
|
||||
→ 可能是因为运行期重新分配了 MPU 区域
|
||||
```
|
||||
|
||||
另外:特权任务可访问整个内存映射;用户模式任务可访问除"特权专用"之外的全部 RAM/FLASH。端口层提供的降权入口(切到用户模式)**是单向的——无法再回到特权模式**。
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 FreeRTOS 目标
|
||||
├─ 看到一个中断服务函数
|
||||
│ ├─ 它调用的是 task 版还是 FromISR 版 API?
|
||||
│ ├─ 是 task 版 → 先确认(上下文判定 / 符号或包装器识别)哪里错了
|
||||
│ └─ 有 pxHigherPriorityTaskWoken + portYIELD_FROM_ISR → ISR 正常触发切换,不是混淆
|
||||
├─ 中断优先级相关异常
|
||||
│ ├─ 查该 ISR 的优先级是否高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(更紧急)
|
||||
│ ├─ 更紧急 → 它不应调用任何 FreeRTOS API
|
||||
│ └─ 注意数值方向与任务优先级相反
|
||||
├─ 某地址"有时能访问、有时不能"
|
||||
│ ├─ 查是否 MPU 受限任务(xTaskCreateRestricted)
|
||||
│ ├─ 查是否运行期调用了 vTaskAllocateMPURegions
|
||||
│ └─ 别直接判堆/栈损坏
|
||||
└─ 上下文判定
|
||||
└─ 区分任务上下文与 ISR 上下文(能否阻塞、能否触发调度)
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 上下文:ISR 与任务代码的入口、端口层的进入/退出宏、会话中使用的 API 变体
|
||||
- 优先级:`configMAX_SYSCALL_INTERRUPT_PRIORITY` 相关配置与各中断的实际 NVIC 优先级
|
||||
- MPU:任务的 `xRegions` 配置、运行期改区调用、端口层写硬件的路径
|
||||
- 验证:能同时说清「上下文是任务还是 ISR + 所用 API 变体是否匹配 + 地址权限来自哪组 MPU 区域」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把 task 版与 FromISR 版 API 当可互换**:阻塞与调度语义完全不同
|
||||
- **在中断上下文按阻塞语义继续分析**:ISR 不能阻塞,也不能在内部触发调度
|
||||
- **忽略 `pxHigherPriorityTaskWoken`**:漏掉 yield 会让更高优先级就绪任务被推迟
|
||||
- **以为 `portYIELD_FROM_ISR()` 立即切换**:通常是 pend,真正切换在中断退出后
|
||||
- **把 ISR 尾部的调度代码当混淆**:那是正常的切换请求
|
||||
- **忘记中断优先级数值方向与任务优先级相反**:判断"能否调用 API"会反
|
||||
- **在高于 `configMAX_SYSCALL_INTERRUPT_PRIORITY` 的 ISR 里调用任何 API**:连 FromISR 也不行
|
||||
- **把 MPU 导致的访问失败判成内存损坏**:受限任务的区域约束是配置决定的
|
||||
- **假设 MPU 区域在任务生命周期内不变**:运行期可以重新分配
|
||||
- **忽略"降权到用户模式无法回退"**:单向行为,不是权限丢失
|
||||
@@ -0,0 +1,139 @@
|
||||
# Nucleus RTOS:线性内存映射 + 保护域 / 动态模块生命周期
|
||||
|
||||
<CORE RULE>
|
||||
最反直觉的一条:
|
||||
|
||||
```
|
||||
存在 "process isolation"
|
||||
≠ 采用 Linux 那种"每进程一个完全独立的虚拟地址世界"
|
||||
```
|
||||
|
||||
Nucleus 的 Process Model:
|
||||
|
||||
- Cortex-A 上可用 **MMU**,Cortex-M 上可用 **MPU**
|
||||
- 提供 **space-domain partitioning(空间域分区)** 与 **entitlement(授权)**
|
||||
- **采用线性(flat)内存映射 + 受保护内存区域**
|
||||
- **application 与 kernel module 可以动态 reload / restart / update,而不需要停掉整个系统**
|
||||
|
||||
于是:
|
||||
|
||||
```
|
||||
两个 process 里的地址看起来线性、接近 → 不能判"没有地址空间隔离"
|
||||
同一地址 A 能访问、B fault → 可能只是 entitlement 不同
|
||||
module 被替换而系统从未 reboot → 可能是正常的模块生命周期
|
||||
```
|
||||
|
||||
**判断隔离要看 MMU/MPU entitlement,而不是比较指针数值。**
|
||||
</CORE RULE>
|
||||
|
||||
## 一、Process Model 与 POSIX 心智模型的区别
|
||||
|
||||
要恢复的图是:
|
||||
|
||||
```
|
||||
Nucleus Process
|
||||
├─ modules
|
||||
├─ tasks
|
||||
├─ protected regions
|
||||
└─ entitlements
|
||||
```
|
||||
|
||||
**而不是** `PID → 页表 → fork 父进程 → exec ELF`。Nucleus 的 "process" 重点是**模块加载、内存访问保护、特权与隔离**,不是复制 Linux process ABI。
|
||||
|
||||
- Cortex-M 上**没有完整 MMU**也能有 process 模型——靠 MPU region 重配置、特权级与 entitlement;**"找不到页表"不能推出"没有 Process Model"**
|
||||
- 一个 process 出错**不应默认全系统一起死**:空间隔离的价值就是让故障恢复可以是"重启出错的 module/process,其他子系统继续"
|
||||
|
||||
## 二、术语坑:`partition` 有两种完全不同的含义
|
||||
|
||||
| 名称 | 含义 |
|
||||
|---|---|
|
||||
| **Process Model 的 space partition** | 进程/子系统的空间隔离域 |
|
||||
| **`NU_PARTITION_POOL`** | **固定大小内存块的分配器(memory partition pool)** |
|
||||
|
||||
```
|
||||
NU_Create_Partition_Pool(pool, name, start_address, pool_size, partition_size, suspend_type)
|
||||
NU_Allocate_Partition(pool, &ptr, suspend)
|
||||
NU_Deallocate_Partition(ptr)
|
||||
```
|
||||
|
||||
这里的 **partition 指的是"固定大小的内存块"**,与隔离域无关。**看到 partition 这个词必须先分清是哪一种。**
|
||||
|
||||
## 三、固定块分配器的语义(决定了"卡在分配器里"是否正常)
|
||||
|
||||
- 池是**连续内存区 + 固定大小块**(例如 2000 字节、块 40 字节 = 50 块)
|
||||
- **分配是确定性的 O(1)**:不做搜索;**不可能产生碎片**,唯一的失败模式是**该池真的耗尽**
|
||||
- suspend 选项:
|
||||
- **`NU_NO_SUSPEND`**:无论能否满足都立即返回;**非任务线程(如 `Application_Initialize`)只能用它**
|
||||
- **`NU_SUSPEND`**:调用任务**挂起直到有块被归还**
|
||||
- **超时值**:挂起到有块或超时
|
||||
- 多个任务挂起在同一池上时,按池创建时指定的 **FIFO 或优先级**顺序恢复
|
||||
- 返回值 **`NU_NO_PARTITION`** 表示"无法立即满足"
|
||||
|
||||
**所以**:
|
||||
|
||||
```
|
||||
task 卡在分配器里、state = suspended
|
||||
→ 用 NU_SUSPEND 且池空时,按语义本来就该挂起
|
||||
→ 不是 heap deadlock
|
||||
|
||||
分配失败但系统 RAM 还剩很多
|
||||
→ 固定池耗尽与通用堆无关,也不是"全局堆碎片"
|
||||
```
|
||||
|
||||
## 四、动态模块生命周期
|
||||
|
||||
Nucleus 支持**动态 reload / restart / update** application 与 kernel modules,**不影响其他 module、也不必 shutdown 整个系统**。
|
||||
|
||||
于是固件时间线里出现:
|
||||
|
||||
```
|
||||
module A 版本 X → 短暂停止 → module A 版本 Y
|
||||
且全程没有 boot event
|
||||
```
|
||||
|
||||
**不能单凭这一点判"运行时注入 / rootkit 替换"**——先查该系统的 module lifecycle/update 路径。
|
||||
|
||||
这与"单体嵌入式镜像:更新一块 → 整机重启"的传统假设不同。
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 Nucleus 目标
|
||||
├─ 看到 "partition"
|
||||
│ ├─ 是 Process Model 的空间隔离域?
|
||||
│ └─ 还是 NU_PARTITION_POOL 固定块分配器?
|
||||
├─ 两个"process"的地址看起来线性/接近
|
||||
│ ├─ 线性内存映射 + 受保护区域就是该模型
|
||||
│ └─ 判断隔离要看 entitlement,不是比地址
|
||||
├─ 同一地址访问权限不同
|
||||
│ └─ 符合 "同一线性地址 + 不同保护授权"
|
||||
├─ Cortex-M 上没有页表
|
||||
│ └─ 仍可有 process 模型(MPU + 特权 + entitlement)
|
||||
├─ task 挂起在分配器里
|
||||
│ ├─ 查是否用了 NU_SUSPEND 且池空
|
||||
│ └─ 查池容量与归还路径,而不是堆死锁
|
||||
├─ 分配失败但内存充足
|
||||
│ └─ 固定池耗尽,与通用堆无关
|
||||
└─ module 在无 reboot 的情况下被替换
|
||||
└─ 查官方 module reload/update 生命周期
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 隔离:MMU/MPU 配置、受保护区域、entitlement 与特权状态
|
||||
- 内存:partition pool 的块大小与块数、当前占用与等待者;区分它与通用堆
|
||||
- 生命周期:模块加载/重载/更新的路径与版本变更点
|
||||
- 验证:能同时说清「隔离靠 MMU 还是 MPU、授权是什么 + 该地址属于哪个域 + 分配失败是池耗尽还是堆问题」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **用 Linux 每进程独立地址空间的模型理解隔离**:这里是线性映射 + 受保护区域 + entitlement
|
||||
- **比较指针数值判断是否共享内存**:地址相同不代表内存共享
|
||||
- **在 Cortex-M 上因找不到页表就否定 Process Model**:MPU 也能构建隔离
|
||||
- **把 `partition` 一律理解为隔离域**:`NU_PARTITION_POOL` 是固定块分配器
|
||||
- **把 `NU_SUSPEND` 导致的挂起当 heap deadlock**
|
||||
- **把固定池耗尽诊断为全局堆碎片**:该分配器不产生碎片
|
||||
- **把无 reboot 的 module 替换当注入**:动态 reload/restart/update 是既有能力
|
||||
- **在非任务线程里用 `NU_SUSPEND`**:只允许 `NU_NO_SUSPEND`
|
||||
- **默认 process 出错就是全系统复位**:隔离的意义正是局部恢复
|
||||
- **期待 fork/exec/mmap 语义**:那不是这个 process 模型的重点
|
||||
117
vendor-new/rev-skills/.claude/skills/re-rtos/references/nuttx.md
Normal file
117
vendor-new/rev-skills/.claude/skills/re-rtos/references/nuttx.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# NuttX:FLAT / PROTECTED / KERNEL 三种内存模型
|
||||
|
||||
<CORE RULE>
|
||||
分析 NuttX 之前**必须先问一句:这是 FLAT、PROTECTED 还是 KERNEL 构建?**
|
||||
|
||||
不知道是哪一种,反编译出来的 syscall 与地址语义**很容易全错**——同一个 API 在三种模型下的产物完全不同。
|
||||
</CORE RULE>
|
||||
|
||||
## 一、三种模型
|
||||
|
||||
| 模型 | 结构 | 地址空间 |
|
||||
|---|---|---|
|
||||
| **FLAT** | OS 与应用在同一个平坦镜像里,可直接互相访问 | 单一 |
|
||||
| **PROTECTED** | 两遍构建产出**两个 blob**:特权内核 blob + 非特权用户 blob;由 MPU 施加限制;**用户侧 API 是自动生成的 syscall proxy** | 内核态/用户态分离 |
|
||||
| **KERNEL** | 内核地址空间 + **每个用户进程独立地址环境**;需要 MMU | 每进程独立 |
|
||||
|
||||
## 二、同一个 API,反编译结果完全不一样
|
||||
|
||||
以 `getpid()` 为例:
|
||||
|
||||
```
|
||||
FLAT :直接 call 到 NuttX 的实现
|
||||
PROTECTED :pid_t getpid(void) { return (pid_t)sys_call0(SYS_getpid); }
|
||||
→ 特权切换 → 内核侧 stub → 真正的 getpid
|
||||
```
|
||||
|
||||
- **PROTECTED 下面向用户的 API 是自动生成的 syscall proxy**:由 `syscall/syscall.csv` 经 `tools/mksyscalls` 生成;内核侧另有自动生成的 stub 把 syscall 映射回真实调用
|
||||
- 因此:
|
||||
|
||||
```
|
||||
getpid() 只有两三条汇编、马上 SVC/trap
|
||||
→ 不要判 "libc 被 hook" 或 "syscall wrapper 异常"
|
||||
→ 这是该构建方式的正常形态
|
||||
```
|
||||
|
||||
**RE 判据**:看到大量"极短函数 + 立即陷入特权级"的模式,先确认是不是 PROTECTED 构建,再谈可疑性。
|
||||
|
||||
## 三、两个堆(PROTECTED 下)
|
||||
|
||||
- 默认情况下可以只有**一个用户空间堆**,被内核态与用户态代码共用——简单,但内核态的分配没有隔离
|
||||
- 开启 `CONFIG_MM_MULTIHEAP=y` 与 `CONFIG_MM_KERNEL_HEAP=y` 后有两套分配器:
|
||||
- **非受保护的用户堆**:内核与用户代码共用(分配器物理上位于用户地址空间,内核从用户 blob 头部的信息拿到它的地址)
|
||||
- **受保护的内核堆**:只有 NuttX 内核可访问,构建进内核块;`CONFIG_MM_KERNEL_HEAPSIZE` 设其大小(默认 8192 字节),剩余内存给用户堆,**按最小内存保护区对齐后内核堆实际可能更大**
|
||||
- 接口分工:`up_allocate_heap()` 返回用户堆,`up_allocate_kheap()` 提供内核堆
|
||||
- 该配置项在 protected/kernel 构建下默认开启、在 flat 下默认关闭
|
||||
|
||||
**所以**:
|
||||
|
||||
```
|
||||
地址 A malloc 出来用户能访问
|
||||
地址 B malloc 出来用户 fault
|
||||
→ 不一定是堆损坏,可能是分别来自 malloc 与 kmalloc
|
||||
```
|
||||
|
||||
## 四、KERNEL 模型的堆布局
|
||||
|
||||
一个内核堆 + **每个 task group 一个用户堆**;只有内核堆在架构的 `up_allocate_kheap()` 里初始化。典型内存图:内核 `.data` / 内核 `.bss` / 内核 idle 栈 / 填充 / 用户 `.data` / 用户 `.bss` / 内核堆(`CONFIG_MM_KERNEL_HEAPSIZE`)/ 用户堆(延伸到 SRAM 末尾)。
|
||||
|
||||
## 五、驱动分 upper half 与 lower half
|
||||
|
||||
NuttX 驱动通常分两层:
|
||||
|
||||
| 层 | 位置 | 职责 |
|
||||
|---|---|---|
|
||||
| **upper half** | `drivers/`(通用、与硬件无关) | 用 `register_driver()` / `register_blockdriver()` 注册,实现 `read`/`write`/`close`/`ioctl`/`poll` 等标准接口,**通过回调调用 lower half** |
|
||||
| **lower half** | `arch/<架构>/src/...` 或 `boards/<架构>/<芯片>/<板>/src` | 寄存器访问与配置、中断处理,按标准回调结构(如串口的 `uart_ops_s`)暴露能力 |
|
||||
|
||||
- 一个 upper half 可以服务**多个** lower half 实例(一对多)
|
||||
- **板级逻辑负责创建 lower-half 实例并把它与通用 upper half 绑定**
|
||||
- 网络驱动同构:接口在 `include/nuttx/net/netdev_lowerhalf.h`,upper half 为 `drivers/net/netdev_upperhalf.c`,注册用 `netdev_lower_register()` 一类接口
|
||||
- 驱动初始化分阶段:`drivers_early_initialize()`(堆/调度器可用之前)→ `drivers_initialize()`(注册核心伪设备如 `/dev/null`、`/dev/zero`)→ `board_app_initialize()`(经 `boardctl` 注册板级 lower half)
|
||||
|
||||
**所以**:
|
||||
|
||||
```
|
||||
open/read("/dev/foo")
|
||||
→ VFS → 通用 upper half → ops 回调 → 板级 lower half → 真正的 MMIO
|
||||
```
|
||||
|
||||
**只逆 `drivers/foo.c` 很可能找不到真正操作寄存器的地方。**
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 NuttX 目标
|
||||
├─ 先判构建模型:FLAT / PROTECTED / KERNEL
|
||||
│ ├─ 看是否存在用户 blob、MPU/MMU 使用、是否有一堆极短的 syscall 包装函数
|
||||
│ └─ 判错 → 后面地址与调用语义全错
|
||||
├─ 看到极短的 API 包装 + 立即 trap
|
||||
│ └─ PROTECTED 的自动生成 proxy(syscall.csv → mksyscalls),不是 hook
|
||||
├─ 内存相关异常
|
||||
│ ├─ 区分 malloc / kmalloc 与 user heap / kernel heap
|
||||
│ ├─ PROTECTED 下默认可能只有一个共享堆;启用 MM_KERNEL_HEAP 才有内核专用堆
|
||||
│ └─ 地址"用户不可访问"可能只是它来自内核堆
|
||||
├─ 找不到硬件操作代码
|
||||
│ └─ 走 upper half → lower half 两层:drivers/ 是通用层,真正 MMIO 在 arch/ 或 boards/
|
||||
└─ 时序/初始化相关
|
||||
└─ 分清 drivers_early_initialize / drivers_initialize / board_app_initialize
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 构建模型:是否存在用户 blob、MPU/MMU 配置、syscall proxy 与自动生成的 stub
|
||||
- 堆:`up_allocate_heap()` / `up_allocate_kheap()` 与相关 Kconfig 项
|
||||
- 驱动:upper half(`drivers/`,注册接口)与 lower half(`arch/`、`boards/`,硬件实现)的绑定关系
|
||||
- 验证:能同时说清「构建模型 + 该 API 走的是直接调用还是 syscall proxy + 地址属于哪个堆/哪个地址空间」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **不先判 FLAT/PROTECTED/KERNEL**:同一 API 的产物与地址语义完全不同
|
||||
- **把 syscall proxy 当真实实现**:PROTECTED 下用户侧 API 是自动生成的两三条汇编 + trap
|
||||
- **把 proxy 判成 hook/libc 异常**:这是设计本身
|
||||
- **忽略双堆**:用户不可访问的地址可能来自内核堆,不是堆损坏
|
||||
- **以为所有构建都有内核堆**:`MM_KERNEL_HEAP` 在 flat 下默认关闭
|
||||
- **只逆 `drivers/` 就找硬件操作**:真正 MMIO 通常在 `arch/` 或 `boards/` 的 lower half
|
||||
- **忽略板级绑定逻辑**:实例由板级代码创建并与通用 upper half 绑定
|
||||
- **混淆驱动初始化阶段**:early / drivers / board 三阶段可用的设施不同
|
||||
@@ -0,0 +1,137 @@
|
||||
# OSE / OSEck:信号模型 / hunt·attach / 池的所有权 / 异构边界
|
||||
|
||||
<CORE RULE>
|
||||
三条件同时成立才能读对 OSE:
|
||||
|
||||
```
|
||||
① 一切通信都是【信号】(消息),信号可以跨 core / CPU / DSP / 节点
|
||||
② 【信号号不是资源的唯一标识】——必须配合次级 id 才能路由
|
||||
③ 内存有【域/块/池】的所有权规则——谁能访问、谁能在哪里分配,是两件事
|
||||
```
|
||||
|
||||
所以:
|
||||
|
||||
```
|
||||
看到"signal" → 不要按 POSIX signal 理解
|
||||
看到整数 handle/信号号 → 先问它在哪个域、配合哪个 id
|
||||
```
|
||||
</CORE RULE>
|
||||
|
||||
> 版本/代际提示:OSE 家族有多个产品线(含紧凑版与更小的内核变体),它们**基于同一套原则但内核规模与能力不同**;分析前先确认目标属于哪一支。
|
||||
|
||||
## 一、信号模型
|
||||
|
||||
- OSE 用**事件("信号")**做通信——命名来自电信背景;**消息存放在附属于每个进程的队列里**
|
||||
- 信号 API 含 **hunt / attach / send / receive** 一类操作
|
||||
- **link handler** 机制让信号在**不同机器上的进程之间**传递,支持多种传输介质;这套机制后来成为开源跨机通信项目 LINX 的基础
|
||||
|
||||
## 二、关键坑:信号号不是唯一标识
|
||||
|
||||
**多个 hunt 端口可以共享同一个 hunt 信号号**——此时**必须靠次级 id**(例如端口 id)把信号路由到正确的接收者。
|
||||
|
||||
实现形态:按"信号号 + id"注册,并提供一个**解析函数**从信号的元数据/载荷里把 id 提取出来。
|
||||
|
||||
**逆向含义**:拿到"信号号"就想定位唯一接收者会错;先看有没有配套的 id 与解析逻辑。
|
||||
|
||||
## 三、两条时序与数据可用性约束
|
||||
|
||||
- **必须先注册监听、再发信号**——顺序反了,信号很可能**丢失**
|
||||
- **与 Unix 不同**:回调触发时,**信号载荷已经收到**了;要**反复取直到取空**(一次事件可能关联多个信号)
|
||||
|
||||
**所以**:"信号来了但取不到数据"在 Unix 直觉下像是丢事件,在这里通常是取法不对。
|
||||
|
||||
## 四、进程、块、段、池:内存所有权的硬核
|
||||
|
||||
- 执行单位是**进程**;进程可以组成**块(block)**,块可以有**自己的内存池**
|
||||
- **内核地址空间属于一个段(segment),段可包含一个或多个块**;把**块映射到段**、**池映射到区域**,就实现了内存保护与程序隔离
|
||||
- **进程类型**:中断进程、定时器进程、优先进程、后台进程;**类别**:静态(系统启动时创建)与动态
|
||||
|
||||
**信号缓冲区的所有权规则(高频踩点)**:
|
||||
|
||||
```
|
||||
通信进程可以访问【其域内所有池】里的信号缓冲区
|
||||
但只能【在自己的池里分配】
|
||||
跨域发送时,缓冲区会被【拷贝到接收方的池】
|
||||
```
|
||||
|
||||
**逆向含义**:缓冲区地址出现在哪个池,决定了它属于谁;"这个指针为什么在对方那里不可用"答案往往在这里。
|
||||
|
||||
## 五、监督与错误处理
|
||||
|
||||
- **链路级监督**由 LINX 提供;**平台级监督**用于检测**过载的 DSP** 与**失控的 DSP 崩溃**
|
||||
- **DSP 应用可以订阅监督信号**;可选配 DSP 看门狗作为额外手段
|
||||
- **运行时做大量检查**:参数、调用者、缓冲区与栈、初始化;检出错误时调用**错误处理器**——它定义了**错误恢复策略的骨架**
|
||||
- **内核错误类别**:内核自身检出的错误、系统调用参数错误、**内存破坏**
|
||||
- **能力边界要记清**(别把检不出当没问题):缓冲区与栈带**结尾标记**,因此部分系统调用能检出越界写;但**不能检出**"不改结尾标记的指针访问""内存泄漏""读未初始化值"——那类问题要靠外部工具
|
||||
- 另有"用户态代码受控切到 supervisor 模式"的途径
|
||||
|
||||
## 六、系统信息接口
|
||||
|
||||
可取到**常规系统调用拿不到的信息**,用于运行期调试、剖析与调优;其中含**信号组**:信号空间使用情况、信号空间分析、列出系统中所有信号、**以及保存的寄存器与栈内容**。
|
||||
|
||||
**逆向含义**:这是定位"这个信号是谁发的、当时寄存器/栈什么样"的现成入口。
|
||||
|
||||
## 七、OSE 与 OSEck 的差异
|
||||
|
||||
| | **OSE** | **OSEck(紧凑版)** |
|
||||
|---|---|---|
|
||||
| 定位 | 高性能、POSIX 兼容的多核 RTOS | **DSP 优化**的紧凑内核 |
|
||||
| 内存占用 | 常规 | **极小** |
|
||||
| 典型目标 | ARM / Power 系应用处理器 | DSP 类处理器(多核 DSP、DSP 核) |
|
||||
| 共同点 | 同一套信号/IPC/原则;**LINX 同时适用于 OSE、OSEck 与 Linux** | 同左 |
|
||||
|
||||
**含义**:跨 DSP 的系统里,**同一份信号语义在不同核上由不同精简程度的实例承载**;不要把某核上的能力假设到另一核。
|
||||
|
||||
## 八、异构边界
|
||||
|
||||
- 信号可跨 **core / CPU / DSP / 节点**传递;LINX 是**分布式透明传输层**,统一核内、处理器间、板间通信
|
||||
- 支持的介质包括**共享内存、DMA、RapidIO/串行互连、以太网、PCI** 等
|
||||
- 公开资料强调**零拷贝共享内存传输**;直接消息传递模型中**消息所有权不共享**,避免内存访问冲突
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 OSE / OSEck 目标
|
||||
├─ 先判:OSE 还是 OSEck?目标核是什么?
|
||||
├─ 看到"信号"
|
||||
│ ├─ 按消息对象解释,不是 POSIX signal
|
||||
│ ├─ 信号号不唯一 → 找次级 id 与解析函数
|
||||
│ └─ 可能跨 core / CPU / DSP / 节点
|
||||
├─ 通信对端找不到 / 数据取不到
|
||||
│ ├─ 是否先注册后发送(顺序反了会丢)
|
||||
│ └─ 是否把同一次事件关联的多个信号取空
|
||||
├─ 缓冲区相关异常
|
||||
│ ├─ 它属于哪个域/块/池?
|
||||
│ ├─ 分配只能在自己的池;跨域发送会被拷贝
|
||||
│ └─ 别把池的归属问题当内存损坏
|
||||
├─ 想看内部状态
|
||||
│ └─ 系统信息接口的信号组(信号空间、寄存器与栈内容)
|
||||
├─ DSP 相关异常
|
||||
│ ├─ 订阅监督信号;查是否过载/失控
|
||||
│ └─ 注意结尾标记能检出什么、检不出什么
|
||||
└─ 跨核行为不一致
|
||||
└─ 各核承载的内核变体能力不同
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 通信:信号号 + 次级 id 的注册与解析、hunt/attach 的使用点、队列归属
|
||||
- 内存:域/块/段/池的映射关系与缓冲区的池归属
|
||||
- 监督:链路级与平台级监督的配置、DSP 监督信号订阅、错误处理器
|
||||
- 运行期:系统信息接口(信号空间、寄存器与栈内容)
|
||||
- 验证:能同时说清「这条信号经哪个传输层到哪个域 + 缓冲区属于哪个池 + 目标核跑的是哪一支内核」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把消息或 "signal" 当 POSIX 信号**
|
||||
- **以为信号号可以唯一标识接收者**:多个端口可共享同一信号号,必须配合次级 id
|
||||
- **先发送后注册监听**:信号会丢
|
||||
- **只取一次载荷**:一次事件可能关联多个信号
|
||||
- **以为缓冲区可以任意分配**:只能在**自己的池**里分配,跨域发送会被拷贝
|
||||
- **把池/域的归属问题判成内存损坏**
|
||||
- **以为通信对端一定在本地**:传输层是分布式透明的
|
||||
- **在本地 syscall trace 里找跨节点通信**
|
||||
- **把结尾标记检不出问题当"没有内存问题"**:不改标记的越界、泄漏、未初始化读取都检不出
|
||||
- **把某一核的内核能力假设到另一核**:OSE 与 OSEck 能力不同
|
||||
- **断言其支持 XIP**:本次核验未证实,不要照搬
|
||||
- **忽略系统信息接口**:它是定位信号来源与现场的最快入口
|
||||
@@ -0,0 +1,208 @@
|
||||
# 分区 RTOS:INTEGRITY / ARINC 653 / VxWorks 653 / PikeOS
|
||||
|
||||
<CORE RULE>
|
||||
这类系统的调度**不止一层**:
|
||||
|
||||
```
|
||||
分区级调度(major frame / partition window / budget)
|
||||
↓
|
||||
分区内调度(进程/任务优先级,本分区自己的调度器)
|
||||
```
|
||||
|
||||
于是最常见的误判是:
|
||||
|
||||
```
|
||||
Task A priority = 250,Task B = 100
|
||||
B 在跑,A 完全没运行
|
||||
→ 不是"调度器坏了",也不是 starvation
|
||||
→ A 可能处在当前没有执行窗口/预算的分区里
|
||||
```
|
||||
|
||||
**"高优先级线程没运行"在这里首先是分区级的解释,然后才是任务级的解释。**
|
||||
</CORE RULE>
|
||||
|
||||
## 一、两层调度与 major frame
|
||||
|
||||
- **分区级**:固定、循环、离线定义的调度。分区在一个循环区间内被周期性释放一段时间
|
||||
- 这个区间叫 **major time frame(MAF)**,由若干 **partition window** 组成;每个窗口由**时长、偏移、周期性**定义
|
||||
- **激活顺序在配置表里静态定义,运行期不可更改**;设计良好时 **MAF 时长 ≈ 各分区周期的最小公倍数**
|
||||
- 分区调度表列出相对 MAF 起点的抢占点;调度就是"当前时钟落在某分区的窗口内(偏移 → 偏移+时长)"
|
||||
- **分区内**:由分区自己的调度器管理其进程(ARINC 653 语境下通常是固定优先级抢占式)
|
||||
|
||||
**分析要求**:时间线必须**先在分区级对齐**,再谈线程级。
|
||||
|
||||
## 二、分区模式:进程"不跑"常常是还没进 NORMAL
|
||||
|
||||
ARINC 653 的分区有四态:**IDLE / COLD_START / WARM_START / NORMAL**
|
||||
|
||||
| 模式 | 含义 |
|
||||
|---|---|
|
||||
| **COLD_START** | 初始化进行中;**抢占被锁定**;应用在跑初始化代码;初始环境可能需要**从非易失存储器把代码拷到 RAM** |
|
||||
| **WARM_START** | 也是初始化且抢占锁定,但初始环境与冷启动**可以不同**(例如不需要从 NVM 拷代码) |
|
||||
| **NORMAL** | 初始化完成,**分区的进程调度器活跃**,已创建的就绪进程可以运行 |
|
||||
| **IDLE** | 没有进程在执行,但分配给该分区的窗口仍保留 |
|
||||
|
||||
- 迁移关系:IDLE → COLD/WARM_START;COLD_START → NORMAL;NORMAL → COLD/WARM_START(重启);→ IDLE(关机/设为 IDLE)
|
||||
- 服务:`SET_PARTITION_MODE` 与 `GET_PARTITION_STATUS`(返回标识、周期、时长、锁层级、**运行模式**、启动条件)
|
||||
- **初始化模式(COLD/WARM_START)下只有主进程可调度**;错误处理程序在初始化阶段创建,并可调用 `SET_PARTITION_MODE` 重启分区
|
||||
|
||||
**所以**:
|
||||
|
||||
```
|
||||
CREATE_PROCESS() / START() 都做了,但进程一直不跑
|
||||
→ 先查是否执行了 SET_PARTITION_MODE(NORMAL)
|
||||
→ 而不是立即判调度器损坏
|
||||
```
|
||||
|
||||
## 三、采样端口与队列端口:语义不同,不能混
|
||||
|
||||
| | 采样端口(sampling) | 队列端口(queuing) |
|
||||
|---|---|---|
|
||||
| 缓冲 | **单条消息** | 可配置的消息条数 |
|
||||
| 覆盖 | **新消息覆盖旧消息**,应用总是读到**最新**的 | **不覆盖**,按 **FIFO** 传送 |
|
||||
| 丢消息 | **可能丢/被覆盖** | 不丢;队列满时发送失败/阻塞 |
|
||||
| 刷新周期 | **只对采样模式的接收有意义**(判断是否有新消息到达) | 不适用 |
|
||||
|
||||
- channel 把一个源端口连到一个或多个目的端口;分区之间由内核强制内存保护,其他分区无法访问本分区的端口
|
||||
|
||||
**所以**:
|
||||
|
||||
```
|
||||
连续写了 A B C,最后只读到 C
|
||||
→ 对采样端口可能完全正常
|
||||
→ 对队列端口则不正常
|
||||
```
|
||||
|
||||
## 四、INTEGRITY:空间 + 时间 + 资源三域分区
|
||||
|
||||
- 微内核架构,支持**多个受保护的虚拟地址空间**,每个可含**多个应用任务**;用硬件 MMU 建立隔离
|
||||
- **完整的分区包含空间、时间与资源三个域**;失败不会跨分区传播
|
||||
- **固定预算**:可为每个进程分配**固定的 CPU 时间与内存预算**;保证时间窗也意味着**运行中的任务不会超出其窗口**
|
||||
- 其他机制:实时调度器支持多优先级与**完整的 CPU 百分比分配控制**;内核服务时间有界;**内核空间不做动态内存分配**;内置 **ARINC 653 Part 1 分区调度器**(带保证的执行时间窗);**内存配额系统**防止一个地址空间耗尽另一个的内存;**内核内存不用于消息、信号量等由进程请求创建的对象**
|
||||
- **静态配置**:架构师预先写好静态配置文件,定义分区、任务集、内存对象、链接、连接通道、时钟与分区调度机制;**启动表定义归属**;**连接静态分配且不可绕过**;MMU 设置可静态验证
|
||||
- 多核:不同分区时间窗内可绑定到不同核;**共享资源争用可使 WCET 增加 8–13 倍**(这也是相关适航指导强调的)
|
||||
|
||||
**RE 主线**:
|
||||
|
||||
```
|
||||
CPU 明明还有空闲,应用却拿不到更多执行时间
|
||||
→ 可能是预算隔离(设计如此),不是调度效率 bug
|
||||
```
|
||||
|
||||
## 五、PikeOS:还有第三层
|
||||
|
||||
PikeOS 是第二代分离微内核(同时是 hypervisor):**静态循环调度分区**(时间隔离)+ 内存区(空间隔离);每个分区有自己的调度结构,**支持 256 个优先级**;**系统分区与当前活动分区同时被调度**以提供系统服务。
|
||||
|
||||
POSIX guest 的层次:
|
||||
|
||||
```
|
||||
PikeOS 分区/资源调度器
|
||||
↓
|
||||
PikeOS system thread(每核一个可供该 POSIX 内核使用)
|
||||
↓
|
||||
POSIX guest 内核自己的调度器
|
||||
↓
|
||||
POSIX 用户线程
|
||||
```
|
||||
|
||||
- POSIX 内核**自己的调度器把它的用户线程映射到一个 PikeOS system thread**;许多 POSIX 驱动还会自建 I/O 线程
|
||||
- POSIX guest 实现的是 PSE52 子集,加上实时与 PikeOS 专有扩展(**包含 ARINC 653 采样/队列端口**)
|
||||
- **APEX guest**:完整实现 ARINC 653 P1-5 与部分 P2-4(分区管理、健康监控、采样/队列端口、进程管理、分区内通信与同步、logbook、文件系统、内存块、服务访问点端口)
|
||||
- **APEX 服务经一张链接在固定地址的 API table 进入**:开发时应用与库及链接脚本给出的 API 服务表地址链接
|
||||
- 初始化完成后 APEX 内核把控制交给用户提供的 `MAIN()`,**起始于 COLD_START**(抢占锁定、创建静态资源),之后分区转入 **NORMAL**(进程调度器活跃、完整 APEX API 可用)
|
||||
|
||||
**两条推论**:
|
||||
|
||||
```
|
||||
guest 内线程是最高优先级,但延迟突然到 10 ms
|
||||
→ 不能只看 pthread 调度器:查当前分区是否有执行窗口、
|
||||
对应 system thread 是否可运行、CPU affinity/分区调度是什么
|
||||
|
||||
看到大量 ldr x16, [固定基址+偏移]; blr x16
|
||||
→ 先查是否是 APEX 服务表,而不是控制流混淆
|
||||
```
|
||||
|
||||
## 六、Deos:两级调度 + slack + SafeMC
|
||||
|
||||
- 时间/空间/资源分区的安全关键 RTOS(DAL A 认证):分区允许把**不同设计保证等级**的软件混跑在同一处理器上,并提供故障隔离与应用重启
|
||||
- **两级调度模型**(SafeMC 明确描述):
|
||||
|
||||
```
|
||||
Level 1:时间被划分为在各核之间【对齐】的执行窗口
|
||||
Level 2:每个核/window 指派具体 scheduler
|
||||
├─ ARINC 653
|
||||
├─ Deos RMA(速率单调)
|
||||
└─ POSIX
|
||||
应用被指派给一个或多个 scheduler;每个 scheduler 可跨多个 process/partition 调度多个 task
|
||||
```
|
||||
|
||||
- **同一颗核在不同时间段可以跑不同调度规律**(window 1 用 ARINC 653、window 2 用 POSIX、window 3 用 RMA 是这套架构能表达的)——**trace 里调度规律切换不能自动判调度器状态损坏**
|
||||
- **Slack Scheduling(滑动时间窗)**:窗口可以**压缩、扩张、滑动**——分区提前完成可压缩窗口,需要更多时间且有空闲可扩张,也可以让他人提前或延后开始。**所以"budget 给了 10 ms、任务只用了 6 ms,另一个任务拿到剩余 CPU"不是时间分区逃逸**,而是 slack 回收;**CPU 接近 100% 也可以仍在预算内**
|
||||
- **Cache partitioning**:**软件实现、可配置**,把缓存隔离做到应用/分区级(不只是核级),**免去分区切换时的 cache flush**(那会影响 WCET),并与**内存池(memory pooling)**互补(控制应用落在物理内存的哪里)
|
||||
- 开发工具本身也可能**利用 slack time** 做通信/profiling/monitoring 以少干扰关键负载——trace 里"应用执行 → 一小段工具流量 → 应用执行"先确认是不是这个
|
||||
- 多核用 **BMP(SMP 的扩展)+ 安全调度 + 缓存分区 + 内存池** 限制共享资源干扰。**任务 READY、CPU 也有,但执行被多核策略约束**,不一定是普通调度决策——要恢复 core assignment / window / cache partition / memory pool / 跨核干扰策略
|
||||
- 也可以承载其他环境:例如某 para-virtualized RTOS 实现运行在它的安全分区里,对外提供 POSIX 接口——**看到 `pthread_*` 不代表脱离了 Deos 分区控制**
|
||||
- 两个不同安全分区**能看到共享内存**也不等于隔离被破坏:应查是否配置为共享、权限、谁可写、生命周期
|
||||
- 若观测到 major frame 起点**缓慢漂移**:Deos 提供外部时钟同步类能力(本次核验未直接命中上游资料,**作为候选解释之一并在具体型号文档上确认**),不要直接判 timer drift 或 trace 时钟坏了
|
||||
|
||||
**排错顺序(重要)**:
|
||||
|
||||
```
|
||||
Task 没运行
|
||||
→ 属于哪个 process/partition?
|
||||
→ 在哪个 core?
|
||||
→ 当前 window 是否 active?
|
||||
→ window 内是什么 scheduler?
|
||||
→ 本地优先级/状态?
|
||||
→ budget?
|
||||
→ SafeMC 限制?
|
||||
→ 才看 deadlock/bug
|
||||
```
|
||||
|
||||
**对这类系统,配置与分区调度表优先于反汇编猜内部调度器**——公开文档未必开放全部内核细节。
|
||||
|
||||
## 同族系统
|
||||
|
||||
- XtratuM(配置驱动的分离 hypervisor):同样是"静态配置 + major frame + 时间槽",另有固定优先级计划与多计划切换,见 [[re-hypervisor/xtratum]]
|
||||
- LynxSecure(静态分离内核):见 [[re-hypervisor/lynxsecure]]
|
||||
- Quest-V(多内核 sandbox + 每 sandbox 一个 monitor):见 [[re-hypervisor/questv]]
|
||||
- Jailhouse / Bao(静态分区 hypervisor):见 [[re-hypervisor/jailhouse]]、[[re-hypervisor/bao]]
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到分区 RTOS 目标
|
||||
├─ 先分层:分区级调度 vs 分区内调度
|
||||
│ ├─ 对齐 major frame / partition window(时长、偏移、周期)
|
||||
│ └─ 再谈进程/任务优先级
|
||||
├─ 高优先级线程不运行
|
||||
│ ├─ 查它所属分区当前是否有执行窗口
|
||||
│ ├─ 查分区模式(是否还在 COLD/WARM_START,未进 NORMAL)
|
||||
│ └─ 查资源/时间预算是否已耗尽(故意的隔离)
|
||||
├─ 通信异常
|
||||
│ ├─ 采样端口:覆盖语义,"只读到最新"属正常
|
||||
│ └─ 队列端口:FIFO、不覆盖,满则失败/阻塞
|
||||
├─ 看到固定地址函数表
|
||||
│ └─ 先考虑 APEX 服务表一类的 ABI,不要直接判混淆
|
||||
└─ 多核 / 性能归因
|
||||
└─ 把共享资源争用与预算隔离算进去(WCET 可能显著放大)
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 调度:分区调度表 / 时间窗配置、MAF 与窗口参数、分区模式状态
|
||||
- 资源:分区/进程的 CPU 时间与内存配额、静态配置文件与启动表
|
||||
- 通信:端口类型(采样/队列)与各自参数
|
||||
- 验证:能同时说清「分区级时间窗 + 分区内调度结果 + 预算/模式状态」,三者缺一不能解释"为什么没跑"
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **用统一的优先级队列解释调度**:至少两层(PikeOS 场景是三层)
|
||||
- **把"高优先级线程不跑"当 starvation 或调度器 bug**:先看分区窗口与预算
|
||||
- **忽略分区模式**:COLD/WARM_START 下只有主进程可调度,进程当然"不跑"
|
||||
- **把采样端口当队列端口**:覆盖语义下"只读到最新"是正常的
|
||||
- **把队列端口当采样端口**:消息不会互相覆盖,丢失就一定是问题
|
||||
- **把资源/时间预算隔离当性能缺陷**:固定预算是隔离机制的一部分
|
||||
- **只逆 guest 内容**:PikeOS 一类系统里 guest 调度器不是最终调度器
|
||||
- **把固定地址的 APEX 服务表当控制流混淆**
|
||||
- **多核分析忽略共享资源争用**:WCET 可能成倍放大
|
||||
141
vendor-new/rev-skills/.claude/skills/re-rtos/references/qnx.md
Normal file
141
vendor-new/rev-skills/.claude/skills/re-rtos/references/qnx.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# QNX Neutrino:资源管理器 / 消息传递 / io-pkt
|
||||
|
||||
<CORE RULE>
|
||||
QNX 的"驱动"大量是**用户态 server**,不是内核模块。
|
||||
|
||||
```
|
||||
open("/dev/foo")
|
||||
→ pathname resolution(进程管理器)
|
||||
→ resource-manager 进程
|
||||
→ MsgSend / MsgReceive
|
||||
→ io_open handler
|
||||
```
|
||||
|
||||
而不是:
|
||||
|
||||
```
|
||||
syscall → VFS → kernel driver file_operations
|
||||
```
|
||||
|
||||
所以见到一个进程:**没有 ioctl 的 syscall handler、没有内核模块、却控制硬件**——这在 QNX 上不奇怪,是常态。
|
||||
|
||||
第一条经验不是"先反汇编内核",而是**先判形态**:这份代码是 resource manager、是 io-pkt 加载的共享对象,还是真在内核态。判错一种,后面全部推理作废。
|
||||
</CORE RULE>
|
||||
|
||||
## 一、先判形态(决定后面全部)
|
||||
|
||||
| 形态 | 表现 | 分析入口 |
|
||||
|---|---|---|
|
||||
| **resource manager 进程** | 独立进程,注册了 pathname,循环收消息 | `resmgr_attach` 的 path + connect/io 函数表 |
|
||||
| **io-pkt 加载的网络驱动** | `devnp-*.so` 共享对象,**既不是独立进程也不是内核模块** | 被 `io-pkt -d <名>` 加载;驱动回调在 io-pkt 内执行 |
|
||||
| **内核态(procnto)** | 微内核 + 进程管理器一体 | IFS 镜像 → `dumpifs` 解出 procnto 再分析 |
|
||||
|
||||
## 二、resource manager:认 API 序列,不认工具链习惯
|
||||
|
||||
一个典型 resource manager 的骨架是固定序列:
|
||||
|
||||
```
|
||||
dispatch_create() / dispatch_create_channel(-1, DISPATCH_FLAG_NOLOCK)
|
||||
→ resmgr_attr_t(nparts_max / msg_max_size)
|
||||
→ iofunc_func_init() # 载入默认 POSIX handler 表
|
||||
→ iofunc_attr_init() # 每设备属性
|
||||
→ resmgr_attach(dpp, &attr, path, file_type, flags, &connect_funcs, &io_funcs, &handle)
|
||||
→ dispatch_context_alloc()
|
||||
→ 循环:dispatch_block() → dispatch_handler()
|
||||
```
|
||||
|
||||
- 张量表分两张:**connect 函数表**(`open` / `unlink` / `rename` / `mknod` / `readlink` / `link` / `unblock` / `mount`)与 **I/O 函数表**(`read` / `write` / `devctl` / `stat` / `lseek` …)
|
||||
- `resmgr_attach()` 一次做完三件事:把 pathname 注册进命名空间、创建收消息用的 channel、把 dispatch handle 与函数表绑起来
|
||||
- **收消息的底层就是 `MsgReceive`/`MsgReceivev`**——`resmgr_context_t` 里的 `rcvid` 就来自它;`dispatch_block()` 阻塞等消息,`dispatch_handler()` 解析并按表分发
|
||||
- **`iofunc_func_init()` 给的是可用的默认实现**——很多 handler 是默认的,不是每个设备都要重写 `stat`/`chmod` 那一套;只有被显式覆盖的(例如 `io_funcs.read = io_read;`)才是该设备自己的逻辑
|
||||
- 多线程版本:`thread_pool_attr_t`(`block_func = dispatch_block`、`unblock_func = dispatch_unblock`、`handler_func = dispatch_handler`、`context_alloc/free`)+ `lo_water`/`hi_water`/`increment`/`maximum` → `thread_pool_create()` / `thread_pool_start()`。**`lo_water` 就是"保持多少个线程阻塞在 RECEIVE 上"**
|
||||
- 私有消息走 `message_attach()`,此时**用 `dispatch_*()` 而不是 `resmgr_*()`**
|
||||
|
||||
**反推要点**:拿到一个进程后,先找 `resmgr_attach` 的 path 参数——**注册了什么 pathname 就是它的身份**;再找被覆盖的表项——**没被覆盖的走默认语义,被覆盖的才是业务**。
|
||||
|
||||
## 三、控制通道是 devctl,不是 ioctl
|
||||
|
||||
- 控制路径是 I/O 函数表里的 `devctl`(`io_devctl_t`),命令码用 `_IO_DEVCTL`;自定义消息用 `_IO_DEVCTL` 或 `_IO_MSG`
|
||||
- 老式的 `other_func` 已被明确不推荐——**在旧样本里见到它说明代码年代较早**,别把它当"主要协议入口"
|
||||
- 恢复顺序:`devctl` 命令码 → 参数结构 → 对应的硬件/共享内存操作;命令码常量常在二进制的只读数据里成组出现
|
||||
|
||||
## 四、io-pkt:网络驱动的第三种形态
|
||||
|
||||
- io-pkt 是**多线程进程**,内含网络栈与 resource manager 分发器;**栈上下文(stack context)是单线程的**(以伪线程处理阻塞)
|
||||
- **阻塞栈上下文 = 阻塞整个 io-pkt 的 resource manager**:`ifconfig` 卡住常常就是栈上下文被阻塞,不是 ifconfig 本身的问题
|
||||
- 原生网络驱动是 **`devnp-*.so` 共享对象**:`io-pkt -d some_driver` 会去找 `devnp-some_driver.so` 并把它加载为原生驱动;找不到时回退尝试 `devn-some_driver.so`(经 **`devnp-shim.so`** 兼容层)
|
||||
- **`devnp-shim.so`** 提供对 io-net 时代 `devn-*` 驱动的向后兼容(通常自动加载;也可显式 `io-pkt -d shim "/lib/dll/devn-speedo.so"`);经 shim 加载的驱动接口名形如 `enX`;用 `pidin me` 确认加载结果
|
||||
- 驱动内建线程**不要用裸 `pthread_create()`**,用 `nw_pthread_create()`(由 io-pkt 跟踪,并要求实现 quiesce 回调);定时器用 `callout_*`
|
||||
|
||||
**RE 含义**:一个 NIC 驱动可能既没有独立进程也没有模块文件——它只是**某个 `devnp-*.so`**。要分析它,得先确认"谁在加载它、加载进哪个进程"。
|
||||
|
||||
## 五、优先级继承:trace 里优先级突变 ≠ 调了 `pthread_setschedparam`
|
||||
|
||||
- QNX 的 Send/Receive/Reply 带**消息驱动的优先级继承**:server 线程**继承发送者的优先级**,请求按优先级顺序被接收
|
||||
- 提升可能发生在**发送时刻**(不等到 server 收到)——这是为了避免"中等优先级线程卡住高优先级客户端的消息投递"这一 inversions
|
||||
- **server 回复后优先级不自动恢复**:一直保持,直到被新的消息/pulse 抬高或压低,或被显式改动
|
||||
- `ChannelCreate()` 的 **`_NTO_CHF_FIXED_PRIORITY`** 可关闭这一继承
|
||||
- **server boost**:当 channel 上没有 RECEIVE-blocked 线程(客户端被 SEND-blocked)时,内核会**提升该 channel 关联的某个 server 线程**;其原始优先级在首次 boost 时被记录,下次收到消息时内核重新评估
|
||||
|
||||
所以:**线程优先级在采样里突然变化,先当作正常的 IPC 继承**;对应地,"一个设计良好的多线程 server 应当始终有至少一个 RECEIVE-blocked 线程"。
|
||||
|
||||
## 六、"长时间阻塞"在 QNX 经常是正常状态
|
||||
|
||||
- `MsgSend()` 是**同步**的:发送线程进入 `STATE_SEND`(已发送未被接收)或 `STATE_REPLY`(已被接收未回复),直到 `MsgReply*()`、信号、超时、server 死亡等才解除
|
||||
- **pulse** 是另一类东西:**固定大小、非阻塞**,载荷是 4 字节数据 + 1 字节 code;用于中断处理通知或 server 无需阻塞地通知客户端;同样按优先级排队并被接收
|
||||
- 注意:在自适应分区下,**pulse 不继承发送者的分区**(Send/Receive/Reply 会)——分区的观测差异不要当成调度 bug
|
||||
|
||||
因此:**"线程卡在 IPC"、"server 里看到一堆 RECEIVE-blocked 线程"都不是 deadlock 证据**,除非能同时给出阻塞对象(谁该 reply 却没 reply)。
|
||||
|
||||
## 七、分析主线
|
||||
|
||||
```
|
||||
pathname
|
||||
→ resource manager(哪个进程注册的)
|
||||
→ channel / connection(谁连谁)
|
||||
→ 消息类型(_IO_* / 私有 message / pulse)
|
||||
→ handler(connect 表 / io 表 / devctl 命令码)
|
||||
→ 硬件 / 共享内存 / 另一个 server
|
||||
```
|
||||
|
||||
对多 server 系统,这条链可以继续接下去:**QNX 的"驱动栈"常常是一串 server**(例如文件系统、块设备、网络各是一个 server),而不是内核里的一条调用链。
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 QNX 目标
|
||||
├─ 先判形态
|
||||
│ ├─ 有 resmgr_attach / dispatch_* 序列 → 用户态 resource manager
|
||||
│ │ ├─ resmgr_attach 的 path 是什么(即"设备名")
|
||||
│ │ ├─ 哪些 connect/io 表项被覆盖(其余走 iofunc 默认)
|
||||
│ │ ├─ devctl 命令码 → 控制路径
|
||||
│ │ └─ channel/connection:谁在 MsgSend 它
|
||||
│ ├─ 是 devnp-*.so → 找加载它的 io-pkt,判是原生驱动还是经 devnp-shim 的旧 devn-*
|
||||
│ └─ 真在内核态 → IFS 镜像(startup 头 0x00ff7eeb + "imagefs")→ dumpifs 解出 procnto
|
||||
├─ 动态观测异常
|
||||
│ ├─ 优先级突然变化 → 优先当 IPC 优先级继承 / server boost,而不是主动改调度
|
||||
│ ├─ 线程长时间阻塞 → 优先当 MsgSend 同步等待,而不是死锁
|
||||
│ └─ 栈上下文被阻塞 → 整个 io-pkt 的 resource manager 一起卡
|
||||
└─ 多 server 系统 → 沿 pathname→channel→server 继续走链,别指望在内核里找到完整调用链
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 线程/进程:`pidin`(含 `pidin me` 确认某个进程里加载了什么)、`/proc/<pid>/ctl` 的 `DCMD_PROC_TIDSTATUS` 一类侧信道读线程状态
|
||||
- 命名空间:查进程注册了什么 pathname、谁打开了它——**把 pathname 当作跨进程的锚点**
|
||||
- 静态:先按 API 序列识别形态(`resmgr_attach` / `dispatch_*` / `MsgReceive` / `io_*`),再谈逻辑;不要从 `_start` 顺着啃
|
||||
- 网络:确认驱动是被哪个 io-pkt 加载、是 `devnp-*` 还是经 shim 的 `devn-*`
|
||||
- 验证:能同时说清「形态(用户态 server / io-pkt 共享对象 / 内核态)+ 注册的 pathname 或加载者 + 消息与命令码路径」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **拿 Linux 驱动思维找"驱动"**:QNX 上控制硬件的进程可能既没有内核模块也没有 ioctl handler——它是用户态 resource manager
|
||||
- **把 `iofunc_*` 默认实现当业务逻辑**:`iofunc_func_init()` 填了一整套可用默认值,**只有被显式覆盖的表项才是这个设备的逻辑**
|
||||
- **在 trace 里把 IPC 优先级继承当成主动调调度**:server 继承 sender 优先级,且在 send 时刻就可能提升、回复后不自动恢复;`_NTO_CHF_FIXED_PRIORITY` 是关闭开关
|
||||
- **把 MsgSend 的同步阻塞当死锁**:发送线程本来就会一直等到 reply;pulse 才是非阻塞的那一类
|
||||
- **忘了 pulse 与消息在自适应分区上的差异**:pulse 不继承发送者分区
|
||||
- **在进程列表里找不到某个网络驱动**:它可能是被 io-pkt 加载的 `devnp-*.so`(既不独立成进程也不是模块)
|
||||
- **把 `devn-*.so` 当原生驱动**:那是 io-net 时代的形态,经 `devnp-shim.so` 兼容加载,接口命名也不同(`enX`)
|
||||
- **以为阻塞某个驱动回调没关系**:驱动回调可能跑在 io-pkt 的**单线程栈上下文**里,阻塞它等于阻塞整个网络 resource manager
|
||||
- **在驱动里看到裸 `pthread_create` 就当普通线程分析**:io-pkt 要求用 `nw_pthread_create()` 并配 quiesce 回调
|
||||
- **把 `other_func` 当协议主入口**:该机制已被明确不推荐,通常是老代码
|
||||
109
vendor-new/rev-skills/.claude/skills/re-rtos/references/rtems.md
Normal file
109
vendor-new/rev-skills/.claude/skills/re-rtos/references/rtems.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# RTEMS:单地址空间 / 构建期配置 / 运行时链接器
|
||||
|
||||
<CORE RULE>
|
||||
RTEMS 的三大特殊性:
|
||||
|
||||
| 特殊性 | 后果 |
|
||||
|---|---|
|
||||
| **单地址空间** | OS、库、应用可视为**一个进程**;没有"每个进程各自一套映射"这回事 |
|
||||
| **大量结构在构建期由配置宏生成** | 任务数、调度器、驱动、时钟节拍、初始化任务来自 `confdefs.h` 的 `CONFIGURE_*` 宏 |
|
||||
| **`dlopen()` 不是 Unix 动态库模型** | 运行时链接器更像**把可重定位目标链接进当前系统地址空间**,不是共享库装载 |
|
||||
|
||||
第三条尤其容易误导——**看到 `dlopen` 就套 ELF `DT_NEEDED` / 共享库命名空间那套,会得到一整套错误结论**。
|
||||
</CORE RULE>
|
||||
|
||||
## 一、单地址空间意味着什么
|
||||
|
||||
- 没有运行期代码共享的余地("在 RTEMS 上没有东西需要共享"),所以**加载代码更接近静态绝对链接过程**,而不是 Unix 共享库
|
||||
- 可加载对象按**位置无关代码**编译,装载时**重定位进目标地址空间**,地址在**装载时固定**
|
||||
- **执行性能与静态链接的同份代码相近**——不要用"动态库调用有 PLT/GOT 开销"这类先验去解释性能观测
|
||||
- 基镜像(reset 后运行的静态链接可执行文件)包含:基础应用、**全局符号表**、被引用到的 OS/库代码
|
||||
|
||||
## 二、`confdefs.h`:配置即生成
|
||||
|
||||
`<rtems/confdefs.h>` 是**配置表模板**,由一组 `CONFIGURE_*` 宏实例化:
|
||||
|
||||
- **`CONFIGURE_INIT` 必须且只能在唯一一个源文件里定义**(否则配置数据结构被实例化多次 → 链接错误)。这是一条很好用的判据:**找配置实例化点,就是找那个定义 `CONFIGURE_INIT` 的翻译单元**
|
||||
- 它的工作方式是**估算**每项配置所需内存并求和,用来算出 `work_space_size`——**估算可能偏高也可能偏低**(例如设备驱动的要求与附加库的资源并不计入)
|
||||
- 设备驱动数量由**设备驱动表的表项数**自动算出:该表**按约定命名为 `Device_drivers`**;应用若要自带驱动表,必须定义 `CONFIGURE_HAS_OWN_DEVICE_DRIVER_TABLE`
|
||||
- 相关的容量宏:最大驱动数 / 最大设备数;控制台、时钟、定时器、RTC、stub 等驱动通过 `CONFIGURE_APPLICATION_NEEDS_*_DRIVER` 引入
|
||||
- **网络驱动不配在设备驱动表里**——"驱动表里没有网络驱动"不是缺件
|
||||
|
||||
## 三、设备模型:driver address table + major/minor
|
||||
|
||||
核心结构是 `rtems_driver_address_table` 数组,每个表项**六个入口**:
|
||||
|
||||
```
|
||||
initialization_entry open_entry close_entry read_entry write_entry control_entry
|
||||
```
|
||||
|
||||
- **major 号就是这张表的索引**;**minor 号**标识同一设备类内的具体设备(同一个驱动或不同驱动可以服务 minor 0/1)
|
||||
- `rtems_io_register_name(name, major, minor)` 把 `(major, minor)` 与文件名关联(例如 `/dev/ttyS0`),通常在 minor 级初始化时调用
|
||||
- 启动路径:`boot_card` → `rtems_initialize_device_drivers` → `_IO_Initialize_all_drivers` → `rtems_io_initialize` → 各驱动的 `initialization_entry`
|
||||
- 表中以 `NULL_DRIVER_TABLE_ENTRY` 形式的空项作占位/兜底
|
||||
|
||||
**因此要恢复的是**:
|
||||
|
||||
```
|
||||
Device Driver Table → major(表索引)→ 回调函数
|
||||
```
|
||||
|
||||
**而不是** Linux 的 `file_operations`。I/O 管理器按驱动表分派,**major 号是"表下标"这一层语义,在 Linux 侧根本不存在**。
|
||||
|
||||
## 四、`dlopen()` 的特殊语义(逐条都有分析后果)
|
||||
|
||||
运行时链接器在目标上运行,经 `<dlfcn.h>` 提供 `dlopen` / `dlclose` / `dlsym` / `dlinfo` / `dlerror`,可装载 **ELF 可重定位目标文件**并解析其外部函数/数据符号。但:
|
||||
|
||||
- **目标文件可以是独立文件,也可以是静态库(archive)的成员**,用 RTEMS 自己的编码:**`libfoo.a:bar.o`**,成员名后可选 `@offset`(给出对象在 archive 中的绝对偏移);带偏移可省去在 archive 中查找的开销。**这套编码是 RTEMS 特有的**——按 Unix 共享库的命名习惯无法解释它
|
||||
- **`RTLD_LAZY` 与 `RTLD_NOW` 行为相同:`dlopen` 返回之前全部重定位已完成**。所以"懒绑定会在首次调用时才解析"的推断在这里不成立
|
||||
- **未解析符号不会让 `dlopen` 失败**(这样互相依赖的对象可以成对加载);未解析项可由 `dlinfo(handle, RTLD_DI_UNRESOLVED, ...)` 读出。因此 **`dlopen` 成功 ≠ 该对象的所有符号都已就绪**
|
||||
- **与基镜像或此前已加载对象重名的符号被视为错误,该对象不会被加载**;**不支持符号版本**
|
||||
- 重复 `dlopen` 同一对象只增加引用计数,**地址空间中只保留一份副本**(即使路径名不同)
|
||||
- `dlopen(NULL, ...)` 返回覆盖"基镜像 + 以 `RTLD_GLOBAL` 加载的对象及其依赖"的全局符号表句柄
|
||||
- `dlclose` 引用计数归零后卸载;**若其提供的符号仍被其他常驻对象引用则无法卸载**;未被显式加载且无人引用的对象会被自动移除。初始化函数按**加载顺序**在 `dlopen` 返回前运行
|
||||
- **基镜像本身没有符号表**:需用 `rtems-syms` 从基镜像提取全局/弱符号,生成符号表对象,然后**嵌入基镜像(需链接两次)或在运行期先行加载**。**符号表必须与基镜像匹配,且不做校验**——用错符号表是静默错误
|
||||
- 加载对象必须是**目标架构、与基镜像相同的 ABI 标志**,且**与 RTEMS 版本绑定**;`rtems-execinfo` 可报出构建标志
|
||||
- **动态加载不被视为实时活动**:应在系统稳定时进行,而不是响应实时事件时;其内存开销高于静态链接,装载地址取决于加载顺序与分配器
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 RTEMS 目标
|
||||
├─ 先确认:单地址空间(OS/库/应用同地址空间)
|
||||
│ └─ 不要按"每进程独立映射"的思路解释地址或可见性
|
||||
├─ 找配置
|
||||
│ ├─ 找定义 CONFIGURE_INIT 的那一个翻译单元 → 配置实例化点
|
||||
│ ├─ 由 CONFIGURE_* 宏还原:任务数、调度器、时钟节拍、初始化任务、驱动容量
|
||||
│ └─ 驱动表按约定名 Device_drivers(或自定义时定义了 CONFIGURE_HAS_OWN_DEVICE_DRIVER_TABLE)
|
||||
├─ 找设备
|
||||
│ ├─ 走 rtems_driver_address_table:major = 表索引 → 六个回调
|
||||
│ ├─ 文件名关联看 rtems_io_register_name(name, major, minor)
|
||||
│ └─ 别套 Linux file_operations;网络驱动不在驱动表里
|
||||
└─ 见到 dlopen
|
||||
├─ 别套 Unix 共享库模型:它是"重定位进当前地址空间",接近静态绝对链接
|
||||
├─ 对象可能是 archive 成员(libfoo.a:bar.o[@offset])
|
||||
├─ 懒/立即绑定无区别:返回前重定位已完成
|
||||
├─ dlopen 成功 ≠ 符号就绪(未解析不报错,需 dlinfo 查)
|
||||
└─ 基镜像无符号表 → 需 rtems-syms 生成且必须与该镜像匹配(不校验)
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 配置:`confdefs.h` 及其被实例化的位置(`CONFIGURE_INIT` 所在文件)
|
||||
- 设备:设备驱动表(`Device_drivers` 约定名)、`major`/`minor` 与 `rtems_io_register_name` 的关联
|
||||
- 可加载对象:`rtems-syms`(生成与基镜像匹配的符号表)、`rtems-execinfo`(查看 ELF 的构建标志)
|
||||
- 运行期:`dlinfo` 的未解析重定位报告(`RTLD_DI_UNRESOLVED`)
|
||||
- 验证:能同时说清「配置来源(哪个宏/哪张表)+ 设备分派路径(major 索引)+ 可加载对象的符号来源与匹配关系」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把 `dlopen()` 理解成 Unix 动态库装载**:它是把可重定位目标重定位进当前地址空间,更接近静态链接
|
||||
- **拿共享库命名空间、`DT_NEEDED`、符号版本那套解释行为**:RTEMS 不支持符号版本,重名符号直接判错
|
||||
- **以为懒绑定会推迟解析**:`RTLD_LAZY` 与 `RTLD_NOW` 行为相同,返回前全部重定位完成
|
||||
- **把 `dlopen` 成功当作符号全部就绪**:未解析符号不会导致失败,要另行查询
|
||||
- **看不懂 `libfoo.a:bar.o` 这类对象名**:那是 RTEMS 自己的 archive 成员编码(可带偏移)
|
||||
- **用 Linux `file_operations` 模型找设备回调**:应恢复设备驱动表与 major(表索引)/minor
|
||||
- **在设备驱动表里找网络驱动**:网络驱动不配在这张表里
|
||||
- **忽略 `CONFIGURE_INIT` 唯一性**:它必须只在一个源文件中定义,这也是定位配置实例化点的线索
|
||||
- **拿配置估算值当精确内存需求**:`confdefs.h` 是估算,且设备驱动与附加库资源不在其计算内
|
||||
- **以为基镜像自带符号表**:静态链接的基镜像没有,需要 `rtems-syms` 生成并保证匹配(不匹配也不会报错)
|
||||
@@ -0,0 +1,120 @@
|
||||
# 认证 RTOS 的隔离特性:SAFERTOS ESM / CMSIS-RTX5 安全特性
|
||||
|
||||
<CORE RULE>
|
||||
这一族 RTOS 在"功能核 + 认证"之上,额外加了**访问控制与诊断特性**,会产生两类误判:
|
||||
|
||||
```
|
||||
同一 VA:Task A 能访问、Task B 不能
|
||||
→ 可能只是 per-task 区域/保护域策略
|
||||
|
||||
某 API 调用失败 / 某线程不运行
|
||||
→ 可能只是调用方安全等级不够
|
||||
```
|
||||
|
||||
**先在"谁能访问什么"这一层解释,再去怀疑对象损坏或逻辑错误。**
|
||||
</CORE RULE>
|
||||
|
||||
## 一、SAFERTOS + Enhanced Security Module(ESM)
|
||||
|
||||
基础 SAFERTOS 的空间隔离目标是**非故意的内存访问**(损坏/编程错误);**ESM 把它加固到抵御"蓄意的"越权访问**。
|
||||
|
||||
| 机制 | 作用 |
|
||||
|---|---|
|
||||
| **访问控制策略(ACP)** | **限制每个任务可访问的 API 数量**(例如禁止某任务改自己的特权级、改访问权限、创建新资源);任务越受限,被攻陷后的攻击面越小 |
|
||||
| **对象访问控制策略(OACP)** | **限制每个任务可访问的 RTOS 对象**(队列、信号量、事件组等),把任务约束在初始化时分配给它的对象上 |
|
||||
| **间接对象 ID** | 把传统句柄(**本质是 C 指针**)换成**经交叉引用表解析的 Object ID**,使任务无法从句柄推出对象/TCB 的内存位置 |
|
||||
| **per-task MPU/MMU 区域** | 每任务 3 或 5 个可配置区域(随处理器变体);**每次上下文切换都会重新编程**;默认非特权任务只能访问自己的栈 |
|
||||
|
||||
- 区域要求:**大小必须是 32 字节的倍数,基址必须 32 字节对齐**
|
||||
- 可定义对所有任务可见的全局区域;**绝不应把另一个任务的栈分配给某任务**
|
||||
- 区域在任务创建时配置(可读/可写/可执行、用户或特权),**运行期可以重新配置**
|
||||
- ESM 还包括:数据混淆、安全的用户/内核空间分离层、任务上下文数据隔离(上下文存在 TCB 而不是栈上)、渗透检测监控
|
||||
|
||||
**RE 判据**:
|
||||
|
||||
```
|
||||
句柄不是内存指针(间接对象 ID)
|
||||
→ 别把句柄当 TCB/对象地址,也别指望从它推出结构布局
|
||||
```
|
||||
|
||||
## 二、CMSIS-RTX5 的安全特性
|
||||
|
||||
在 `OS_SAFETY_FEATURES` 等开关下提供:
|
||||
|
||||
| 特性 | 含义 |
|
||||
|---|---|
|
||||
| **Safety Classes** | 低安全等级的线程**不能修改**高等级线程;提供线程类的查询/挂起/恢复等接口。**动机**:RTOS 对象是经"以数字 ID 为参数、在 handler 模式执行"的系统调用访问的,**单靠 MPU 保护可以被"用别的对象 ID 调 API"绕过**——安全等级给对象访问加了访问控制 |
|
||||
| **MPU Protected Zones** | 空间隔离:一个 zone 是一组带访问权限的内存区/外设,分配给一个或多个线程;**zone ID 就是 MPU 表的行号**,上下文切换时内核比较 zone ID 并调用回调(用户实现,通常是把对应行载入 MPU);**内存违规触发 MemManage 异常**。注意:**中断处理程序绕过 MPU 保护**,因此必须以最高完整性等级验证 |
|
||||
| **Thread Watchdog** | 时间隔离:安全等级线程声明期望的周期/响应时间并"喂狗",未达标时由监督线程升级处理(重启线程/重启应用/受控关停/硬件复位) |
|
||||
| **对象指针检查** | 检查对象指针的**对齐与内存区域** |
|
||||
| **SVC 函数指针检查** | 检查 SVC 函数指针的对齐与内存区域;**要求链接器划出专门的执行区**容纳 SVC veneer 输入段 |
|
||||
|
||||
**ISR FIFO 队列(延迟处理)**:
|
||||
|
||||
- **在 ISR 中调用部分 RTX API 不会立即完成**——请求先进入 **ISR FIFO 队列**,等 ISR 结束后由调度器处理
|
||||
- 所以:
|
||||
|
||||
```
|
||||
IRQ 中调用 osXxx()
|
||||
→ 对象的实际状态在 IRQ 退出之后才变化
|
||||
→ 完全正常
|
||||
```
|
||||
|
||||
- **队列太小会产生 ISR 队列溢出错误**(专门的错误码),**此时系统状态已经不一致**;对策是**增大引起溢出的对象尺寸**,而不是怀疑队列对象损坏
|
||||
- 另有"无效 SVC 函数"错误码(对象 ID 位置放的是函数)
|
||||
|
||||
## 三、权限模型与"拒绝"的语义
|
||||
|
||||
- SAFERTOS 的任务模型包含:自己的栈、**被许可的 MPU/MMU 区域**、**特权状态**、可访问的 RTOS 对象;**上下文切换时重新编程任务的存储区域**
|
||||
- 因此三类现象都应先在权限层解释:
|
||||
|
||||
| 现象 | 优先解释 |
|
||||
|---|---|
|
||||
| 同一地址 Task A 能读、Task B fault | **per-task 区域配置**,不是内存损坏 |
|
||||
| 普通任务不能访问某地址、**ISR 却能** | 用户任务与特权内核/ISR 上下文的权限本就不同 |
|
||||
| API 调用附近出现特权转换 | 正常的 RTOS API 网关,**别当 ROP/trap 异常** |
|
||||
|
||||
**ESM 下"拒绝"的来源**:
|
||||
|
||||
- Task A 用某个句柄成功、Task B 用同一个句柄被拒 → 可能是**对象访问策略**拒绝,**不是句柄损坏**
|
||||
- 句柄看起来完全不像指针 → ESM 用**间接/混淆的对象 ID** 取代直接指向控制块的句柄;应恢复 **ID → 查表 → 对象** 的路径,而不是硬套 `句柄 = 内核指针`
|
||||
- **合法 API 返回拒绝** → 也可能是**访问控制策略**:当前任务根本没有调用该 API 的权限,而不是 API 实现坏了
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 SAFERTOS / RTX5 一类认证 RTOS 目标
|
||||
├─ 同一地址的访问权限不一致
|
||||
│ ├─ 查 per-task 区域配置(SAFERTOS)或 MPU zone 归属(RTX5)
|
||||
│ └─ 区域可能被运行期重新配置
|
||||
├─ 句柄看起来不像指针 / 指向奇怪的地方
|
||||
│ └─ SAFERTOS ESM 用的是间接对象 ID + 交叉引用表
|
||||
├─ 某 API 调用被拒
|
||||
│ ├─ 查调用方的访问控制策略(能否调这个 API)与对象访问策略(能否用这个对象)
|
||||
│ └─ RTX5 下查调用线程的安全等级
|
||||
├─ ISR 里调用的 API 没立即生效
|
||||
│ ├─ 请求进了 ISR FIFO,ISR 退出后才处理
|
||||
│ └─ 出现队列溢出错误 → 增大对象尺寸,且此时系统已不一致
|
||||
└─ 出现内存管理异常
|
||||
├─ RTX5:MPU zone 违规
|
||||
└─ 注意中断处理程序绕过 MPU 保护
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 访问控制:ACP/OACP 配置与运行期检查点(SAFERTOS);安全等级与对象属性(RTX5)
|
||||
- 空间:per-task 区域/zone 表、区域对齐与大小约束、运行期重配置调用
|
||||
- 延迟处理:ISR FIFO 队列的容量与溢出错误码
|
||||
- 验证:能同时说清「这次访问受哪条策略约束 + 区域/等级当前是什么 + 失败是策略拒绝还是真的坏了」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把 per-task 区域导致的访问失败当内存损坏**:权限差异可能完全合法
|
||||
- **假设区域在任务生命周期内不变**:运行期可以重新配置
|
||||
- **把 SAFERTOS 的句柄当内存指针**:ESM 下是经交叉引用表解析的间接对象 ID
|
||||
- **忘了区域的大小/对齐约束**:必须 32 字节倍数且基址 32 字节对齐
|
||||
- **以为 MPU 保护覆盖中断处理程序**:中断绕过保护,必须单独验证
|
||||
- **只在 MPU 层找越权防线**:RTX5 明确说单靠 MPU 可被"用别的对象 ID 调 API"绕过,需要安全等级
|
||||
- **把 ISR 中调用的 API 未立即生效当 bug**:请求进 ISR FIFO,退出后才处理
|
||||
- **把 ISR 队列溢出当队列对象损坏**:是容量不足,且此时系统状态已不一致
|
||||
- **忽略 SVC 函数指针检查对链接布局的要求**:需要专门的执行区容纳 veneer
|
||||
@@ -0,0 +1,136 @@
|
||||
# ThreadX Modules:module preamble / dispatch request ID / XIP
|
||||
|
||||
<CORE RULE>
|
||||
模块里的 API 调用**可能根本不是普通的 API 调用**:
|
||||
|
||||
```
|
||||
Module
|
||||
→ 预定义的 request ID
|
||||
→ 软件 dispatch 函数
|
||||
→ 常驻 Module Manager
|
||||
→ 真正的 ThreadX 服务
|
||||
```
|
||||
|
||||
所以在模块里看到:
|
||||
|
||||
```
|
||||
mov request_id, #37
|
||||
branch common_dispatch
|
||||
```
|
||||
|
||||
**不要判"大量功能被一个奇怪的 dispatcher 混淆了"**——它很可能就是 ThreadX Module 的 ABI。真正该恢复的是 **request ID → 服务名**的映射(本质接近一张 syscall table)。
|
||||
</CORE RULE>
|
||||
|
||||
## 一、Module Manager:常驻的唯一一份
|
||||
|
||||
- Module Manager 是**单份常驻代码**,负责创建模块的初始线程、启动其执行,并**承接模块发出的全部 ThreadX API 请求**
|
||||
- 源文件以 `txm_module_manager_*` 命名;模块侧文件不带 "manager"(如 `txm_thread_relinquish.c`),共用 `txm_module.h`
|
||||
- 组成:模块装载器(加载/重定位/准备)、**API dispatch**、内存管理、线程管理
|
||||
- 同时在载的模块数量**没有上限**(受可用内存限制),但 **Module Manager 只有一份**
|
||||
|
||||
## 二、module preamble:必须在模块的第一个地址
|
||||
|
||||
布局固定:
|
||||
|
||||
```
|
||||
[module preamble][module instruction area][module RAM area]
|
||||
```
|
||||
|
||||
preamble 描述模块的基本特征与资源:
|
||||
|
||||
| 内容 | 说明 |
|
||||
|---|---|
|
||||
| module ID / 主次版本 / preamble 大小 | 标识与格式版本 |
|
||||
| **properties 位图** | 见下 |
|
||||
| shell 入口 | 模块 shell 入口点 |
|
||||
| start / stop 线程入口 | 模块的主线程入口 |
|
||||
| start / stop 线程优先级与栈大小 | 线程配置 |
|
||||
| callback 线程入口 / 优先级 / 栈大小 | 回调线程配置 |
|
||||
| 代码大小 / 数据大小 | 区域尺寸 |
|
||||
|
||||
**properties 位图(含义各不相同)**:
|
||||
|
||||
- **bit 0**:特权模式 vs 用户模式(**用户模式 = MMU 保护**)
|
||||
- **bit 1**:无 MPU 保护 vs MPU 保护(**MPU 保护要求用户模式**)
|
||||
- **bit 2**:共享/外部内存访问的开关
|
||||
- **bits 31–24**:编译器 ID(区分 IAR / ARM / GNU 等工具链)
|
||||
|
||||
开发者常改的字段偏移(例如 module ID 在 `0x10`、start 线程在 `0x1C`、优先级在 `0x24`、栈大小在 `0x28`)在不同版本/工具链下要现场核对,**不要跨版本照搬**。
|
||||
|
||||
模块还需**用位置无关代码/数据(PIC/PID)构建**,才能被放到任意内存区执行。
|
||||
|
||||
## 三、request ID 与软件 dispatch
|
||||
|
||||
模块通过 `txm_*` 版本的 API 调 ThreadX,例如:
|
||||
|
||||
```
|
||||
(_txm_module_kernel_call_dispatcher)(TXM_THREAD_RELINQUISH_CALL, 0, 0, 0);
|
||||
```
|
||||
|
||||
- Module Manager 提供该函数指针,它带着某个服务对应的 **ID** 调用 Module Manager 的 dispatch 函数,由后者执行**真正的 API**
|
||||
- 在模块源码里定义 `TXM_MODULE` 可把 ThreadX API 调用**重映射**为这些模块专用版本
|
||||
- **ARM 上非特权模块用 SVC 指令陷入内核(特权)模式执行 API**;若 preamble 把模块配为特权模式,则**可直接调 API,不陷入**
|
||||
|
||||
**RE 主线**:把 dispatch 调用点收集起来,**建 request ID → 服务名的映射表**,之后所有模块代码才可读。
|
||||
|
||||
## 四、装载方式与 XIP
|
||||
|
||||
- 模块的**指令区可以原地执行(XIP),也可以被拷贝进模块内存区后再执行**;**模块的数据内存需求总是从模块内存区分配**
|
||||
- 因此可能出现:
|
||||
|
||||
```
|
||||
代码地址完全不在 module RAM 分配范围内
|
||||
→ 不一定是控制流劫持
|
||||
→ 可能只是 XIP 模块(代码还在 Flash,数据在 RAM)
|
||||
```
|
||||
|
||||
- 唯一的硬要求还是:**preamble 永远在模块第一个地址**
|
||||
- 加载流程:分配内存 → 校验完整性 → 处理 preamble → 重定位代码与数据 → 初始化 → 创建模块线程 → 启动
|
||||
|
||||
## 五、线程与栈的两个细节
|
||||
|
||||
- 每个模块提供**一个线程入口**及其栈大小/优先级/模块 ID,另有 callback 线程的栈大小/优先级
|
||||
- **模块线程在受内存保护的内核态执行时会用到另一个内核栈**(由端口配置给出)
|
||||
- **模块线程栈顶存放线程入口信息结构**(`TXM_MODULE_THREAD_ENTRY_INFO` 一类)——**算栈用量时必须把它计入**,否则会误判栈溢出
|
||||
- 模块拥有完整的 ThreadX API 访问权,包括在模块内创建更多线程
|
||||
|
||||
## 六、内存保护是可选的
|
||||
|
||||
启用时,**只有该模块的线程能访问其代码与数据内存**;任何越界访问都会造成内存故障并**终止出错的线程**。因此"某线程被静默终止"可能来自这里,而不是看门狗。
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 ThreadX Modules 目标
|
||||
├─ 先定位 module preamble(必然在模块第一个地址)
|
||||
│ ├─ 读 properties:特权/用户模式、MPU 保护、共享内存、编译器 ID
|
||||
│ └─ 读线程入口/优先级/栈大小与代码/数据尺寸
|
||||
├─ 看到大量「填 request ID → 跳公共 dispatcher」
|
||||
│ └─ 这是 Module ABI;建 request ID → 服务名映射表,别判混淆
|
||||
├─ 代码地址不在模块内存区
|
||||
│ └─ 先考虑 XIP(指令在 Flash、数据在 RAM)
|
||||
├─ 线程异常终止
|
||||
│ ├─ 查是否越界触发了内存保护故障
|
||||
│ └─ 查栈用量是否把栈顶的入口信息结构算进去
|
||||
└─ 线程进内核态的行为
|
||||
├─ 非特权模块 → SVC 陷入后才执行 API
|
||||
└─ 特权模块 → 直接调用,不产生陷入
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- preamble:逐字段解析(含 properties 位与编译器 ID),并用它定位指令区与数据区
|
||||
- API 路径:`txm_*` 侧调用点与 dispatch 函数、request ID 常量的分布
|
||||
- 装载方式:XIP 与拷贝装载的区分(代码地址与模块内存区的关系)
|
||||
- 验证:能同时说清「模块是怎么装的(XIP/拷贝)+ API 是怎么进来的(dispatch ID)+ 保护是否开启以及是哪一种」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把 request ID + 公共 dispatcher 当控制流混淆**:那是 Module ABI 的正常形态
|
||||
- **忘记 preamble 必在第一个地址**:找不到它就很难定位指令区/数据区边界
|
||||
- **跨版本照搬 preamble 字段偏移**:随版本与工具链变化
|
||||
- **把 XIP 模块的代码地址当控制流劫持**:指令区可以留在 Flash
|
||||
- **忽略 properties 位**:特权/用户模式与 MPU 开关决定了 API 是否经由 SVC 陷入
|
||||
- **算栈用量时漏掉栈顶的线程入口信息结构**:会误判栈溢出
|
||||
- **把"线程被终止"归因于看门狗**:内存保护故障也会终止线程
|
||||
- **以为模块只能有一个**:模块数量受内存限制,常驻的只有 Module Manager
|
||||
@@ -0,0 +1,105 @@
|
||||
# T-Kernel 与 TOPPERS/ITRON:上下文合法性 与 静态配置
|
||||
|
||||
<CORE RULE>
|
||||
TRON/ITRON 这一系最核心的经验是:
|
||||
|
||||
```
|
||||
同一个服务调用,在不同执行上下文里合法性不同
|
||||
```
|
||||
|
||||
所以看到 `act_tsk` / `sta_cyc` / 设备读写这类调用时,**先恢复"它跑在哪种上下文里",再谈语义**——只恢复 API 名会得出错误结论。
|
||||
|
||||
第二条:这一系**高度依赖静态配置**,**没有普通代码交叉引用的 task/handler 可能来自配置生成**。
|
||||
</CORE RULE>
|
||||
|
||||
## 一、T-Kernel:三种执行上下文
|
||||
|
||||
| 上下文 | 特征 |
|
||||
|---|---|
|
||||
| **任务部分(task portion)** | 常规任务执行 |
|
||||
| **准任务部分(quasi-task portion)** | 由**特定任务请求**而运行的处理:能识别发起任务、可定义任务状态迁移、**可以发出会进入等待状态的服务调用**;但有独立的处理器操作模式与栈空间(是 OS 的扩展部分) |
|
||||
| **任务独立部分(task-independent portion)** | 由**与任务进度无关的因素**激活(外部中断处理程序、时间事件处理程序):**不存在"当前发起任务"概念**——不能调用会进入等待状态、或隐式指定发起任务的服务调用;**不发生派遣**(延后派遣) |
|
||||
|
||||
**服务调用的合法上下文是规范明确规定的**,例如"等待设备请求完成":任务部分与准任务部分合法,**任务独立部分不合法**。
|
||||
|
||||
**设备驱动模型**:
|
||||
|
||||
- 设备经 `tk_def_dev` 注册,注册时指定处理函数集合:**openfn / closefn / execfn / waitfn / abortfn / eventfn**
|
||||
- 这些函数由设备管理调用,**以准任务部分运行**;**必须可重入**,且**不保证互斥调用**——需要互斥时由驱动自己做
|
||||
- 具体运行上下文细化:open/close 是调用 open/close 的那个任务的准任务部分;**execfn 是调用"读/写起始"的任务的准任务部分;waitfn 是调用"等待"的函数的任务的准任务部分**
|
||||
|
||||
**异步请求模型**:
|
||||
|
||||
```
|
||||
tk_rea_dev / tk_wri_dev → execfn(只发起,不等完成,返回)
|
||||
tk_wai_dev → waitfn(等待完成)
|
||||
```
|
||||
|
||||
- 完成顺序**不一定等于请求顺序**(取决于驱动),但**处理结果必须与请求顺序保持一致**
|
||||
- `tk_wai_dev` **只等待调用时刻正在处理的那些请求**;之后发出的请求不在等待范围内
|
||||
- 超时返回后**请求仍在继续处理**——需要再次调用等待;返回码与请求包中的错误字段分别表示"等待本身"与"处理结果"的错误
|
||||
- 中止语义有区别:单请求时中止当前处理;多请求时**只中止等待(释放等待),不中止处理**
|
||||
|
||||
## 二、TOPPERS:先确定家族,再谈语义
|
||||
|
||||
第三代 TOPPERS 内核(ITRON 系)是**四个不同家族**,规范分开规定:
|
||||
|
||||
| 家族 | 定位 |
|
||||
|---|---|
|
||||
| **ASP3** | 基础内核:**无保护、单核** |
|
||||
| **HRP3** | ASP3 + **保护**(内存保护、时间分区、对象访问保护、扩展服务调用) |
|
||||
| **FMP3** | ASP3 扩展为**多处理器** |
|
||||
| **HRMP3** | **保护 + 多处理器**兼具(可视为 HRP3 的多核扩展,或 FMP3 加保护) |
|
||||
|
||||
- 就绪队列设计也随之不同:HRP3 **每个保护域一个**就绪队列;FMP3 **每个处理器一个**;**HRMP3 两者结合**(按保护域 + 按处理器),且只在某域实际用到的处理器上建队列以省内存
|
||||
- 目标硬件要求(HRMP3):同构多核(大致 2–8 个)、**每个处理器都要有特权/非特权模式与 MMU/MPU**、且所有处理器能执行同一份二进制
|
||||
- 与 ASP3 一样:**内核内不做动态内存管理**,内核对象静态创建(只为应用提供固定大小的内存池)
|
||||
|
||||
**上下文类型(决定服务调用合法性)**:任务上下文、非任务上下文、ISR、循环处理程序、闹钟处理程序各有一套合法性规则。
|
||||
|
||||
## 三、共同的分析要求
|
||||
|
||||
1. **先确定家族/规格**(ASP3 还是 HRP3/FMP3/HRMP3;T-Kernel 的版本与规格)——保护与多核语义不同
|
||||
2. **恢复每个调用的上下文类型**(任务 / 准任务 / 任务独立;任务 / 非任务 / ISR / 循环 / 闹钟)
|
||||
3. **静态配置生成的 task/handler 可以没有普通 xref**——找配置与生成结果,而不是判死代码
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 T-Kernel / TOPPERS 目标
|
||||
├─ 先确定规格家族
|
||||
│ ├─ T-Kernel:确认版本与规格(设备管理、系统状态章节)
|
||||
│ └─ TOPPERS:ASP3 / HRP3 / FMP3 / HRMP3(保护与多核语义不同)
|
||||
├─ 某服务调用"非法"或被拒
|
||||
│ ├─ 恢复它的上下文类型(任务 / 准任务 / 任务独立;ISR / 循环 / 闹钟)
|
||||
│ └─ 对照规范的有效上下文表,而不是按 API 名推测
|
||||
├─ 设备驱动相关
|
||||
│ ├─ 六个处理函数分别以哪种上下文运行(准任务部分)
|
||||
│ ├─ 必须可重入且不保证互斥
|
||||
│ └─ 异步请求:发起与等待分离,完成顺序可不同但结果保持顺序一致
|
||||
├─ 超时后行为
|
||||
│ └─ 请求仍在处理,需再次等待;等待错误与处理错误是两回事
|
||||
└─ 找不到 task/handler 的创建点
|
||||
└─ 静态配置生成,别判死代码
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 规格:目标属于哪个家族/版本(保护与多核支持)
|
||||
- 上下文:每个调用的执行上下文类型与规范规定的合法性
|
||||
- 设备:`tk_def_dev` 注册的处理函数集合与它们各自的运行上下文
|
||||
- 配置:静态配置与生成结果(task / handler / 对象)
|
||||
- 验证:能同时说清「规格家族 + 该调用的上下文类型 + 该对象是静态配置还是动态创建」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **只恢复 API 名不恢复上下文**:合法性完全取决于上下文类型
|
||||
- **在任务独立部分调用会等待或隐式指定发起任务的服务**:规范禁止
|
||||
- **把准任务部分当普通任务**:它有独立的处理器模式与栈空间
|
||||
- **以为设备驱动函数之间自动互斥**:不保证互斥,驱动需自己处理,且必须可重入
|
||||
- **按请求顺序推断完成顺序**:完成顺序可不同,但结果要保持顺序一致
|
||||
- **超时后以为请求已中止**:处理仍在继续
|
||||
- **把等待错误与处理结果错误混同**
|
||||
- **不区分 TOPPERS 四家族**:保护与多核语义不同,跨家族套用会错
|
||||
- **在内核里期待动态内存管理**:这一系内核对象静态创建
|
||||
- **把静态配置生成的 task/handler 当死代码**
|
||||
@@ -0,0 +1,150 @@
|
||||
# µC/OS 与 SYS/BIOS:中断进入/退出 与分层调度
|
||||
|
||||
<CORE RULE>
|
||||
这两个内核都把"中断参与调度"做成了**显式协议**,于是:
|
||||
|
||||
```
|
||||
ISR 尾部突然发生上下文切换
|
||||
→ 合理行为,不是异常
|
||||
|
||||
task 已 ready,却要等中断退出后才运行
|
||||
→ 不是调度延迟
|
||||
```
|
||||
|
||||
**判断调度时机时,必须把"中断进入/退出"这一段算进控制流。**
|
||||
</CORE RULE>
|
||||
|
||||
## 一、Micrium µC/OS:ISR 必须通知内核
|
||||
|
||||
标准 ISR 形态:
|
||||
|
||||
```
|
||||
保存寄存器
|
||||
→ OSIntEnter() (或直接 OSIntNestingCtr++)
|
||||
→ …处理…
|
||||
→ OSIntExit()
|
||||
恢复寄存器 → 从中断返回
|
||||
```
|
||||
|
||||
- `OSIntEnter()` / `OSIntExit()` **必须成对**,且 **`OSIntEnter()` 不能被任务级代码调用**
|
||||
- `OSIntEnter()` 递增全局**中断嵌套计数**,从而**保证 ISR 执行期间不做任务调度**
|
||||
- 实现细节:若内核未在运行则直接返回;嵌套计数达到上限(250)也返回
|
||||
- **直接递增计数可省去函数调用开销**,但要求"递增时中断已关闭"——**并非所有架构都成立**,需要自己确认
|
||||
|
||||
**`OSIntExit()` 的决策序列**(这是恢复控制流的关键):
|
||||
|
||||
```
|
||||
内核未运行 → 返回
|
||||
关中断 → 若嵌套计数为 0 → 开中断返回(防计数回绕)
|
||||
递减嵌套计数 → 若仍 > 0(还有嵌套 ISR)→ 开中断返回
|
||||
若调度器仍被锁 → 开中断返回
|
||||
找出最高优先级就绪任务 → 与当前任务相同 → 开中断返回
|
||||
否则更新切换计数并执行"中断级上下文切换"
|
||||
```
|
||||
|
||||
- 即:**只有最后一个嵌套 ISR 退出时**才判断"是否需要切换到被 ISR 变为就绪的更高优先级任务"
|
||||
- 在 Cortex-M 上,该切换通常**只是触发 PendSV**(最低优先级异常),**真正的切换推迟到所有 ISR 完成之后**
|
||||
|
||||
**所以**:`ready 但没运行` 常可解释为"还在 ISR 里 / 调度器被锁 / PendSV 尚未执行"。
|
||||
|
||||
## 二、TI SYS/BIOS:四级线程 + Swi 禁用会连带禁用 Task 调度
|
||||
|
||||
线程层次(优先级由高到低):
|
||||
|
||||
```
|
||||
Hwi(硬件中断) > Swi(软件中断) > Task > Idle
|
||||
```
|
||||
|
||||
- **Swi 不能阻塞、总是运行到完成**;Task 可以阻塞,且会被任何更高优先级的线程抢占
|
||||
- Swi 与 Task 都有优先级级别(数值越大优先级越高,0 最低)
|
||||
|
||||
**关键的连带效应**:
|
||||
|
||||
- **`Swi_disable()` 会同时禁用 Task 调度器**——等价于同时调用"禁 Task"与"禁 Swi"
|
||||
- 因此:`Swi_post()` 在临界区中不会立即运行;即使 `Semaphore_post()` 让更高优先级任务就绪,**当前任务也不会在 `Swi_restore()` 之前被抢占**
|
||||
- `Swi_restore()` 在**此前 Task 调度器未被禁用**的前提下,会重新启用并进入 Task 调度器
|
||||
- **硬性约束**:Swi 调度器被禁用时**绝不能调用阻塞 API**(如信号量等待/门互斥进入),否则会"不可恢复地损坏"Task 调度器内部状态,行为"不可预测且通常是灾难性的"
|
||||
- 禁用某一层**不影响更高层抢占**(`Swi_disable()` 不阻止 Hwi 抢占)
|
||||
|
||||
**栈模型**:
|
||||
|
||||
- **Hwi 与 Swi 共用同一个系统中断栈(ISR 栈)**,一个程序一个;每个 Task 有私有栈
|
||||
- 若系统里没有 Task,所有线程共用一个系统栈;默认系统栈大小 4096 字节
|
||||
- **每多一个嵌套的 Hwi/Swi 优先级层次,栈需求就上升**;Swi 栈需求与所用 Swi 优先级数量成正比
|
||||
|
||||
**所以**:
|
||||
|
||||
```
|
||||
高优先级 Task 已就绪却不切换
|
||||
→ 先查 Swi 调度器是否被禁用(它会连带禁 Task 调度)
|
||||
→ 不要先找死锁
|
||||
```
|
||||
|
||||
## 三、µC/OS 的其余高频误判
|
||||
|
||||
**调度器锁与挂起是可嵌套的**:
|
||||
|
||||
- `OSSchedLock()` 是可**嵌套**的:即使退出了 ISR,**只要调度器仍被锁住就不会重调度**;必须成对解除
|
||||
- `OSTaskSuspend()` 同样可嵌套——**长期不就绪的高优先级任务先查这两者的嵌套层级**,再谈优先级反转
|
||||
|
||||
**信号量 vs 互斥量**:
|
||||
|
||||
- 多个任务可同时共享的资源 → **信号量**;**独占资源 → 互斥量**(互斥量实现会处理优先级反转)
|
||||
- 因此出现"低优先级任务影响高优先级任务"时,**先确认作者是不是把信号量当互斥量用了**
|
||||
|
||||
**软件定时器回调运行在定时器任务上下文**(这是很值钱的一条):
|
||||
|
||||
- 回调**在 OS 内部的定时器任务上下文中执行**——**既不是中断,也不是创建/启动该定时器的任务**
|
||||
- **用的是定时器任务的栈**,所以定时器任务的栈要按回调需求配足
|
||||
- **回调里绝不能做阻塞/等待**(延时、任何 `OS???Pend()`)——因为所有回调在定时器任务里**串行执行**,阻塞一个回调会把整个定时器子系统卡住
|
||||
- 所以:
|
||||
|
||||
```
|
||||
TimerCallback() → 内部有 blocking wait
|
||||
→ 高价值的 bug 信号(不是"偶发卡顿")
|
||||
```
|
||||
|
||||
**PendAbort:被唤醒 ≠ 拿到资源**:
|
||||
|
||||
- 中止等待(针对任务内建消息队列或普通消息队列)的作用是**中止等待并让任务就绪**,而**不是正常投递**
|
||||
- **只能由任务调用(不能从 ISR 调)**;指针为 NULL 或指向调用者自己是无效用法
|
||||
- 被唤醒的任务会**带着"等待被中止"的错误码返回**
|
||||
|
||||
**所以**:任务被唤醒、但队列/信号量里并没有新消息——先看是不是中止等待路径,**"被唤醒"与"成功获取资源"是两件事**。
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 µC/OS 或 SYS/BIOS 目标
|
||||
├─ 调度时机对不上
|
||||
│ ├─ µC/OS:查 OSIntNestingCtr(是否还在 ISR 里)、调度器锁、PendSV 是否已执行
|
||||
│ ├─ SYS/BIOS:查 Swi 调度器是否被禁用(连带禁用 Task 调度)
|
||||
│ └─ 别先判"调度延迟/死锁"
|
||||
├─ ISR 内调用内核 API
|
||||
│ ├─ µC/OS:是否成对调用 OSIntEnter/OSIntExit
|
||||
│ ├─ SYS/BIOS:是否误用阻塞 API(Swi 禁用期间调用会损坏调度器状态)
|
||||
│ └─ 注意 SYS/BIOS 的 Swi 不能阻塞
|
||||
├─ 栈相关异常
|
||||
│ ├─ SYS/BIOS:Hwi 与 Swi 共用 ISR 栈;嵌套层次与 Swi 优先级数影响栈需求
|
||||
│ └─ 默认系统栈 4096 字节,注意配置项
|
||||
└─ 看到"直接递增嵌套计数"的写法
|
||||
└─ 只在"递增时中断已关闭"的架构上安全,需逐架构确认
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 中断协议:ISR 的进入/退出调用是否成对;嵌套计数的读写点
|
||||
- 调度禁用:调度器锁、Swi/Task 禁用标志与恢复点
|
||||
- 栈:系统(ISR)栈与任务栈的划分、嵌套层次与栈配置
|
||||
- 验证:能同时说清「当前是否在 ISR 上下文 + 哪一层的调度被禁用 + 切换在何时真正发生」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把 ISR 尾部的切换当异常**:那是中断退出时的正常调度决策
|
||||
- **忽略嵌套计数**:只有最后一个嵌套 ISR 退出才判断切换
|
||||
- **以为 PendSV 触发就等于已切换**:真正的切换在所有 ISR 完成后
|
||||
- **在 µC/OS 里让任务级代码调用 `OSIntEnter()`**:协议禁止
|
||||
- **照搬"直接递增计数"的优化**:只在特定架构安全
|
||||
- **忘记 `Swi_disable()` 会连带禁用 Task 调度**:这是 SYS/BIOS 最反直觉的一条
|
||||
- **在 Swi 调度器禁用期间调用阻塞 API**:会不可恢复地损坏调度器状态
|
||||
- **按"每个线程独立栈"估算 SYS/BIOS 栈需求**:Hwi 与 Swi 共用 ISR 栈
|
||||
@@ -0,0 +1,94 @@
|
||||
# VxWorks:DKM / RTP / 全局符号环境
|
||||
|
||||
<CORE RULE>
|
||||
第一步不是解析 ELF,是**分清两种运行模型**:
|
||||
|
||||
| 模型 | 产物 | 执行位置 | 地址空间 |
|
||||
|---|---|---|---|
|
||||
| **DKM**(Downloadable Kernel Module) | `.out` | **内核态**(supervisor mode) | 与内核同一地址空间 |
|
||||
| **RTP**(Real-Time Process) | `.vxe` | **用户态** | 各自独立,MMU 保护 |
|
||||
|
||||
**`.out` 不要按 Linux `.ko` 直接解析。** 它最特别的地方不是格式,而是**符号环境**:一个 DKM 是"链接到内核符号表"的,脱离目标镜像与全局符号环境看它,**永远会看到一堆 unresolved symbols**——那不是损坏,是模型。
|
||||
</CORE RULE>
|
||||
|
||||
## 一、DKM:内核态,符号来自运行镜像
|
||||
|
||||
- 运行在 supervisor mode,**可被静态链接进内核镜像,也可动态加载**
|
||||
- **`.out` 与内核符号表动态链接**——它的外部引用指向**目标镜像的全局符号环境**
|
||||
- 加载方式:目标 shell 里 `ld < my_first_dkm.out`(模块的 `usrAppInit` 随后自动运行),或经调试器 `module load <path>`
|
||||
- 现场核对手段:`lkup "符号名"` 查符号、`moduleShow` / `taskShow` 看模块与任务
|
||||
- **典型加载失败是 `undefined symbol`**:DKM 调用了某个组件,而**该组件在构建 VIP 内核时没被包含进去**——对策是把组件加进 VIP 配置并重建 VIP(再重建 DKM),**不是"修 DKM 本身"**
|
||||
- 为便于事后定位,VIP 里可打开 `INCLUDE_STANDALONE_SYM_TBL`(内建符号表),让崩溃/启动信息给出更有意义的调用栈
|
||||
|
||||
## 二、RTP:用户态,标准 ELF 与 `.so` 链接
|
||||
|
||||
- 每个 RTP 有独立虚拟地址空间与 MMU 保护,**一个 RTP 崩溃不影响内核与其他 RTP**;内核服务经系统调用进入
|
||||
- 产物 `.vxe` 是**标准 ELF 可执行文件**;动态链接的 `.vxe` 按 **NEEDED / SONAME / RPATH** 在运行时解析 `.so`
|
||||
- 可从 ROMFS/NFS/SD 卡等加载,或经调试器;也可用环境变量方式启动,例如 `LD_LIBRARY_PATH=/romfs/lib` 配 `rtp exec ... demo.vxe &`
|
||||
- 加载失败排查路径与普通 ELF 一致:**库找不到**(`Shared object not found`,查 `LD_LIBRARY_PATH` 与文件系统挂载)→ **名字/版本不匹配**(`readelf -d` 比对 NEEDED/SONAME/RPATH)→ 二级依赖断裂 → 或它本来就是静态构建的
|
||||
- `shl` / `shl info` 可看当前实际加载了哪些共享库
|
||||
- 调试提示:**不要在动态链接器完成符号登记之前就把调试器停住**(早期暂停会看到"符号没登记"的假象)
|
||||
|
||||
## 三、全局符号环境:独立 ELF "看起来不完整"是常态
|
||||
|
||||
这是一个 DKM **必须整体恢复**的最小集合:
|
||||
|
||||
```
|
||||
VxWorks image(目标运行镜像)
|
||||
+ VSB / VIP 配置
|
||||
+ global symbol table(全局符号表)
|
||||
+ DKM 本身
|
||||
→ 再做 relocation / symbol resolution
|
||||
```
|
||||
|
||||
- **只拿一个 DKM 单独看**:大量 unresolved symbols 是预期结果,不是 malformed
|
||||
- 反过来也成立:**分析时能"解出来"的调用目标,取决于你把哪份符号环境当成真值**——用错 image 的符号表,函数名会整体错位
|
||||
- 所以 RE 主线应写成:**先定位目标镜像与配置,再谈模块内的逻辑**
|
||||
|
||||
## 四、"同一个 VxWorks 版本"不等于"同一个系统 ABI 环境"
|
||||
|
||||
- SDK 本身是**针对具体 VSB/VIP 生成**的;DKM 也基于特定 VIP 编译,以保证与那个内核镜像的 API/ABI 兼容
|
||||
- 因此遇到"这个 DKM 在另一台**同版本** VxWorks 上为什么不工作":**优先核对具体的 image configuration**,而不是只比版本字符串
|
||||
- 同理,**不要把 RTP 的 `.so` 当 DKM 模块加载**(反之亦然)——两类产物走的是两套加载与解析机制,混用是常见错误
|
||||
|
||||
## 五、与任务/TCB 分析的接续
|
||||
|
||||
DKM 侧任务与内核对象仍走 [[re-rtos]] 主文档的任务表方法(`taskSpawn`/`taskCreate` + `WIND_TCB` 字段从上下文切换现场反推)。本分支补的是**"这份代码能不能加载、符号从哪来、跑在哪一侧"**——**先解决身份与符号环境,再谈任务结构**;顺序颠倒会得到一堆偏移正确但语义全错的结论。
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 VxWorks 目标
|
||||
├─ 先分型:.out(DKM) / .vxe(RTP) / 整个 image
|
||||
│ ├─ .out → 内核态:符号来自目标镜像的全局符号环境
|
||||
│ ├─ .vxe → 用户态:按普通 ELF 分析;.so 走动态链接器
|
||||
│ └─ image → 先提取全局符号表与配置,作为其他分析的基准
|
||||
├─ 看到大量 unresolved symbol
|
||||
│ ├─ 先问:我是不是只拿到了 DKM、而没拿到它的符号环境?
|
||||
│ └─ 再问:目标 VIP 是否本就未包含该组件(undefined symbol 的真实成因)
|
||||
├─ 模块加载不了
|
||||
│ ├─ DKM → 查 VIP 组件是否包含;查与目标镜像的 ABI 是否匹配
|
||||
│ └─ RTP → 查库路径/NEEDED/SONAME/RPATH/二级依赖/是否静态构建
|
||||
├─ 同版本却行为不同
|
||||
│ └─ 核对 image configuration(VSB/VIP)而不是版本字符串
|
||||
└─ 确认身份与符号环境之后 → 再进任务表/TCB 分析([[re-rtos]] 主文档方法)
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 现场枚举:`lkup`(符号)、`moduleShow`(模块)、`taskShow`(任务)、`shl` / `shl info`(已加载共享库)
|
||||
- 加载:`ld < module.out`(DKM)、`rtp exec ... demo.vxe`(RTP)
|
||||
- ELF 侧:`readelf -d`(NEEDED/SONAME/RPATH 比对)、常规符号与重定位工具
|
||||
- 配置侧:VSB / VIP 配置与 image 构建产物(决定符号环境与组件集合)
|
||||
- 验证:能同时说清「模型(DKM/RTP)+ 目标 image 与配置 + 符号解析依据 + 加载路径」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把 DKM `.out` 当 Linux `.ko` 直接解析**:执行模型、符号模型、加载机制都不同
|
||||
- **只看单个 DKM 就判断"文件不完整/被裁剪"**:它是链接到目标镜像符号环境的,孤立看必然一堆未解析符号
|
||||
- **`undefined symbol` 时去改 DKM**:真实成因通常是 VIP 未包含该组件,要改的是镜像配置并重建
|
||||
- **用错 image 的符号表**:函数名会整体错位,比"没有符号"更危险
|
||||
- **拿版本号当 ABI 兼容性依据**:SDK 与 DKM 都针对具体 VSB/VIP 生成,要核对配置
|
||||
- **把 RTP 的 `.so` 当 DKM 模块加载**(或反之):两套加载/解析机制不可混用
|
||||
- **在动态链接器完成符号登记前就下结论**:早期暂停会看到"符号没登记"的假象
|
||||
- **跳过身份与符号环境直接做任务/TCB 分析**:会得到偏移正确但语义全错的结论
|
||||
@@ -0,0 +1,133 @@
|
||||
# Zephyr:编译期设备图 / iterable sections / LLEXT
|
||||
|
||||
<CORE RULE>
|
||||
**Zephyr 里大量"运行时对象"其实在编译期就由 Kconfig + devicetree + linker 生成好了。**
|
||||
|
||||
```
|
||||
driver_init() ← 0 个直接 xref
|
||||
```
|
||||
|
||||
**这不是 dead function。** 它可能是被 `DEVICE_DEFINE` / `SYS_INIT` / iterable section 注册进系统的——**引用关系在链接期成立,不在源码里成立**。
|
||||
|
||||
所以第一条判断是:**先看它是不是被"注册"进去的,再谈它是否可达。**
|
||||
</CORE RULE>
|
||||
|
||||
## 一、设备不是"被注册的",是"被链接进来的"
|
||||
|
||||
- `DEVICE_DEFINE`(基础)/ `DEVICE_DT_DEFINE`(devicetree 感知,单实例)/ `DEVICE_DT_INST_DEFINE`(实例化驱动,配 `DT_DRV_COMPAT`)会把设备元数据放进**专门的 linker section**,内核启动时**统一遍历**
|
||||
- 历史上设备与 `SYS_INIT` 共用一套 init 基础设施,**后来被拆开**:设备进入自己的 section,但沿用同一套 init level
|
||||
- 同一 level 内,**先初始化设备,再执行 `SYS_INIT` 调用**;顺序由 linker 排序保证
|
||||
- 内核按 section 迭代的顺序是**确定的**——所以"init 顺序"在 Zephyr 里是可推导的,不是运行期偶然
|
||||
|
||||
**RE 含义**:看到一个 `struct device` 实例没有任何代码引用它,**先去找它落在哪个 section**,而不是标 dead data。
|
||||
|
||||
## 二、`SYS_INIT`:注册式入口,且**返回值有后果**
|
||||
|
||||
- 形态:`SYS_INIT(func, level, prio)`,函数签名 `int (*)(const struct device *dev)`
|
||||
- level:**`PRE_KERNEL_1` → `PRE_KERNEL_2` → `POST_KERNEL` → `APPLICATION`**(SMP 下另有对应层级);`prio` 决定**同一 level 内**的先后(数值小的先跑)
|
||||
- **`PRE_KERNEL_1` / `PRE_KERNEL_2` 跑在内核初始化上下文中(中断栈)**,其余 level 跑在内核主任务中
|
||||
- **返回非零(错误码)可能导致启动失败**——分析"为什么起不来"时,这是一条应逐一核对的路径,而不是先怀疑硬件
|
||||
|
||||
## 三、iterable sections:别把注册表当混淆表
|
||||
|
||||
Zephyr 的 iterable sections 把**相同结构体的实例放进连续的 linker section**,运行时统一遍历;**条目甚至按名称排序**。
|
||||
|
||||
所以看到下面这些,**不要轻率判定为 vtable / jump table / 混淆表**:
|
||||
|
||||
- 一整段**规则排列**的函数指针或结构体
|
||||
- 条目**按名字有序**、字段布局完全一致
|
||||
- 数量正好等于某个配置项(Kconfig)能算出来的值
|
||||
|
||||
**先假设它是 linker 生成的注册表**,到构建产物(map 文件、section 名)里核对,再下结论。
|
||||
|
||||
## 四、init level 的语义约束:early init 的"缺省行为"是设计
|
||||
|
||||
`PRE_KERNEL_1` / `PRE_KERNEL_2` 阶段,**内核服务尚不可正常使用**。因此:
|
||||
|
||||
```
|
||||
early init 里:不调用 mutex / thread / sleep,只配 IRQ / MMIO
|
||||
```
|
||||
|
||||
**这符合设计**,不是"代码被裁剪"或"缺少同步所以是漏洞"。
|
||||
|
||||
**反过来才是异常**:如果 early init 里出现了创建线程、取互斥、睡眠这类调用,**优先怀疑阶段判断错了**(它可能不是 early init),而不是先判"违反约定"。
|
||||
|
||||
## 五、恢复 devicetree 时:`/chosen` 与 `/aliases` 不是硬件
|
||||
|
||||
- `/chosen` 与 `/aliases`**本身不是硬件节点**,只是**对其他节点的引用/选择**(以及启动参数类信息)
|
||||
- 因此逆向恢复 DT 时**不能创建一个假设备节点 "chosen"**——那会把"引用"变成"实体",后续所有依赖它的推断跟着错
|
||||
- 同样,`status = "disabled"` 的节点**仍然存在于 DT 里**:**"DT 里有"不等于"被编译进设备图"**,要结合该节点的实际引用与构建配置判断
|
||||
|
||||
## 六、map 文件是这一域的"交叉引用替代品"
|
||||
|
||||
**验证设备/init 归属,最直接的证据是构建产物**:
|
||||
|
||||
- 在 `build/zephyr/zephyr.map` 里搜 `__init_PRE_KERNEL` / `__init_POST_KERNEL` 一类的 section 名,可以直接看到**谁落在哪个 init 段**
|
||||
- 同理可核对设备 section、iterable section 的成员与顺序
|
||||
|
||||
**RE 主线因此是**:源码符号只当线索,**归属问题一律回到 linker section / map 文件裁决**。
|
||||
|
||||
## 七、LLEXT:运行期装载的 ELF 扩展,但不是 Linux `.ko`
|
||||
|
||||
现代 Zephyr 支持运行期装载 ELF 扩展(**Linkable Loadable EXtensions**):
|
||||
|
||||
- 扩展是**经预编译的 ELF 可执行文件**,可被校验、装载、与主镜像链接,之后卸载
|
||||
- 有**独立的导出符号表**;架构相关支持有限(并非所有架构都可用)
|
||||
- 装载:`llext_load(loader, name, &ext, ldr_parm)` 返回 `struct llext` 或负错误码;loader 可为缓冲区实现等
|
||||
- 初始化:`llext_bringup`(调用 `.preinit_array` / `.init_array`)、`llext_teardown`(清理)、`llext_bootstrap`(两者兼具,可配 `k_thread_create`)
|
||||
- **`llext_unload` 之后,先前取得的所有符号指针全部失效**——"卸载后指针还能用"是错的
|
||||
- 两张符号表要分清:**`sym_tab`**(扩展内全部全局符号,供内部链接)与 **`exp_tab`**(扩展**导出**的符号,供主镜像查找,宏为 `LL_EXTENSION_SYMBOL`);查找用 `llext_find_sym(表, 名)`,**传 NULL 表表示查主镜像的符号表**
|
||||
- 主镜像侧有一张专门的 LLEXT 符号表(每个导出符号一条 名字→地址)。开启 **SLID**(把名字换成指针宽度的哈希)可加快查找并缩小表,但**主镜像与扩展必须用同一设置**——不一致会表现为"符号找不到"
|
||||
|
||||
**最值得记住的特殊情况**:
|
||||
|
||||
```
|
||||
扩展装载成功、内核能调用它
|
||||
但用户线程一访问就 fault
|
||||
```
|
||||
|
||||
若启用了 **User Mode**,先查是不是**没有把扩展加入相应的内存域**——扩展**默认不属于任何用户内存域**,需要显式调用 `llext_add_domain(ext, domain)` 把扩展的内存区加进 `k_mem_domain`;未启用 User Mode 时该函数返回 `-ENOSYS`。
|
||||
|
||||
## 决策树
|
||||
|
||||
```
|
||||
拿到 Zephyr 目标
|
||||
├─ 某个 init/设备函数没有直接 xref
|
||||
│ ├─ 查它是否被 DEVICE_DEFINE / DEVICE_DT_DEFINE / SYS_INIT 注册
|
||||
│ ├─ 查它落在哪个 linker section(map 文件 / __init_* section 名)
|
||||
│ └─ 别标 dead code
|
||||
├─ 一整段规则排列的结构体/函数指针
|
||||
│ ├─ 先按 iterable section / 注册表理解
|
||||
│ └─ 到 map 与 section 名核对,再考虑 vtable / jump table / 混淆
|
||||
├─ early init 里没调用内核服务
|
||||
│ └─ 符合 PRE_KERNEL_* 的设计约束,不是被裁剪
|
||||
│ (反之:early init 里出现线程/互斥/睡眠 → 先怀疑阶段判断错了)
|
||||
├─ 恢复 devicetree
|
||||
│ ├─ /chosen 与 /aliases 是引用,不建假设备节点
|
||||
│ └─ disabled 节点仍在 DT 里:DT 里有 ≠ 进了设备图
|
||||
└─ LLEXT 相关
|
||||
├─ 装载/链接失败 → 符号表(sym_tab/exp_tab)、SLID 设置是否与主镜像一致、架构是否支持
|
||||
├─ 内核能调、用户线程 fault → 查是否调用 llext_add_domain 加入内存域
|
||||
└─ 卸载之后指针失效属预期,不是 UAF
|
||||
```
|
||||
|
||||
## 工具与验证
|
||||
|
||||
- 归属与顺序:构建产物 `zephyr.map` 中搜 `__init_PRE_KERNEL*` / `__init_POST_KERNEL*` 与设备 section 名
|
||||
- 配置:Kconfig(决定哪些对象会被生成)+ devicetree(决定节点与实例是否存在)
|
||||
- 调试:LLEXT 侧提高日志等级;用符号文件配合正确的加载偏移(通常取 `.text` 起始)做符号化
|
||||
- 验证:能同时说清「对象来源(源码 / linker 生成 / 运行期扩展)+ 所属 section 或 init level + 是否进入设备图」
|
||||
|
||||
## 该平台的坑(汇总)
|
||||
|
||||
- **把无 xref 的 init/设备对象当 dead code**:它们由 linker section 注册,引用关系在链接期成立
|
||||
- **把规则排列的注册表当 vtable / jump table / 混淆表**:iterable section 的条目本就连续且按名排序
|
||||
- **把 early init 的"不调用内核服务"当裁剪或缺陷**:`PRE_KERNEL_1`/`PRE_KERNEL_2` 阶段服务尚不可用,这符合设计
|
||||
- **反过来漏判**:early init 里出现线程/互斥/睡眠,先怀疑"这根本不是 early init"
|
||||
- **给 `/chosen`、`/aliases` 造硬件设备节点**:它们只是引用,不是实体
|
||||
- **以为 DT 里有的节点就一定生效**:`status = "disabled"` 的节点仍在 DT 中,是否进入设备图要看构建与引用
|
||||
- **用源码调用图替代 linker 归属判断**:顺序与成员应当到 section / map 文件里裁决
|
||||
- **把 LLEXT 当 Linux `.ko`**:它是 ELF 扩展,有独立的导出符号表、架构限制与初始化数组语义
|
||||
- **LLEXT 装载成功就以为用户态可用**:扩展默认不属于任何用户内存域,需 `llext_add_domain`
|
||||
- **卸载扩展后继续使用旧符号指针**:`llext_unload` 后这些指针全部失效
|
||||
- **主镜像与扩展的符号查找设置不一致(如 SLID)**:会表现为莫名其妙的"符号找不到"
|
||||
Reference in New Issue
Block a user