8

嵌入式领域的Rust语言

 3 years ago
source link: https://zhuanlan.zhihu.com/p/352945545
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

嵌入式领域的Rust语言

热爱古琴的Rust语言开发者

Rust语言是二十一世纪的语言新星。Rust被人广泛承认的一点,就是因为它能运行在多样的目标上,从桌面和服务器设备,到资源有限的嵌入式设备。

我们可以用适合来评价一门语言和技术。Rust非常适合开发嵌入式应用,它是一种和C相仿的、能应用于嵌入式设备开发的编程语言。

操作系统都是从裸机设备开始运行的,Rust语言的这一点也意味着,它能很好地用于编写操作系统。无论是应用层还是内核本身,Rust都是极富竞争力、值得投入时间的技术选项。

二十一世纪的裸机编程语言

在这个互联网全面普及、性价比设备应用更广的时代,安全和可靠性成为一门语言必须考虑的因素。Rust语言采用移动语义,拥有严格的代数类型系统以及生命周期、所有权模型;相比传统的编程语言,这些模型能在合适的时候释放所用资源,减少漏洞的出现。此外,通过语义检查,Rust能在编译期有效寻找内存和线程安全问题,降低开发和测试的负担。

Rust语言是的运行效率高、开发效率好、适用范围广。作为一门编译型语言,它直接编译输出到汇编代码,通常公认裸机的Rust语言性能在C语言级别,拥有较高的运行效率。Rust语言的开发效率很高,文档完善、编译器提示有帮助,能节省软件开发所需的时间。它能应用在多个平台和指令集中,这包括裸机平台;处理核、操作系统厂家还可以提供自己的编译目标,无需厂家自己重新开发、提供工具链。

Rust语言出彩的地方在于,它向嵌入式平台引入了大量新的编程技术。这包括了闭包、过程宏等传统上用于函数式编程的技术,和多态、虚函数表等面向对象语言的技术。新编程技术的引入,扩充了开发者的选择。即使彻底理解Rust的编程概念有一定难度,但这些易用的新技术,让开发者只需阅读实例代码,便可快速进入开发状态。这些新技术的引入,是嵌入式平台从未有过的,Rust能提高开发者的工作效率,降低平台间迁移的学习时间和成本。

裸机上的过程宏

传统用于嵌入式平台的编程,我们加快开发速度使用的宏,常常基于语法字符串的替换和修改。Rust语言扩充了宏的概念,提出了基于语法树的“过程宏”编程方法,让宏语法更容易使用、编写更方便。

“过程宏”是接收Rust代码作为输入,操作这些代码,然后产生另一些代码的过程。它和字符串的替换不同,是从语法树到语法树的替换。开发一个过程宏,可以使用简单的定义过程,或者有工作量的属性宏定义过程。简单的定义中,我们编写代码,给出宏的输入有哪些,要翻译到哪些输出代码,这样就完成了一个宏的定义。属性宏定义则允许完成语法树分析、代码生成甚至代码优化的过程,就需要编写专门的“属性宏库”,借用Rust编译器的一部分,完成宏代码的转化和输出。

过程宏是基于语法树的分析过程,借助“树”的结构我们能理解它的一些特点。因为Rust语法树的子树也是Rust代码,所以宏的定义内也可以完成语法分析,这就为代码编辑器的提示和补全提供了便利。一个语法项目不可能同时属于两颗不是亲子关系的子树,因为如果属于两颗子树,将和语法树的树根产生环,就和语法树的定义相违背,所以语法项目都是独立的,宏内代码的解析不会影响外界代码的解析。

这样的独立性也就是“卫生宏”思想的提出,Rust的过程宏可以理解为代码的“内部展开”,不影响代码的上下文。正因为Rust过程宏产生完整的语法子树,它的定义不需要额外的界符,因此只需要满足Rust语法就可以了。

在过程宏的定义之外,Rust语言提供了大量便于嵌入式开发的标签。“align”标签定义内存对齐的方式,“link_section”标签给定代码要链接到的段或区。这样,过程宏可以包装各种各样的标签,Rust语言的用户可以方便地使用,而不需要深入宏了解代码的具体要求。Rust语言定义的过程宏可以导出到包外,给其它的库使用,这有利于嵌入式Rust生态的搭建和共享。Rust语言宏灵活的特性,让宏在更多的领域有可用之处,更好地服务嵌入式平台的开发工作。

嵌入式中的模块化编程

Rust语言拥有很好的模块化编程概念。传统平台的Rust语言中,社区总结出了“模块-包-项目”的模型。这个模型也适用于嵌入式平台,增加协作开发的效率,更好地共享生态。

Rust的模块化编程分为模块、包、项目三级。模块是Rust语言可见性分划的最小单位,语言中提供了专门的关键字,来区分不同模块的代码和可见性,是由Rust语言本身确定的。在Rust语法中,“mod”是定义模块的关键字,“pub”是定义可见性的关键字。

包是Rust项目的二进制目标,这个等级是由Rust工具链给定的。每个包有版本号、作者和许可协议等元数据,要依赖和使用的库也要登记到包中,以便共同编译。库的特性有点像传统语言的条件编译,也是以包为单位规定的,每个包使用的库可以开启不同的特性,但库在同一个包中开启的特性是相同的。

“项目”这一层并非由Rust语言给定;人们开发软件时,发现一个解决方案中包含多个二进制目标是非常好的,总结之后就出现了项目的抽象模型。项目由核心和外围包组成,或者是功能相近的一组包,它通常由同一个团队组织和维护,可以在项目上添加扩展。项目在习惯上由核心包到功能包,以依赖的形式构成。实践中,“项目”可以放在同一个工作空间里,以统一管理和发布编译版本。

Rust将模块化编程引入到嵌入式开发中,也可以方便地编写测试和性能检测代码。模块化编程能提高Rust嵌入式开发者的工作效率,适应现代化嵌入式软件的需求。

搭建Rust嵌入式生态

生态是软件工业不可或缺的一部分。从编译器到软件支持,嵌入式Rust目前已经拥有良好的基础生态。此外,操作系统内核也是嵌入式编程的重要部分,嵌入式Rust和内核开发也有较好的相容度。

Rust语言的嵌入式生态。图源:洛佳.Rust嵌入式开发[R],Rust中国大会2020

你的架构和指令集

嵌入式Rust的应用支持分为两个部分:一个是目标处理核的支持,一个是芯片外设的支持。

针对目标处理核,首先我们要编译Rust到这个指令集架构。Rust语言提供丰富的编译目标,主流的编译目标都有很好的支持;此外,如果有自主研发的指令集架构,可以为Rust添加自己的编译目标。编译完成后,还需要编写微架构支持库和微架构运行时。微架构运行时提供最小的启动代码实现,能搭建一个适合Rust代码运行的环境。微架构支持库简单包装汇编代码,允许应用代码操作寄存器、运行特殊的指令,作为编译器系统的补充。这之后,Rust对这个指令集架构的代码运行支持就完成了。

嵌入式应用定义了各有特点的中断控制器,有些是指令集架构定义的,有些是芯片设计厂家自己定义的。嵌入式Rust要支持这些中断控制器,需要在微架构运行时中添加处理和封装部分,或者作为通用架构的补充,在专用架构的支持库中添加专有架构的中断运行时。架构虽然定义了标准,但基地址、中断数量等配置可能相互不同。这些元数据配置可以放在外设访问库的中断部分,和架构支持库共同构成中断控制器的支持。

目标的处理核定义了调试接口和闪存烧写算法,我们需要在调试器软件中编写这些算法。社区通用的软件“probe-rs”是很好的调试器实现,可以替代OpenOCD,作为非常好的Rust语言调试软件。如果自己的操作系统有软件调试接口,可以添加操作系统调试器的载荷,共同完成调试软件的部分。只要处理器厂商实现了调试接口,提供相关的文档,配套的Rust软件可以尽快完成,方便各种技术的开发者调试和使用。

嵌入式生态的标准

起初嵌入式开发者会为每个芯片都编写一次代码。随着生态的发展,大家认识到,需要提供一个基本的抽象,大家都围绕着抽象去编写,就能省下为大量外设反复编码的时间。embedded-hal就是这样的标准,它是Rust语言的嵌入式外设抽象,支持大量的片内和片外外设,包括传感器等,很好地扩充了嵌入式的生态。

embedded-hal是统一的Rust语言标准,它是针对外设功能本身的抽象,是抽象的集合,具体实现由实现库去完成。它的扩展性很好,比如“SPI-GPIO扩展器”外设输入SPI接口抽象,输出GPIO的抽象,很多模块都是抽象到抽象的过程,就可以方便的极联、衔接和嵌套,整合更多的项目;这就非常容易为新的芯片编写支持库。

市场上海量的芯片都支持embedded-hal标准。K210、GD32V和BL602系列的芯片都提供很好的embedded-hal实现库。要编写embedded-hal标准的支持库,只需要机器生成外设库,然后编写中间层库,就能完成对此标准的原厂支持。

Rust与操作系统内核

操作系统也是嵌入式应用。常见的操作系统如按是否包含虚拟内存区分,有不含虚拟内存的实时系统,和包含虚拟内存传统操作系统。基于微架构的支持库和运行时库,操作系统内核可以很方便地编写。

社区中提供了大量成熟的操作系统运行时。如rCore系列操作系统是第一个基于RISC-V架构的完整Rust操作系统,尤其适合教学使用。RTIC框架是中断驱动的异步实时系统,完全针对应用使用Rust的宏语法生成,拥有极高的效率。Tock系统是针对微处理器的安全实时系统,已经用于手表、智能路标和加密狗等产品。

针对操作系统和应用程序开发,Rust是适合编写硬件驱动的语言。如果使用有产权的代码,可以以混合链接的形式,与Rust代码联合编译为二进制使用。系统模块、插件和动态链接库等等都能受益于Rust语言内存安全的特性,适合现在对安全敏感的开发需求。

物联网系统要求嵌入式的操作系统能够连上网络。Rust嵌入式社区也在探索射频连接的技术标准,包括蓝牙、WiFi等硬件标准。smoltcp是社区提供的非常好的TCP协议栈实现,它可以代替lwip,在嵌入式系统领域高效、安全地完成网络传输。搭配缓冲区和协议库,物联网操作系统就可以连上网了。

RustSBI:新型操作系统引导软件

我们在开发操作系统内核时,有的内核直接运行在裸机上,有的还依托于一个运行环境。在RISC-V上,“SBI”就是这样的运行环境。它除了引导启动内核,还将常驻后台,提供操作系统需要的实用功能。

RISC-V标准中,“SBI”意味着“操作系统二进制接口”,运行在其上的操作系统会通过环境调用“ecall”指令,陷入到二进制接口的实现中,由其调用具体硬件的实现功能。这种实现被称作“SBI实现”,社区常用的实现有开源的OpenSBI。RustSBI是鹏城实验室“rCore代码之夏-2020”活动提出的SBI实现,它是全新的操作系统引导软件。

实现与模块组成

RustSBI由几个功能模块组成。硬件环境接口实现了RISC-V SBI v0.2版本的接口,能运行支持此版本的操作系统。硬件运行时则是SBI实现运行在裸机环境的必要模块,它将由硬件启动,开始运行所有的RustSBI模块。SBI的初始化完成后,将进入引导启动模块,这里将发挥SBI标准“引导启动”的功能,最终启动操作系统内核。另外,兼容性模块能完成硬件到硬件间的支持,能模拟旧版硬件不存在的指令、寄存器,进一步延长操作系统的生命周期。

去年12月,RustSBI的0.1版本在深圳的Rust中国社区2020年年会上发布。使用目前最新的0.1.1版本,RustSBI已经支持大量SBI标准提出的功能,支持大量自定义的扩展功能;完全使用安全的Rust语言编写,提高开发效率。开发Rust语言的操作系统内核,可以统一编译工具链。另外,RustSBI已经被RISC-V组织收录入RISC-V SBI标准,它的实现编号为4。

RustSBI是一个库,它以库的形式设计的初衷是,便于平台开发者“积木”式地引入库的模块,为自己的硬件目标开发SBI支持。虽然RustSBI提供了QEMU、K210平台的参考实现,但应用开发者不应当将自己的目标也加入参考实现中,而是在自己的仓库里引用RustSBI的模块,可以选择参考这些实现的内容,最终完成完全可控的开发过程。这两个平台的使用范围较广,参考实现也会长期维护,以发现RustSBI本身可能的少量问题,并及时修补完善。

为什么用Rust开发RustSBI呢?我们认为,相比使用C语言,嵌入式Rust的生态圈在协调发展阶段,它容易支持新硬件,Rust语言较强的编译约束也提高了硬件代码的安全性。

硬件到硬件的兼容性

RISC-V是快速更迭的指令集规范。我们为新版RISC-V硬件编写软件,会遇到与旧版硬件不兼容的情况。硬件和硬件之间的兼容性,也能通过软件完成——这是RustSBI提供的功能与亮点之一。

RustSBI实现的硬件兼容性,是靠捕获指令异常完成的。例如,K210平台实现的是1.9.1版本的RISC-V特权级标准,它规定了旧版的页表刷新指令;而目前最新的1.11版标准,规定的是新版的刷新指令。为新标准编写的操作系统内核,使用新版刷新指令,会因为K210硬件无法找到新版指令,抛出非法指令异常。这个非法指令异常被RustSBI捕获,它解析后,发现是新版的页表刷新指令,便直接在硬件上运行旧版的指令,完成指令的页表刷新功能。

这种硬件兼容性,目前能支持新增的指令和寄存器。一切情况下,指令、寄存器在仍然存在,但新版中修改了它们的功能和意义。只靠RustSBI软件本身,就不足以提供兼容性支持了。如果RISC-V芯片实现提供特定的兼容性外设,比如这个外设能拦截特定CSR寄存器的访问指令,就可以在功能修改的寄存器访问时,产生一个可供软件捕获的中断。这样的外设设计之后,使用RustSBI软件,将能支持功能修改的指令和寄存器,将进一步提升操作系统内核的硬件兼容性。

兼容旧硬件,也是兼容未来新硬件的过程。未来的RISC-V标准快速发展,将与目前的硬件标准产生一定的差异;在硬件不变的前提下,未来软件能对当前的硬件兼容,就能延长软件的生命周期。或许,我们未来升级RISC-V上的操作系统,只需要更换硬件中的RustSBI固件,就能完美兼容最新标准的操作系统了。升级原有系统的硬件也非常容易,替换RustSBI固件就能达到升级效果。

另外,硬件兼容性也意味着实现硬件上缺少的指令集。当这些指令集运行时,就会陷入到软件中,由RustSBI软件模拟这些指令,最终返回,这个过程应用软件不会有感知。当然,这种软件模拟过程可以满足正确性,效率不如新版的硬件,但临时运行一个新版的软件、体验新版的指令集还是足够的。当模拟指令的过程多到影响性能时,也就是硬件该升级的时候了。

RustSBI与嵌入式Rust生态

在RustSBI的实现中,多次使用“embedded-hal”的实现完成编写过程。“embedded-hal”是Rust嵌入式的外设规范,它对大量厂家的外设提供了软件支持。只要厂家的硬件支持“embedded-hal”,只需要编写部分抽象接口代码,RustSBI支持就可以快速地开发完成。

硬件处理核和SoC系统的开发也受益于设计好的RustSBI软件架构。“RustSBI很快速地实现了仿真环境的双核测试,”华中科技大学的社区贡献者车春池说,“这能为处理核提供丰富的测试环境,在开发高性能RISC-V处理核中非常重要。”

无论硬件和软件,我们都乐于看到各个应用领域积极互动,嵌入式Rust生态的发展过程得到加快。“embedded-hal”本是裸机外设的标准,RustSBI将这个标准运用在引导软件上,能加速裸机外设的开发和建设,也能更快适配SBI标准到平台上。

借这个项目,我们很高兴能参与嵌入式领域Rust语言的建设,希望这些微小的技术更新和迭代,最终能回馈到未来物联网行业更轻便、更安全的开发体验中去。

本文原载于《Rust中文月刊》2021年第1期。原标题:《嵌入式领域的Rust语言》

作者简介:洛佳,华中科技大学网络空间安全学院本科生,热爱操作系统、嵌入式开发。RustSBI项目作者,3年Rust语言开发经验,社区活跃贡献者。目前致力于向产业、教学等更多的领域推广Rust语言。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK