Files
reverse-skills-max/vendor-new/rev-skills/.claude/skills/re-rtos/references/nuttx.md

6.3 KiB
Raw Blame History

NuttX:FLAT / PROTECTED / KERNEL 三种内存模型

分析 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 三阶段可用的设施不同