- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
本文基于 Tock 仓库中 doc/wg/core/notes/core-notes-2022-04-01.md(2022 年 4 月 1 日 Core Working Group 会议纪要)整理。本次会议围绕三个直接影响 Tock 内核进程加载与系统调用 ABI 的技术议题展开:Ergonomic Static Apps(静态应用加载的易用性)、固定位置应用(Fixed Position)与位置无关应用(PIC)的混合部署、以及Command 系统调用新增指针返回类型的提议(PR #3003)。通过对照本仓库内核源码(
kernel/src/process_standard.rs、kernel/src/syscall_driver.rs、kernel/src/syscall.rs)、TBF 头格式库(libraries/tock-tbf/src/types.rs)以及系统调用 TRD(doc/reference/trd104-syscalls.md),本文对每个议题的讨论背景、技术约束与最终设计方向进行了完整还原与源码级印证。阅读本文后,你将能理解 Tock 静态地址应用加载为何受 MPU 对齐限制、固定地址(TbfHeaderV2FixedAddresses)与 PIC 应用在 TBF 头中的表达方式,以及CommandReturn/SyscallReturn的返回值变体如何在 32 位与 64 位架构间保持 ABI 一致性。
会议背景与议程概览
本次会议于 2022 年 4 月 1 日召开,与会者包括 Hudson Ayers、Philip Levis、Alexandru Radovici、Leon Schuermann、Johnathan Van Why、Brad Campbell、Alyssa Haroldsen、Branden Ghena、Vadim Sukhomlinov、Jett Rink 等 Tock 核心维护者。会议开头简短汇报了两项动态:Rust 社区在稳定性问题上没有进展(Brad 报告),Hudson 将前往 EuroSec 会议并在会上介绍 Tock 相关论文。
随后会议进入三个技术议题:
- Ergonomic Static Apps(PR #3001):讨论静态地址(fixed address)应用在 Tock 内核中的加载问题,尤其是地址与 MPU 对齐冲突时进程无法加载的现象。
- Mixing Fixed Position and PIC apps(tockloader issue #82):讨论 Tockloader 如何在同一个设备上混合放置"固定地址编译"与"位置无关(PIC)"两类应用。
- Command Result Type for Pointers(PR #3003):提议为
Command系统调用新增一个"成功并返回指针"的返回值变体,使胶囊(capsule)能以平台无关的方式向用户态返回指针。
下面按议题逐一展开,并给出仓库内对应的源码依据。
议题一:Ergonomic Static Apps——静态地址应用为何会在加载时失败
1.1 讨论起点:内核该不该为静态地址应用做更多事
PR #3001 的目标是让"以固定地址链接(statically linked at a fixed address)"的应用更容易被内核加载。Hudson 首先提出一个根本性问题:这类逻辑应该放在 Tock 内核里,还是推给应用的 Makefile / 链接过程?内核代码的每一点体积对所有应用都有代价——即使那些根本不需要固定地址功能的应用也要为其买单。另一种思路是:让应用在构建时就自己选择合适的地址。
Brad 从内核开发者的角度给出反证:在开发内核期间,应用 RAM 的起始地址是会变化的。这限制了应用可以放置的位置和数量。如果内核能提供更多灵活性会更方便,因为你很难预知将来会需要哪些地址,而为大量可能地址分别链接应用又很耗时。
Hudson 进一步点明根因:在应用构建时,你并不知道内核内存区域的结束位置(build time 时内核镜像最终占用的 RAM 边界未定),因此静态链接应用时无法保证其固定地址最终落在合法范围内。
1.2 问题本质:MPU 对齐导致固定地址应用加载失败
Phil 最初怀疑这是不是 Tockloader 的问题,但 Brad 澄清了当前内核的行为:
内核查看已加载的应用,检查它是否需要固定地址,然后尝试在其请求的地址处启动应用区域。问题在于:这个地址必须对 MPU 良好对齐(well-aligned)。如果不对齐,创建 MPU 区域会失败,进程就不会被加载。
Brad 用一个具体例子解释对齐问题如何随需求增长而恶化:
假设应用需要 2 字节内存,可以放在地址 2(对齐可用)。但如果把内存需求增加到 4 字节(例如增大栈),应用仍然固定在地址 2,此时对齐就不再满足,加载就会失败。
也就是说,固定地址一旦写入链接脚本,它不会随应用内存需求的增长而自动调整;而 MPU 区域要求起始地址满足其对齐约束,于是"编译时能工作的地址"可能因为后续 RAM 需求变大而失效。
1.3 为什么地址是硬编码的:libtock-c 与 libtock-rs
Branden 问"这个地址到底是怎么选出来的?" Brad 回答:它是应用编译时在链接脚本里硬编码(hard-coded)的。Alexandru 追问这是常量还是按需内存计算,Brad 确认在 libtock-c 中是固定的,Johnathan 补充 libtock-rs 中同样是硬编码。
1.4 一个可能的修复方向:允许 MPU 区域起始地址低于应用起始地址
Brad 描述了 PR 的核心思路:如果应用从地址 2 开始但需要 5 字节内存,可以让 MPU 区域从地址 0 开始——应用仍然从 2 开始,同时拥有完整 5 字节。也就是说,不再要求"应用起始地址 == MPU 区域起始地址",而是允许 MPU 区域向前(低地址方向)扩展。
Leon 担心这会显著增加复杂度(可能需要向后检查碰撞)。Brad 认为可以缓解:MPU 接口基于"可用内存的起始位置"来划分区域,而已有应用之后的可用区起点是已知的,所以向前的扩展理论上不会与之前应用冲突——但需要再确认。
1.5 替代方案:能否在链接时做更好的检查
Leon 提出另一个方向:在链接时做更严格的检查来规避这个问题。Hudson 指出困难:链接时不知道内核区域的结束位置。Brad 补充:固定地址应用理论上可以选一个更靠后的地址(浪费一些内存),这样即使内核增长也不至于重编译应用,但这会得到相当次优的结果(Leon 语)。
Branden 则担心改动链接流程会非常困难,而 PR 的做法(内核侧调整)看起来能直接解决问题。Hudson 提到该实现大约增加200 字节左右的内核代码,他倾向于反对把这类非常专用(special-purpose)的逻辑放进内核、浪费所有应用共享的内核空间。
1.6 Tockloader 的困境:无法决定选哪个地址
Phil 提出一个关键使用场景:如果只给你二进制(不能重编译),你仍然应该能加载它。Brad 随即点出 Tockloader 面临的困境:
Tockloader 不知道应用 RAM 的起始位置应该是哪。如果你为一个应用编译了 100 个不同的 RAM 位置,它不知道如何选择。如果有一个应用是为地址 2 编译的,另一个是为地址 16 编译的,Tockloader 不知道该选哪个。
在动态加载与静态加载并存的情况下(Phil),Tockloader 更无从判断。即使只有静态应用,Tockloader 也需要知道 MPU 信息、内核内存边界和应用内存大小才能做出选择(Brad)。Leon 提议让内核向 Tockloader 暴露信息,但 Brad 提醒这又会增加内核代码体积。
1.7 可重定位应用的未来:RISC-V 有机会,ARM 仍受工具链限制
Alexandru 提出关键问题:距离可重定位(relocatable)应用还有多远?GCC for Rust 的进展能否解决这个问题?
Johnathan 的回答值得注意:
上游 libtock-rs 是链接到一个固定内核上的,偶尔需要更新并调整链接脚本。未来在 RISC-V 上有一定信心实现可重定位应用;但在 ARM 上,LLVM 仍然做不到,所以还需要 GCC for Rust。
此外 Johnathan 提到 TI50(Google 的 Titan Security Key 相关项目)在做自己的内核+应用组合方案。Phil 则提醒一个远期问题:由于未来应用会被签名,拿到二进制后不能触碰它,但仍然希望加载它们。Hudson 补充:通常签名的是TAB(Tock Application Bundle)而不是 ELF,所以参数信息是受保护的、完整的。Johnathan 指出,既然链接脚本由 libtock-rs 生成,必要时可以动态生成带正确尺寸的链接脚本。
1.8 结论与源码印证
会议并未要求在本次彻底解决,Hudson 计划会后从 libtock-rs 侧再审视链接器能否获得全部正确信息;如果很难,就优化内核侧逻辑。Brad 提出了一个可行的内核侧最小改动方向:"把当前'向前扫描内存、推进到满足应用需求'的逻辑,改成'推进到最近的 MPU 区域'"。
当前仓库中的源码印证:固定地址加载逻辑确实实现在内核进程创建路径中。kernel/src/process_standard.rs 的ProcessStandard::create在分配内存前读取pb.header.get_fixed_address_ram():
- 若应用请求的固定地址恰好等于
remaining_memory当前起始地址,直接使用; - 若请求地址更靠后且差值在剩余内存范围内,通过
raw_slice_split_at_mut向前跳进(跳过一段 RAM 不用),使内存区从应用所需地址开始; - 若请求地址更靠前(已越过),则返回
ProcessLoadError::MemoryAddressMismatch加载失败。
这正是会议讨论中"支持跳过一段 RAM 让内存区从进程需要的地址开始"的实现(源码注释明确写道:"Right now, we only support skipping some RAM and leaving a chunk unused so that the memory region starts where the process needs it to.")。而在 kernel/src/process_standard.rs 中,内核还会在allocate_app_memory_region之后二次校验实际分配的 RAM 起始地址与 TBF 头中声明的固定地址一致,不一致则返回MemoryAddressMismatch。这与 Brad 描述的"创建 MPU 区域失败导致进程不加载"的行为一一对应。
议题二:Mixing Fixed Position and PIC apps——固定位置与位置无关应用的混合放置
2.1 议题来源与 Brad 的第一直觉
该议题来源于 tockloader issue #82。Hudson 认为这是一个值得支持的使用场景,提议作者为此投入了大量工作,希望与会者审阅并反馈。
Brad 的第一反应非常直接:"我以为这会是个简单改动——先把固定位置应用放在前面,再把可重定位(PIC)应用放在后面,不明白为什么需要更复杂。" Hudson 解释:提案里引入SAT 求解器(SAT solver)是为了优化放置方案;但对这种不常见的使用场景,次优放置通常是可以接受的。Hudson 同时质疑:为什么需要重构 TBF 处理和 App 类?
2.2 关键约束:TBF 对"混合"缺乏表达
Brad 指出一个曾经的 bug(现已修复):一旦遇到一个固定位置应用,就假定后续所有应用也都是固定的——这是错误假设,但 TBF 头格式本身并没有专门为"混合模式"设计表达,因此可能确实需要改动。Hudson 对此的回应是:或许可以假设"一旦看到固定应用,后续都是固定应用"来简化处理。
2.3 依赖与审计考量
关于引入依赖(SAT 求解器等),Brad 只担心复杂度与可移植性:
纯 Python 依赖没问题。这个 SAT 求解器让我不安——它有不少原生(native)组件,可能让 Tockloader 在某些平台上无法运行。
Hudson 补充:真正需要严格审计的场景基本不使用 Tockloader,所以对依赖的顾虑有限。最终 Brad 表态可以跟进核实提案到底真正需要哪些改动,并认为"在闪存先于 RAM 耗尽之前会塞下这么多应用"的场景并不常见(但 Hudson 提醒这取决于平台)。
2.4 源码印证:TBF 头中的固定地址字段
会议讨论的"固定位置应用"在 TBF(Tock Binary Format)v2 头中由TbfHeaderFixedAddresses(TLV 类型 5)表达。libraries/tock-tbf/src/types.rs 中定义:
/// Optional fixed addresses for flash and RAM for this process. /// /// If a process is compiled for a specific address this header entry lets the /// kernel know what those addresses are. /// /// If this header is omitted the kernel will assume that the process is /// position-independent and can be loaded at any (reasonably aligned) flash /// address and can be given any (reasonable aligned) memory segment. #[derive(Clone, Copy, Debug, Default)] pub struct TbfHeaderV2FixedAddresses { /// The absolute address of the start of RAM that the process expects. start_process_ram: u32, /// The absolute address of the start of the process binary. This does _not_ /// include the TBF header. start_process_flash: u32, }语义要点(与会议讨论完全吻合):
- 不包含该字段→ 内核假定应用是位置无关的(PIC),可加载到任意(合理对齐的)flash 地址、分配任意(合理对齐的)内存段;
- 包含该字段→ 内核在设置进程时会校验这些值;
- 只想固定其一→ 另一个字段填
0xFFFFFFFF表示"不固定"。
对应的访问器在 libraries/tock-tbf/src/types.rs:get_fixed_address_ram()与get_fixed_address_flash()都通过0xFFFFFFFF哨兵值判断是否返回None。该 TLV 类型的注册见 libraries/tock-tbf/src/types.rs(TbfHeaderFixedAddresses = 5)。
这解释了议题二的本质:TBF 头是"要么固定、要么 PIC"的二元表达,Tockloader 要做混合放置,就必须解析每个应用 TBF 头中的fixed_addresses字段,先放置固定地址应用(受限于它们声明的 flash/RAM 地址与 MPU 对齐),再在剩余空间放置 PIC 应用——这正是 Brad 所说"固定位置应用放前面、可重定位应用放后面"方案的底层依据。
议题三:Command Result Type for Pointers——Command 返回指针的 ABI 设计
3.1 提案内容:新增"success with pointer"变体
Jett Rink 在 PR #3003 中提议为Command系统调用新增一个成功变体:"success with pointer"。其语义与u32/u64返回值不同:内核不保证该指针的有效性(validity)或可访问性(accessibility)——它只是一个地址值。
两个驱动用例:
- 返回指向内核持有缓冲区(kernel-owned buffer)的指针:应用不知道该缓冲区的地址。缓冲区通过某个驱动租借给应用使用,内核需要先做 MPU 共享(MPU share),使其对特定应用独占;应用读取、修改后通知内核完成。这是"单个内核缓冲区、在应用间移动访问权"而不是"每个应用各自建缓冲区占空间"的模型(Phil 确认了这一点)。
- 系统管理器类信息:应用可能想知道自己地址空间内的某些信息,这涉及把指针交给应用。
3.2 关键设计分歧:32 位参数 vs usize
Phil 首先提出疑问:之前不是打算"返回一个既有指针的偏移量(offset)"吗?Jett 说明两个用例后,Leon 抛出核心架构担忧:
在 Tock 2.0 中,一类系统调用(如 allow、memop)因为必须与指针交互,本身就携带指针;而
command概念上应该在任何平台上行为完全一致。我不赞成把u32参数加宽成usize,因为在 64 位平台上开发的东西可能在 32 位平台上坏掉。所以驱动 trait 的参数应限制为 32 位值,而不是跨平台保持一致。
Jett 回应其"最小改动"方案:
我采取了最小改动。command 的输入参数保持不变(仍为 u32/usize 语义下的 32 位值);指针作为输入仍通过 allow 或 memop 传递;本改动只是让内核可以通过 command 返回指针。command 的输入永远不需要 usize。
而返回的指针值本身是usize 宽度的("it's a usize-width value"),以便在 64 位平台上能容纳地址。
3.3 memop 与 64 位宿主模拟(host emulation)之争
Phil 追查 memop 的行为:memop 返回一系列起始地址、flash 地址等,TRD104 中规定其返回类型是success-with-u32(见 doc/reference/trd104-syscalls.md)。Rust 结构体内部用usize保存没有问题,但用户态最终应假定它是 32 位值。Phil 质疑:宿主模拟(host emulation,在 64 位机器上原生跑内核)是否真的传了 64 位值?
Jett 承认需要核查;Phil 再次追问"为什么不直接在 32 位模式下跑宿主模拟?" Jett 回答他们是在自己的机器上原生运行的;Alyssa 则明确反对:"我不想仅为这个重构我们的工具链(toolchain)"——Phil 针锋相对:"可你现在要求重构的是内核。"
Alyssa 提出不同观点:
我认为没有 usize 返回类型是个 bug。在我看来,应该允许这种返回类型里携带可变宽度(variable-width)的数据。
Leon 冷静拆分出两个独立问题:
memop 的问题可以通过 64 位 ABI 解决,但关于驱动和 command 的讨论仍然存在。两者相关,但确实是两个独立议题。
Jett 认同,并强调新增 success-with-pointer 变体即使在 32 位 ABI 下也是良好定义的,同时为未来 64 位 ABI 铺路。
3.4 会议结论
Phil 认为这是棘手问题:command 返回指向内核内存的指针,与传统 Tock 内存模型不同,但他不觉得有显著问题。Jett 总结其价值:给胶囊一个一等公民(first-class)的指针返回方式,使"返回指针"在平台无关的驱动代码中成为良定义的操作;内核不承诺可访问性,只是一个 usize 宽度的值。Hudson 提醒两年前的会议已经讨论过该话题(Phil 缺席),应避免重复纠结;Phil 表示将与 Jett 线下深入讨论。
3.5 源码印证:CommandReturn 与 SyscallReturn 的变体约束
当前仓库的 kernel/src/syscall_driver.rs 精确体现了这次讨论确立的架构原则:
/// Possible return values of a `command` driver method, as specified in TRD104. /// /// This is just a wrapper around [`SyscallReturn`] since a `command` driver /// method may only return primitive integer types as payload. /// /// It is important for this wrapper to only be constructable over variants of /// [`SyscallReturn`] that are deemed safe for a capsule to construct and return /// to an application (e.g. not /// `SubscribeSuccess`). This /// means that the inner value **must** remain private. pub struct CommandReturn(SyscallReturn);CommandReturn是SyscallReturn的安全子集包装,其构造器被限制为纯数值载荷变体(kernel/src/syscall_driver.rs):
- 失败:
failure/failure_u32/failure_u32_u32/failure_u64 - 成功:
success/success_u32/success_u32_u32/success_u32_u32_u32/success_u64/success_u32_u64
这印证了 Jett 在会议中的"最小改动"主张:command 不接收指针作为输入(指针输入走 allow / memop),返回值也保持数值类型。而SyscallReturn在 kernel/src/syscall.rs 中为整个内核定义了更完整的变体集,其中确实包含指针相关变体:
SuccessAddr(usize):成功且携带一个地址宽度的值,不隐含对进程的访问权限;SuccessPtr(CapabilityPtr):成功且携带一个指针,携带 provenance、隐含访问权限;AllowReadWriteSuccess(*mut u8, usize)、AllowReadOnlySuccess(*const u8, usize)、SubscribeSuccess(*const (), usize)等:allow/subscribe 专用指针返回。
SuccessAddr与SuccessPtr的区分("不隐含访问权限" vs "携带 provenance 并隐含访问权限")正是会议中"no guarantees about validity or accessibility"讨论在代码层面的落点:command 返回的地址值不向应用承诺可访问性,与 allow/subscribe 返回的、隐含权限的指针严格区分。
系统调用 handler 侧(kernel/src/syscall.rs)通过from_command_return把CommandReturn转换回SyscallReturn,其注释也强调"command 驱动方法只能返回原始整数类型作为载荷"——用户态收到的仍是 TRD104 定义的寄存器值。
此外,kernel/src/syscall_driver.rs 中的SyscallDriver::commandtrait 签名保持了输入参数的 32 位语义:
fn command( &self, command_num: usize, r2: usize, r3: usize, process_id: ProcessId, ) -> CommandReturn { CommandReturn::failure(ErrorCode::NOSUPPORT) }与 TRD104 对 command 的 ABI 描述一致(doc/reference/trd104-syscalls.md):寄存器 r0 为 Driver Number、r1 为 Command Number,均为u32语义;每个 command 实例(Driver + Command number 组合)自行规定返回变体。TRD 同时规定 Command Identifier 0 是驱动存在性检查,若驱动未安装内核返回NODEVICE。
三个议题的共性与后续影响
4.1 共性问题:ABI 与 MPU 约束下的"地址"处理
三个议题表面独立,实则共享同一核心矛盾:Tock 的地址空间布局由 MPU 对齐约束、内核 RAM 边界的不确定性、TBF 头的静态声明三者共同决定:
- 静态地址应用把地址写死在链接脚本里(议题一),承受 MPU 对齐与内核增长的双重风险;
- TBF 头用
fixed_addressesTLV 表达"固定或 PIC"的二元事实(议题二),混合放置只能依赖 Tockloader 的解析与排序; - command 返回值变体(议题三)则要保证在 32 位平台上良好定义,同时为 64 位 ABI 留出空间——指针值可以以 usize 宽度返回,但输入参数与 ABI 语义仍锁定在 32 位。
4.2 与当前仓库实现的对应关系
| 会议议题 | 对应仓库实现 | 关键位置 |
|---|---|---|
| 固定地址应用的 RAM 对齐与跳进 | 进程创建时的内存区调整与二次校验 | kernel/src/process_standard.rs |
| 固定地址在 TBF 头中的表达 | TbfHeaderV2FixedAddresses(TLV 5)与访问器 | libraries/tock-tbf/src/types.rs |
| command 返回值变体约束 | CommandReturn安全子集包装 | kernel/src/syscall_driver.rs |
| 指针返回的权限语义 | SuccessAddr/SuccessPtr区分 | kernel/src/syscall.rs |
| command ABI 定义 | TRD104 寄存器约定与 Command 0 语义 | doc/reference/trd104-syscalls.md |
需要说明的是,本仓库当前快照中的CommandReturn尚未包含 "success with pointer" 变体——会议中 Jett 的提案(PR #3003)以"新增SuccessAddr/SuccessPtr风格的指针返回能力"为方向继续演进,最终在SyscallReturn层面落地了地址/指针返回变体并保持CommandReturn的安全子集约束。这也符合会议"不急于当场定案、会后线下深入"的结论。
4.3 给内核与工具链开发者的可操作要点
- 加载固定地址应用前,确认其声明的
start_process_ram满足目标平台 MPU 的起始地址对齐要求;内存需求增长(如增大栈)可能导致原本对齐的固定地址失效,需要重新链接或依赖内核的"跳过 RAM"逻辑。 - 为应用选择固定地址时,宁可偏后浪费少量内存,也不要紧贴当前内核边界——内核镜像增长后无需重编译应用(Brad 观点);但过于靠后同样可能超出剩余 RAM。
- Tockloader 做混合放置时,按"固定位置应用优先、PIC 应用随后"排序放置,并注意避免引入含原生组件的 SAT 求解器类依赖,保证跨平台可运行性。
- 编写胶囊的 command 时,输入参数保持 32 位语义、指针输入走 allow/memop;若需要向应用返回地址值,使用不隐含访问权限的返回变体,避免在 64 位宿主与 32 位目标之间制造 ABI 不一致。
参考与延伸阅读
- 会议纪要原文:doc/wg/core/notes/core-notes-2022-04-01.md
- TBF v2 头类型定义(固定地址、权限、程序头):libraries/tock-tbf/src/types.rs
- 进程创建与固定地址内存区调整:kernel/src/process_standard.rs
- 系统调用返回值变体与 ABI:kernel/src/syscall.rs、kernel/src/syscall_driver.rs
- 系统调用规范 TRD104(command、memop 的寄存器与返回类型约定):doc/reference/trd104-syscalls.md
- 既有讨论记录(可对照本议题的延续性):doc/wg/core/notes/core-notes-2020-06-12.md、doc/wg/core/notes/core-notes-2020-05-29.md
- 操作系统
- 嵌入式
- 嵌入式OS
【免费下载链接】tock
A secure embedded operating system for microcontrollers
相关推荐
Tock 用户态服务与 IPC 共享内存设计解析:Network WG 2026-08-31 会议技术讨论
Tock 用户态服务与 IPC 共享内存设计解析:Network WG 2026 08 31 会议技术讨论 本文基于 Tock 内核仓库中 Network Wo
操作系统嵌入式嵌入式OSTock 威胁模型与加密 HIL 设计决策回顾:解读 2020-04-03 Core 工作组会议纪要
Tock 威胁模型与加密 HIL 设计决策回顾:解读 2020 04 03 Core 工作组会议纪要 导读 本文以 Tock 嵌入式操作系统 Core 工作组
操作系统嵌入式嵌入式OSTock Network WG 会议纪要精读:Userspace Services 与 IPC 共享内存的设计走向
Tock Network WG 会议纪要精读:Userspace Services 与 IPC 共享内存的设计走向 本文基于 Tock 网络工作组(Networ
操作系统嵌入式嵌入式OS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考