Files

9.3 KiB
Raw Permalink Blame History

Redox OS:scheme / 启动链 / relibc / 驱动即用户态 daemon

Redox 的核心抽象不是"VFS + 内核驱动",而是 **scheme**:
普通程序 read()/write()/mmap()
  → 内核翻译成 SQE 消息
  → scheme provider(通常是用户态 daemon)处理
  → CQE 消息 → 内核转成系统调用返回值

所以 read() 不必然意味着文件系统——它可能是网络、设备、IRQ 抽象或任意系统服务,含义由目标 scheme 决定。

第二条:scheme 侧的 handle 描述符 ≠ 客户端的 fd,两者由内核映射,不能跨侧比较整数。

第三条(分析定位):syscall ABI 是刻意不稳定的——稳定层被推到了用户态库。所以你手上的二进制未必对应当前内核的 syscall 号,版本对不上不要急着判"被改过"。 </CORE RULE>

一、scheme 的形态

类型 提供者
用户态 scheme 用户态程序(通常是 daemon)——可能实现的都放用户态
内核 scheme 内核直接实现(只保留关键部分)
  • 根 scheme(:)是内核提供的特殊 scheme,作为所有其他 scheme 名字的容器
  • provider 通过创建 ":myscheme" 建立 scheme,返回的文件描述符就是它与内核之间的消息通道
  • 调度分类:涉及路径的文件系统调用被标为路径类或文件类——内核按路径前缀匹配 scheme 名,或者记住打开该 fd 时用的是哪个 scheme;两套匹配方式决定了"换了个路径为什么行为变了"
  • scheme 在内核侧是一个 trait(对应用户态 daemon 的 UserScheme 实现)
  • 其他 IPC 不走 scheme:管道一类用普通系统调用;另有专门的 IPC daemon 提供共享内存/通道风格的 IPC

二、内核侧的转换与映射

  • 普通程序的文件操作被内核翻译成 SQE 消息交给 provider;provider 用 CQE 消息回应,内核再把它转成系统调用的结果
  • SQE/CQE 是双向队列条目:opcode(Open/Read/Write…)、flags、tag(请求 ID)、caller(调用方进程 ID)、args(操作相关参数)
  • 描述符映射是关键:open 请求里带待打开项的名字,scheme 分配自己的编号描述符;这个描述符与客户端的文件描述符不是同一个——内核在 (客户进程, fd 号) 与 (provider 进程, handle 号) 之间做映射,并把它们放进消息包

RE 含义:trace 里两侧各有一个整数句柄,它们本来就不该相等;建立关系要经内核的映射,而不是比较整数。

  • 阻塞语义:客户端做阻塞 read/write 时被挂起;内核对 provider 的事件描述符发事件包把它唤醒;完成后客户端被标记就绪并放入运行队列
  • 缓冲区捕获:对 read(fd, buf, len) 这类操作,内核把调用方缓冲区暴露给 provider——头部拷贝、中间页零拷贝(pin 住)、尾部处理
  • FD 传递:scheme 可以当 IPC broker,把描述符插进调用方的文件表
  • 内存映射:用户态 scheme 经专用 opcode 提供 mmap 区域
  • 命名空间:进程继承父命名空间;较新的架构把命名空间管理移到用户态 manager 守护进程,scheme 在内核里匿名创建,请求经 openat + 命名空间描述符分发

三、daemon 侧的固定套路(决定了它的行为边界)

一个 scheme provider 的典型初始化:

  1. 创建 scheme(拿到 fd)
  2. 打开它需要的资源 fd(中断、定时器、事件等)
  3. 进入 null namespace——此后无法再按名字打开普通资源
  4. 把要监听的事件 fd 登记到事件 scheme
  5. 进入循环:等事件 → 读事件判断是定时器 / 资源事件(如设备中断)/ scheme 请求 → 处理请求 → 按 tag 回响应

所以:

driver 初始化后进入 null namespace、随后打不开普通资源
  → 不是 sandbox 异常,是安全设计

四、启动链:从引导到第一个用户态程序

bootloader(BIOS 版 / EFI 版,后者覆盖 x86_64、aarch64、riscv64)
  → 载入 kernel 与 initfs 到内存、建立初始页表
  → 跳到内核入口(架构相关的汇编入口)
  → Rust 入口:校验 BSS/data、建初始栈、带"内核参数"结构进入
  → 初始化调试输出、内存管理器、分页与堆、(x86)GDT/IDT 与 syscall 指令
  → 主初始化
  → 进入用户态调度循环
  • 那个"内核参数"结构携带:内核映像、栈、环境、硬件描述(x86 上是 ACPI RSDP,aarch64/riscv64 上是设备树)、引导器提供的内存区、以及 bootstrap / initfs 区域
  • 多核的从核走另一条入口路径

bootstrap(第一个用户态程序):

  • 内核载入 initfs blob、为它建立地址空间、跳到引导器给的偏移
  • bootstrap 在汇编桩里分配栈、对自己做内存保护设置、然后 exec init,并启动 initfs scheme daemon
  • 它本身就是一个极小的 ELF 加载器(Rust 编写、带每架构的自定义链接脚本、静态链接)

initfs 是压缩归档,含两级启动所需的最小二进制与配置:架构相关的驱动(块设备、输入等)与核心 daemon。这样系统就能支持多种文件系统驱动而不把它们编进内核。

五、relibc 与 C 运行时

  • relibc 是 Rust 写的 C 库(连头文件也是,宏除外),同时支持 Redox 与 Linux,两套后端:Linux 上走原始 syscall,Redox 上走运行时库
  • 稳定 ABI 层在用户态:应用面向一个稳定 ABI,不稳定部分(syscall ABI、IPC、进程管理)被收进运行时库
  • C 启动序列:_start(crt0)→ 运行时启动函数解析辅助向量、建立 argc/argv/env、初始化线程控制块 → .init_array 构造 → main()
  • 辅助向量里有平台特有的项(例如进程 fd、线程 fd、继承的信号屏蔽),还有动态链接器要用的程序头指针——所以"这个二进制是静态还是动态"可以从辅助向量侧确认
  • 线程控制块用架构寄存器承载;线程经 clone 创建,在 Redox 上每个线程用一个文件描述符表示,共享地址空间并继承文件表
  • syscall 包装层负责可重启语义(被信号打断后重试)与把内核错误映射成 errno

六、驱动与 daemon 的实际形态

  • 驱动是独立的用户态程序(driver schemes,表现为文件风格的服务)
  • 它们在文件系统里有固定位置,PCI 相关的设备配置也以数据文件形式存放(按厂商/设备组织)
  • 驱动是两遍构建:initfs 里用体积优化 + panic-abort 的版本,主文件系统里用标准 release 版本

逆向含义:同一个驱动在两个位置不是同一份产物——不要拿 initfs 里的版本推断主系统里的行为。

决策树

拿到 Redox 目标
├─ 先判:内核 / 用户态 daemon / 应用
│    ├─ 内核很小,syscall ABI 刻意不稳定 → 版本对不上别判被改
│    └─ 用户态 daemon 是最可能的目标
├─ 一次 read/write 背后是什么
│    ├─ 先定位它是哪个 scheme(路径的 scheme 名即"类型")
│    ├─ 匹配方式:路径前缀 还是 "记住打开时的 scheme"
│    └─ 别默认它是文件系统
├─ 两侧句柄对不上
│    ├─ 客户端 fd 与 provider 描述符本就不相等
│    └─ 经内核映射建立关系
├─ 某个进程之后打不开新资源
│    └─ 可能是 provider 主动进入 null namespace
├─ 想看消息内容
│    └─ 按 SQE/CQE 条目解析:opcode / flags / tag / caller / args
├─ 启动/加载相关
│    ├─ 找 bootstrap 与 initfs(两级启动、极小 ELF 加载器)
│    └─ 设备接线来自 initfs 与用户态 daemon,不在内核
└─ 同款驱动两份产物
     └─ initfs 版与主系统版构建方式不同

工具与验证

  • 服务拓扑:路径里的 scheme 名 → provider daemon;scheme 的创建点(:name)
  • 请求流:SQE/CQE 条目字段(opcode / tag / caller / args)与响应匹配
  • 句柄:客户端 fd 与 provider 描述符的内核映射关系
  • 启动:引导器类型、内核参数里的硬件描述来源(ACPI vs 设备树)、initfs 内容
  • 运行时:辅助向量项(静态/动态、fd 承载的线程与进程句柄)
  • 验证:能同时说清「这次 I/O 落到哪个 scheme + provider 是谁 + 两个句柄怎么对应 + 这份产物属于哪一遍构建」

该平台的坑(汇总)

  • 把 read/write 默认当文件系统:含义由 scheme 决定
  • 跨侧比较句柄整数:客户端 fd 与 provider 描述符由内核映射,不等价是常态
  • 把进入 null namespace 当沙箱异常:那是 provider 的安全设计
  • 在内核里找设备驱动:驱动多为用户态 daemon
  • 忽略 scheme 的两种匹配方式:路径前缀 vs 记住打开时的 scheme
  • 把 syscall 号写死:ABI 刻意不稳定,稳定层在用户态运行时
  • 把 initfs 里的驱动当主系统的同一份产物:两遍构建,优化与 panic 策略不同
  • 把阻塞归因于死锁:客户端阻塞是设计,由 provider 的事件唤醒机制驱动
  • 忽略 fd 传递能力:scheme 可以充当 IPC broker
  • 把启动时的硬件描述来源搞错:x86 与 aarch64/riscv64 不同(ACPI vs 设备树)