网易首页 > 网易号 > 正文 申请入驻

Linux用Rust受阻?Android工程师发文:Rust改写C代码太简单了!

0
分享至

不久前,开源维护者 Wedson Almeida Filho 决定退出一直参与的 ,起因是社区中一些 C 语言内核开发者“似乎决心让 Rust 维护者的工作变得非常艰难,因为他们不觉得 Rust 有价值,甚至希望它消失”,还有人觉得使用 Rust 来改写代码真的太难了。

矛盾激化之下,不少人也将关注重点放到了难以下手的 Rust 身上。然而,没想到的是,另一家对 Rust 感兴趣的 Google,其 Android 工程师于近日直接发了一篇文章展示了 Rust 改写 Android 固件的过程,还直言:太简单了!

来源:https://security.googleblog.com/2024/09/deploying-rust-in-existing-firmware.html

作者 | Ivan Lozano、Dominik Maier

出品 | CSDN(ID:CSDNnews)

在当前的编程语言中,Rust 可谓风头正劲,国内外不少大厂都在深度拥抱。

随着企业技术栈的转变,身处一线的开发人员也要随之学习新的技术才能跟上时代的潮流。然而,对于以学习曲线陡峭著称的 Rust 编程语言来说,“简单 ”并不是一个常听到的词。

因此,用安全的编程语言 Rust 替换原有的 C、C++ 代码,操作难度究竟如何?日前,来自 Google 的 Android 工程师 Ivan Lozano 和 Dominik Maier 发文分享了自己在现有固件代码中部署 Rust 的实践经历。

Lozano 和 Maier 坦言:“通过本文,你会发现,使用 Rust 直接替换来提高安全性是多么容易,我们甚至会演示 Rust 工具链如何处理专门的裸机目标。”

以下是他们的实践全文:

固件的内存安全

固件是硬件与高级软件之间的接口。由于缺乏上层软件标准的软件安全机制,固件代码中的漏洞可能被恶意行为者利用,造成危险。现代手机包含许多负责处理各种操作的协处理器(coprocessor),每个协处理器都运行自己的固件。

通常情况下,固件由用 C 或 C++ 等内存不安全语言编写的大型传统代码库组成。内存不安全是导致 Android、Chrome 浏览器和许多其他代码库出现漏洞的主要原因。

Rust 是 C 和 C++ 的内存安全替代语言,性能和代码大小相当。此外,它还支持与 C 的互操作性,且没有任何开销。

逐步采用 Rust

在替换编程语言过程中,Android 团队采用了渐进式方法,首先侧重于替换新的和风险最高的现有代码(例如,处理外部不受信任输入的代码),可以以最少的投入获得最大的安全效益。只需用 Rust 编写任何新代码,就能减少新漏洞的数量,随着时间的推移,还能减少未解决漏洞的数量。

你可以通过编写一个 Rust shim(轻量级的中间层)来替换现有的 C 代码功能,该 shim 可在现有 Rust API 和代码库所需要的 C API 之间进行转换。shim 会复制并导出 C API,以供现有代码库使用。shim 代码充当 Rust 库 API 的包装器,在现有 C API 和 Rust API 之间架起桥梁。这是用 Rust 替代库重写或替换现有库时常用的方法。

挑战和注意事项

在固件代码库中引入 Rust 之前,你需要考虑几个挑战。下文将介绍 no_std Rust(即裸机 Rust 代码)的一般情况、如何找到合适的现成 crate(Rust 库)、将标准 crate 移植到 no_std、使用 Bindgen 生成 FFI 绑定、如何处理分配器,以及如何设置工具链。

Rust 标准库和裸机环境

Rust 标准库由三个包组成:core、alloc 和 std。core 包始终可用。alloc 包需要一个分配器来实现其功能。std crate 假定有一个完整的操作系统,通常不支持裸机环境。第三方 crate 通过 crate-level #![no_std] 属性表示它不依赖 std。这种 crate 被称为 no_std 兼容 crate。

本文的其余部分将重点讨论这些内容。

选择要替换的组件

在选择要替换的组件时,应将重点放在具有强大测试功能的独立组件上。理想情况下,组件的功能可以由支持裸机环境的开源实现提供。

处理标准和常用数据格式或协议(如 XML 或 DNS)的解析器是很好的初始候选组件。这样可以确保初期工作的重点放在将 Rust 与现有代码库和构建系统集成的挑战上,而不是复杂组件的细节上,并简化测试。这种方法可以简化日后引入更多 Rust 的工作。

选择已有的 Crate(Rust 库)

选择合适的开源 crate(Rust 库)来替换所选组件至关重要。需要考虑的事项有:

  • crate 是否得到了很好的维护,例如,未解决的问题是否得到解决,它是否使用最新的 crate 版本?

  • crate 的使用范围有多广?这可以作为质量信号,但在以后使用可能依赖 crate 时也要考虑。

  • crate 是否有可接受的文档?

  • 它是否有可接受的测试覆盖范围?

此外,crate 最好与 no_std 兼容,这意味着标准库要么未使用,要么可以禁用。虽然有很多兼容 no_std 的 crate,但也有一些尚不支持这种操作模式的情况——在这种情况下,请参阅下一节关于将标准库转换为 no_std 的内容。

按照惯例,可选支持 no_std 的 crate 将会提供一个 std 特性,以指示是否应使用标准库。类似地,alloc 特性通常表示使用分配器是可选的。

注意:即使库在源代码中声明了 #![no_std],也不能保证其依赖库不依赖于 std。我们建议查看依赖关系树,以确保所有依赖关系都支持 no_std,或测试库是否可编译为 no_std 目标。目前唯一的办法是尝试为裸机目标编译 crate。

例如,一种方法是使用 rustup 提供的裸机工具链运行 cargo check:

$ rustup target add aarch64-unknown-none
$ cargo check --target aarch64-unknown-none --no-default-features

将 std 库移植到 no_std

如果某个库不支持 no_std,仍有可能将其移植到裸机环境中,尤其是文件格式解析器和其他与操作系统无关的工作负载。文件处理、线程和异步代码等高级功能可能会带来更大的挑战。在这种情况下,这些功能可以隐藏在功能标志后面,以便在 no_std 构建中仍能提供核心功能。

将 std crate 移植到 no_std(core+alloc):

  • 在 cargo.toml 文件中添加一个 std 功能,然后将此 std 功能添加到默认功能中

  • 在 lib.rs 文件顶部添加以下几行:


#![no_std]

#[cfg(feature = "std")]

extern crate std;

extern crate alloc;

然后,按如下方法反复修正所有出现的编译器错误:

  1. 将所有使用指令从 std 移至 core 或 alloc。

  2. 为所有会被 std prelude 自动导入的类型添加使用指令,例如 alloc::vec::Vec 和 alloc::string::String。

  3. 在 #[cfg(feature = “std”)] guard 后隐藏任何不存在于 core 或 alloc 中、且无法在 no_std 构建中支持的内容(如文件系统访问)。

  4. 任何需要与嵌入式环境交互的内容都可能需要显式处理,例如 I/O 函数。这些函数可能需要置于 #[cfg(not(feature = “std”))] 防护后。

  5. 禁用所有依赖项中的 std(也就是说,如果使用 Cargo,则在 Cargo.toml 中更改它们的定义)。

需要对 crate 依赖关系树中尚未支持 no_std 的所有依赖关系重复此操作。

自定义目标架构

Rust 编译器有许多官方支持的目标,但其中缺少许多裸机目标。值得庆幸的是,Rust 编译器可以降维到 LLVM IR,并使用 LLVM 的内部副本来降维到机器码。因此,通过定义自定义目标,它可以支持 LLVM 支持的任何目标架构。

定义自定义目标需要将通道设置为 dev 或 nightly 的工具链。Rust 的 Embedonomicon 提供了这方面的大量信息。

简要介绍一下,自定义目标 JSON 文件可以通过查找类似的受支持目标并转储 JSON 表示来构建:

$ rustc --print target-list

[...]

armv7a-none-eabi

[...]


$ rustc -Z unstable-options --print target-spec-json --target armv7a-none-eabi

这将打印出类似的目标 JSON 文件:

$ rustc --print target-spec-json -Z unstable-options --target=armv7a-none-eabi

{

"abi": "eabi",

"arch": "arm",

"c-enum-min-bits": 8,

"crt-objects-fallback": "false",

  "data-layout": "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64",

[...]

}

该输出可作为定义目标的起点。特别值得注意的是,data-layout 字段在 LLVM 文档中有定义。

一旦定义了目标,就必须为新定义的目标从源代码构建 libcore 和 liballoc(以及 libstd,如果适用)。如果使用 Cargo,使用 -Z build-std 构建就能实现这一点,表明这些库应与 crate 模块一起从源代码为目标构建:

# set build-std to the list of libraries needed
cargo build -Z build-std=core,alloc --target my_target.json

使用 LLVM 预编译器构建 Rust

如果 Rust 工具链内部捆绑的 LLVM 不支持裸机架构,则可以使用任何支持目标的 LLVM 预编译器生成定制的 Rust 工具链。

在 config.toml 中,llvm-config 必须设置为 LLVM 预编译器的路径。

你可以通过查看发行说明,查找将 LLVM 最低支持版本提升的版本,从而找到特定版本的 LLVM 所支持的最新 Rust 工具链。例如,Rust 1.76 将 LLVM 的最低版本提升至 16,而 1.73 则将 LLVM 的最低版本提升至 15。这意味着使用 LLVM15 预编译器,可以构建的最新 Rust 工具链是 1.75。

创建可直接插入的 Rust Shim

要创建一个可直接替换的 C/C++ 函数或 API,Shim 需要做到两点:它必须提供与被替换库相同的 API,而且必须知道如何在固件的裸机环境中运行。

提供相同的 API

第一点可以通过定义具有相同函数签名的 Rust FFI 接口来实现。

我们将实现放在一个安全函数中、并在其周围公开一个较薄的封装类型,从而尽可能减少不安全 Rust 的数量。

例如,FreeRTOS coreJSON 示例包含一个 JSON_Validate C 函数,其签名如下:

JSONStatus_t JSON_Validate( const char * buf, size_t max );

我们可以在 Rust 中编写一个 shim,在它和内存安全 serde_json crate 之间公开 C 函数签名。我们尽量减少不安全代码,并尽早调用安全函数:

#[no_mangle]
pub unsafe extern "C" fn JSON_Validate(buf: *const c_char, len: usize) -> JSONStatus_t {
if buf.is_null() {
JSONStatus::JSONNullParameter as _
} else if len == 0 {
JSONStatus::JSONBadParameter as _
} else {
json_validate(slice_from_raw_parts(buf as _, len).as_ref().unwrap()) as _
}
}


// No more unsafe code in here.
fn json_validate(buf: &[u8]) -> JSONStatus {
if serde_json::from_slice:: (buf).is_ok() {
JSONStatus::JSONSuccess
} else {
ILLEGAL_DOC
}
}

注:这是一个非常简单的示例。对于资源高度紧张的目标,可以避免使用 alloc,而使用 serde_json_core,它的开销更低,但需要预先定义 JSON 结构,以便在堆栈上分配。

回调 C/C++ 代码

为了让任何 Rust 组件都能在基于 C 的固件中发挥作用,它需要回调 C 代码,以便进行分配或日志记录等操作。值得庆幸的是,有多种工具可以自动生成 Rust FFI 与 C 代码的绑定。

标准的方法是使用 Bindgen 工具。你可以使用 Bindgen 解析所有相关的 C 头文件,这些文件定义了 Rust 需要调用的函数。重要的是,在调用 Bindgen 时要使用与相关代码相同的 CFLAGS,以确保正确生成绑定。

此外,Bindgen 还为生成静态内联函数绑定提供了实验支持。

连接固件的裸机环境

接下来,我们需要将 Rust panic 处理程序、全局分配器和关键部分处理程序连接到现有代码库上。这就需要为每种处理程序定义调用现有固件的 C 语言函数。

必须定义 Rust panic 处理程序,来处理意外状态或失败的情况。可通过 panic_handler 属性定义自定义的 panic 处理程序。这是针对目标的,在大多数情况下,应指向当前任务/进程的 abort 函数,或环境提供的 panic 函数。

如果固件中存在分配器,而 crate 依赖于 alloc crate,则可以通过定义一个实现 GlobalAlloc 的全局分配器来连接 Rust 分配器。

如果有问题的 crate 依赖并发,则需要处理关键部分。Rust 的 core 或 alloc crate 并不直接提供定义此功能的方法,但 critical_section crate 常用于处理许多架构的此功能,并可扩展以支持更多架构。

此外,连接日志记录功能也很有用。固件现有日志功能的简单封装器可以将这些功能暴露给 Rust,并用来代替 print 或 eprint 等功能。一个方便的选择是实现日志特质。

易失效的分配和 alloc

Rusts allocate 通常假定分配是无误的(即内存分配不会失败)。然而,由于内存限制,在大多数裸机环境中,这种假设并不成立。在正常情况下,当分配失败时,Rust 会出现 panic 或者终止;这可能是某些裸机环境可以接受的行为,在这种情况下,使用 alloc 时无需考虑其他因素。

但是,如果有明确的理由或要求使用易失效的分配,则需要额外的努力来确保分配不会失败或失败后能得到处理。

一种方法是使用能提供静态易错分配集合的代码板块(如 heapless 代码板块),或动态易错分配(如 fallible_vec)。另一种方法是专门使用 try_* 方法,如 Vec::try_reserve,它可以检查分配是否可行。

Rust 正在正式完善对易错分配的支持,夜间版本中的实验性分配器允许实现处理失败的分配。此外,alloc 的不稳定 cfg 标志名为 no_global_oom_handling,它可以移除不可靠的方法,确保它们不会被使用。

构建优化

使用 LTO 构建 Rust 库是优化代码大小所必需的。向 rustc 传递 -C lto=true 时,现有的 C/C++ 代码库无需使用 LTO 构建。此外,设置 -C codegen-unit=1 还能进一步优化代码的可重复性。

如果使用 Cargo 构建,建议使用以下 Cargo.toml 设置来减少输出库的大小:

[profile.release]
panic = "abort"
lto = true
codegen-units = 1
strip = "symbols"


# opt-level "z" may produce better results in some circumstances
opt-level = "s"

向 rustc 传递 -Z remap-cwd-prefix=. 标志,或在使用 Cargo 构建时通过 RUSTFLAGS env 变量向 Cargo 传递 -Z remap-cwd-prefix=. 标志,以剥离 cwd 路径字符串。

最相关的例子可能是 Rust binder Linux 内核驱动程序,它发现 “Rust binder 的性能与 C binder 相似”。

在将 LTO 的 Rust staticlib 与 C/C++ 链接在一起时,建议确保最终链接中只有一个 Rust staticlib,否则在链接时可能会出现重复符号错误。这可能意味着要将多个 Rust shims 合并到一个静态库中,方法是从封装模块中重新导出它们。

当今固件的内存安全

使用本博文中概述的流程,你可以立即开始在大型遗留固件代码库中引入 Rust。使用现成的开源内存安全实现替换安全关键组件,并使用内存安全语言开发新功能,这将减少关键漏洞,同时改善开发人员的体验。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
大陆通告全球,不许台湾做一件事,话音刚落,郑丽文派人来京交底

大陆通告全球,不许台湾做一件事,话音刚落,郑丽文派人来京交底

孤痞野猫
2026-05-14 06:22:05
特朗普刚下飞机,美媒就泼冷水:49年都没见过的怪事发生了

特朗普刚下飞机,美媒就泼冷水:49年都没见过的怪事发生了

老头的传奇色彩
2026-05-14 05:05:59
杨幂父母苏州买豪宅!母亲背爱马仕身材超绝,女销售却只围着父亲转

杨幂父母苏州买豪宅!母亲背爱马仕身材超绝,女销售却只围着父亲转

八卦王者
2026-05-13 13:49:11
6000亿杭州联合银行,空降48岁女行长

6000亿杭州联合银行,空降48岁女行长

财经众议院
2026-05-13 17:16:18
《给阿嬷的情书》破亿,史上“最寒酸”赞助商赢麻了

《给阿嬷的情书》破亿,史上“最寒酸”赞助商赢麻了

首席品牌观察
2026-05-12 14:44:48
大碗宽面被人捞出去了

大碗宽面被人捞出去了

毒舌扒姨太
2026-05-12 22:34:02
她曾经玩过14位男星,人前玉女,人后“欲女”,47岁如今还是单身

她曾经玩过14位男星,人前玉女,人后“欲女”,47岁如今还是单身

喜欢历史的阿繁
2026-05-13 17:30:17
一场2-0,让登贝莱金球奖稳了!法甲蝉联 欧冠两连冠在望,创造历史

一场2-0,让登贝莱金球奖稳了!法甲蝉联 欧冠两连冠在望,创造历史

等等talk
2026-05-14 05:57:12
媒体人:山西外援配置顶级,无奈本土球员没有建队基石级球员

媒体人:山西外援配置顶级,无奈本土球员没有建队基石级球员

懂球帝
2026-05-13 23:30:15
美军无视中方警告,40国共谋派兵护航,穆杰塔巴无退路

美军无视中方警告,40国共谋派兵护航,穆杰塔巴无退路

谭麤爱搞笑
2026-05-14 06:39:58
罗马诺:穆帅回归皇马已推进至最后阶段,谈判进展顺利

罗马诺:穆帅回归皇马已推进至最后阶段,谈判进展顺利

懂球帝
2026-05-14 01:06:10
又一个郭晶晶?退役后嫁顶级豪门,7年连生4娃,如今已是顶级阔太

又一个郭晶晶?退役后嫁顶级豪门,7年连生4娃,如今已是顶级阔太

珺瑶婉史
2026-05-03 19:20:12
刘嘉玲默许梁朝伟在岛国养私生子 !?

刘嘉玲默许梁朝伟在岛国养私生子 !?

八卦疯叔
2026-05-12 10:10:13
江苏多条高速临时限速取消

江苏多条高速临时限速取消

鲁中晨报
2026-05-13 09:38:19
45岁阿娇降级去演短剧了!剧照美到窒息,网友:可惜了!

45岁阿娇降级去演短剧了!剧照美到窒息,网友:可惜了!

黎兜兜
2026-05-13 21:19:52
东契奇逼宫,或迎来詹库合体,球迷:再加上字母哥勇士能争冠!

东契奇逼宫,或迎来詹库合体,球迷:再加上字母哥勇士能争冠!

我就是一个说球的
2026-05-13 20:50:27
食堂阿姨偷偷给贫困生多打菜,被同学举报后开除,次日接到局里电话

食堂阿姨偷偷给贫困生多打菜,被同学举报后开除,次日接到局里电话

罪案洞察者
2025-11-03 13:59:03
别只盯特朗普专机,鲁比奥还是来了,释放比访问更重要的信号

别只盯特朗普专机,鲁比奥还是来了,释放比访问更重要的信号

兰妮搞笑分享
2026-05-13 09:06:32
记者曝布莱克·莱弗利采访黑幕:怕被封杀不敢反抗

记者曝布莱克·莱弗利采访黑幕:怕被封杀不敢反抗

娱圈观察员
2026-05-14 07:29:51
广东回基地众人迎接!各将情绪不佳,杜锋拥抱奎因,陆续有人离开

广东回基地众人迎接!各将情绪不佳,杜锋拥抱奎因,陆续有人离开

篮球资讯达人
2026-05-13 23:22:28
2026-05-14 08:55:03
CSDN incentive-icons
CSDN
成就一亿技术人
26537文章数 242284关注度
往期回顾 全部

科技要闻

马斯克:只有我和黄仁勋坐上了"空军一号"

头条要闻

专机落地特朗普又舞起熟悉手势 乘专车前往酒店

头条要闻

专机落地特朗普又舞起熟悉手势 乘专车前往酒店

体育要闻

14年半,74万,何冰娇没选那条更安稳的路

娱乐要闻

白鹿掉20万粉,网友为李晨鸣不平

财经要闻

片仔癀依旧困在“片仔癀”

汽车要闻

C级纯电轿跑 吉利银河"TT"申报图来了

态度原创

教育
艺术
旅游
手机
军事航空

教育要闻

今年中考改革力度大,多省份普高扩招,科目缩减,河南有哪些变化

艺术要闻

这才是真正的“史上最强毕业证”,书法堪比字帖!

旅游要闻

泰国拟缩短93国游客免签停留期限,从60天减少至30天

手机要闻

都是Ultra,下一代不一定更新,小米、vivo、OPPO

军事要闻

美以伊战争期间以总理密访阿联酋

无障碍浏览 进入关怀版