想象你在开一辆配备了顶级自动驾驶辅助的汽车。Rust 的 safe 模式就是这样一辆车:编译器是副驾上的安全员,一直盯着你——不准超速、不准变道不打灯、每个借用的数据都有明确的车主。你几乎不可能因为驾驶习惯而撞车,因为安全员会在危险发生前就踩住刹车。
但自动驾驶有边界。你想驶入操作系统预留的越野区(调用系统 API)、想让车驶入另一个国家的公路(与 C 库互操作)、或者需要直接查看柏油路下的地基(操控裸指针)——自动驾驶辅助系统说:这段路我管不了。于是你按下“手动模式”按钮,这就是 unsafe。
注意,按下手动模式按钮,并不意味着安全员下车了。你依然要遵守交通法规、依然要系安全带,只是此时“不出事故”的责任,从系统转移到了你——司机——身上。这就是 unsafe 的灵魂契约:编译器把方向盘交给你,但所有的错误,都要你来承担。
学会在 Rust 中“安全地跳出安全区”,是系统级 AI 基础设施工程师的必修课:无论是绑定 C++ 推理引擎、对接 GPU 驱动,还是优化关键路径,你都将在 FFI 的边界上工作。理解了这一步,你就能在大模型推理、高性能向量计算等场景中游刃有余。
可视化演示
展示Unsafe Rust五种不安全操作、Rust与C通过FFI互调的数据流,以及安全封装模式将不安全代码隐藏在安全抽象层下。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Unsafe Rust五种不安全操作 DREF=解引用裸指针 *const, *mut T CALL=调用unsafe函数 STAT=访问可变static变量 unsafe { 5种操作 }
unsafe 初识
按下车内那个“手动模式”按钮之前,你得先弄清它会开启哪些新权限。Rust 的安全员(借用检查器)平日里管着五扇门,平时全是锁死的:第一扇门是解引用裸指针,第二扇是调用标了 unsafe 的函数,第三扇是读写可变静态变量,第四扇是实现 unsafe 的 trait(比如 Send 和 Sync),第五扇是访问联合体(union)的字段。手动模式一次性把这五扇门全打开了。
打开门不等于拆掉房子。许多初学者误以为进了 unsafe 块,借用规则就全部失效,可以随意造悬垂引用。实际上你的副驾安全员仍然坐在那里,普通的借用检查、生命周期检查照常工作。unsafe 只是解锁了五类“系统管不了”的操作,而不是把整辆车的安全系统都关了。
这种设计暗示了一个重要原则:最小化原则。越野路段能多短就多短,把 unsafe 块缩到最小,最好只有一行。每开一段手动模式,还要在代码旁写注释,告诉以后的司机:你为什么在这里跳出了安全区、你依赖了哪些不变量。
越野路段开多了,你会形成一种直觉:手动模式不是用来兜风的,而是用来解决最后几百米问题的。每次按下按钮都问自己一句——这段路我真的不能绕过去吗?
unsafe 的五种能力列成集合:不是“关闭检查”,而是精确地多给五把钥匙。- 解引用裸指针对
*const T或*mut T执行*p操作,编译器无法确认指针背后有没有有效数据。 - 调用 unsafe 函数函数签名前带
unsafe关键字的函数,调用时必须处于unsafe块中。 - 访问可变静态变量
static mut类型的全局变量本身就有数据竞争风险,读写都需手动保证互斥。
裸指针操作
在越野路面上,GPS 信号会断,地图不会提醒你前方是悬崖。裸指针就是这么个玩意儿:*const T 和 *mut T 没有生命周期约束,不保证指向有效内存,甚至可以悬垂或为空。如果说引用是副驾上的安全员盯着你“必须安全到达目的地”,那么裸指针就是把你扔在没有护栏的山路上,让你自己判断路况。
安全员平时会阻止你做的事,在这里全部交给你判断。比如引用 &T 要求非空、对齐、指向有效值;而裸指针可以指向任意内存地址。你从引用转换到裸指针很容易,因为编译器知道引用此刻有效;但从裸指针回到引用,编译器就完全无法看清了——这正是必须进 unsafe 的时刻。你相当于在说:“我亲眼确认过这段路面没塌,让我下车走两步。”
引用到裸指针是免费且安全的:let p = &x as *const i32,不涉及任何风险,因为借用检查器已经证明了 x 活着。反过来,let r = &*p 就需要 unsafe,因为没人知道 p 指向哪儿。
回到驾驶的比喻上:当你拿着裸指针时,你就是那个下车检查路面的人。你可以自由地绕开路面的坑洼(检查空指针)、确认地基是否坚固(验证生命周期),但你必须自己记得——刚才 GPS 显示的距离,现在全靠脚来量了。
- 引用转裸指针
as转型是语言内置的安全转换,引用有效,所以取出的裸指针也指向有效数据。 - 裸指针解引用
*p的读取可能访问非法内存,编译器不再背书,必须由程序员保证其有效性。
C 接口桥接
FFI 全称是“外部函数接口”(Foreign Function Interface),你可以把它理解成一个国境口岸。Rust 的车要开到 C 语言的国家去,两边得先签订协议:你们承认什么样的驾照(调用约定)、什么样的车型(内存布局)。如果不提前约定好,你挂着 R 档往前开,对方还以为你在倒车,必然出事。
extern "C" 块就是这份协议。在块里声明你想调用的 C 函数,例如 extern "C" { fn strlen(s: *const c_char) -> usize; },意思是:“我要在 C 国调用这个叫 strlen 的驾照,它的规则全部按 C 标准来。”调用这种函数本身是 unsafe 的,因为你不知道 C 那头的实现是否靠谱——它可能返回一个悬垂指针,可能永远不会返回。
除了函数签名,数据结构也必须对齐暗号。Rust 结构体的字段顺序可能会被编译器私自重排以便优化,而 C 结构体则一定是声明顺序排列。所以跨边界传送结构体时,要加上 #[repr(C)] 这个魔法标记,强制 Rust 的结构体按照 C 的方式排兵布阵。
过了 FFI 这个口岸之后,你就正式踏上了异国公路:C 侧不会帮你做任何内存安全检查,它只负责按协议办事。你递过去的指针、你承诺的生命周期,都要一言九鼎。
- extern "C" 块告诉编译器,块内函数使用 C 的 ABI(应用二进制接口),调用约定与 C 编译器一致。
- C 字符串指针参数C 函数通常接收
char*,在 Rust 侧对应*const c_char,一个指向以\0结尾字节序列的裸指针。
字符串飞地
C 国的路牌和 Rust 国的路牌长得完全不一样。Rust 的 &str 知道自己有多长——它在肚子里装了一个 GPS,记录着“我还剩几个字节”;而 C 字符串不知道自己有多长,它只能靠一个约定:看到 \0 就算走到头了。就像 C 国的路牌,写到“前方 5 公里”就不写了,你自己数到电线杆为止。
因此,跨过 FFI 边界传字符串,不能直接把 &str 扔过去。你需要 CString:它把 Rust 字符串复制进一块新的内存,并在末尾补上 \0,再把这块内存的所有权握在自己手里。CString::new("hello") 成功后,用 .as_ptr() 取出 *const c_char 交给 C 函数。
反过来,C 返回的 char*,你要用 CStr 来包一层借用:CStr 会从指针出发,沿着内存一直扫到 \0,然后在前面包上一层 &str 让 Rust 安全地读出内容。注意,CStr 只是借用,不拥有内存。谁负责释放那段内存?还是得靠 C 侧自己的规则——可能用 free(),也可能是静态内存,你只管读,不要乱释放。
在字符串这片飞地上,最常见的车祸就是忘记 \0,或释放了 CStr 正在借用的内存。记住两国路牌的不同写法,每次过境前检查自己的后备箱里有没有带够终止符。
CString 负责这套翻译并管理底层内存。- CString 构造
CString::new检查字符串内部是否含\0,若含则报错;成功后追加一个\0终止符号。 - 暴露裸指针
as_ptr()返回指向内部缓冲区的*const c_char,这个指针在CString存活期间有效。
安全封装层
越野驾驶总会让人提心吊胆,但你完全可以把它藏起来:在内部开一小段危险路段,然后把车交给乘客的时候,又切换回自动驾驶模式。这就是安全封装层的精髓——内部用 unsafe,对外暴露安全 API。unsafe 代码不过夜,更不能洒得到处都是。
比如你要封装一个 C 库的 create/use/destroy 三个函数,就可以设计一个 Rust 结构体:构造函数内部调用 create(unsafe),Drop 实现里调用 destroy(unsafe),而中间业务方法全部只用安全的 &self 就能完成。外面的调用者完全感受不到任何 unsafe 的存在,他拿到的是一辆带全景天窗的自动驾驶汽车——只管用,不用懂发动机。
这个模式背后是 RAII(资源获取即初始化):资源在构造函数里获取,在 Drop 里释放,编译器帮你保证释放必然执行。加上对 Option、Result 的映射,把 C 的“返回 -1 表示失败”翻译成 Rust 的 Result<T, Error>,封装层就让危险的异国驾驶变成了本地的舒适通勤。
老司机的经验告诉我们:所有 unsafe 都应该有一个“防弹玻璃”。当你能把危险路段浓缩到一个结构体的内部实现里,剩下的代码就回到了安全员的怀抱,你的项目才能长时间平稳运行而不出大事故。
- 验证前置条件进入 unsafe 前,先检查指针是否为空、下标是否越界,保证后面手动操作的前提成立。
- unsafe 核心操作前置条件满足后,调用外部 C 函数并解引用返回的指针。
- 错误翻译把 C 的返回码(0 成功、-1 失败)映射到 Rust 的
Result类型,调用方可以放心地用?传播错误。
回调消息站
有些时候,C 函数办完事会主动打给你——这叫回调(callback)。你走到 C 国办事大厅,留下一张名片:“如果处理完,请打这个电话通知我。”这个名片就是函数指针。
在 Rust 里,要传给 C 的回调函数必须声明为 extern "C" fn,保证调用约定和 C 一致。同时它不能是闭包,因为闭包可能捕获环境,而 C 不知道什么叫“环境”。那怎么把状态带进回调里呢?业界标准做法是“用户数据指针”:你传给 C 一个 *mut c_void(或 *mut T),C 原封不动地在回调时递回来,你在回调里再转回 &mut T。
这套“递名片 + 暗号验证”的流程也有陷阱:如果回调可能在线程池里被调用,你就要保证传给 C 的数据是 Send 的,且生命周期覆盖整个回调窗口。更危险的是,C 可能持有你的函数指针不放,等你 Rust 侧都 Drop 了之后才拨打电话——那就成了打给死人的电话。
回调像是你和 C 库之间的对讲机。用得好,双方配合默契;用不好,就可能出现 C 侧在深夜拨号给你已经注销的手机号。确保对讲机的持有期限、确保暗号不失效,是你作为司机的责任。
- extern "C" fn 声明确保函数使用 C 的调用约定,C 侧才能正确压栈、弹栈。
- 暗号还原回调内部把
*mut c_void转回&mut T,这个转换必须 unsafe,且要求 C 递回来的确实是当初传出去的指针。
UB 探测器
手动模式下最怕什么?不是明显的报错,而是未定义行为(Undefined Behavior,简称 UB)。UB 像路上的暗冰:程序看起来正常行驶,但整个行为已不在任何规范的保护范围内。悬垂指针解引用、对未对齐地址取值、整数溢出、数据竞争,都是暗冰。它们不会立刻让你翻车,但可能在随机时刻炸毁整个程序。
怎么在真正上路之前发现暗冰?这时候就要请出 Miri 这位“模拟驾驶教练”。Miri 是一个 Rust 解释器,它不真跑编译后的机器码,而是一行行地解释执行,同时监控每一次内存访问:指针是否悬垂、对齐是否满足、引用是否被违反。运行 cargo miri test,它会在你的测试集里模拟所有路径,一旦踩到 UB 就立刻勒令停车。
有了 Miri,你可以把 unsafe 的越野路段先放到模拟驾驶舱里开几百遍。不过它只能检测“能不能被实际跑出来的 UB”,有些 UB(比如数据竞争在特定硬件上才触发)它也只能靠模型去猜。所以 Miri 不是万能的,但它是每个 unsafe 司机最称心的副驾。
Miri 就像驾校里的模拟器,让你在安全环境里把一切危险都经历一遍。在你真正驾驶上生产环境的公路之前,先在模拟器里把所有暗冰都碾一遍吧。
- 悬垂指针指针指向的内存已被释放,解引用等于走进了一栋已经拆迁的楼。
- 数据竞争两个线程同时读写同一块内存且至少一方在写,读写次序未同步。
工程纪律区
最后一个区域不写代码,而是立规矩。一个成熟的 unsafe 司机,出发前会做完整的检查清单。首先是代码审查:每一行 unsafe 都要经过两个人的眼睛,因为一个人的盲区,可能正好覆盖了悬垂指针坠落的位置。
其次是注释不变量。每次写 unsafe,都要回答三个问题:为什么这里是安全的、什么条件保证了它安全、如果条件被破坏会怎样。这些注释是给未来司机的路标,让他们维护时不至于重新踩进同一个坑。Rust 社区甚至建议把不变量写进文档测试,让 cargo test 自动验证一部分。
最后是模糊测试(Fuzzing)。给 FFI 边界喂随机输入,让 C 函数面对各种奇怪的字符串、奇葩的长度、边界值。模拟驾驶只测你想到的场景,而模糊测试会测你没想到的场景——这相当于夜间无人路段的高速急刹测试,逼出那些只在极限边缘才露头的 UB。
工程纪律的本质是敬畏:手动模式不是更自由,而是更自律。所有成熟的 Rust 项目,表面看是一套安全的 API,内里都有一套严苛的纪律在守护着少数几行 unsafe——就像最有经验的越野司机,恰恰是那个出发前检查轮胎最仔细的人。
- 最小化 unsafe把 unsafe 块收缩到只剩必要操作,周围全用安全代码包裹。
- 不变量注释用
// SAFETY:开头写清楚安全前提,让审阅者能够核对。 - 模糊测试使用
cargo-fuzz对 FFI 函数做随机输入轰炸,让隐藏 UB 现形。
AI 应用场景
绑定 C++ 推理引擎(llama.cpp)
阶段:模型部署与推理加速
解决的问题:大语言模型的推理引擎(如 llama.cpp)是 C/C++ 写的,直接用原生代码调用容易内存泄漏或崩溃;Rust 的 AI 服务端需要用安全的接口接住这些 C 函数。
具体案例:某 AI 公司要在 Rust 编写的推理网关中集成一个 GGUF 格式的量化模型库。Rust 工程师用 extern "C" 声明 C API,写了一个 LlamaSession 结构体:构造函数创建上下文,Drop 中释放内存,generate(prompt) 方法走安全封装层,内部才触碰裸指针。结果上层 AI 业务代码完全无感,所有 C 侧资源都由 Rust 结构体 RAII 管理。
为 GPU 计算库做安全封装
阶段:高性能数值计算
解决的问题:CUDA 驱动和 cuBLAS 都是 C API,直接调用需要管理大量裸指针和设备内存;普通 AI 研发人员不可能记住每个指针的生命周期。
具体案例:向量检索项目要调用 cuBLAS 做矩阵乘法,Rust 团队通过 unsafe 封装了 Tensor 类型:内部持有设备指针,外部提供 matmul(&self, rhs: &Tensor) -> Tensor 安全方法。每个 Tensor 的 Drop 自动调用 cudaFree,半精度计算时的错误码也翻译成 Result。AI 算法工程师因此能用安全 Rust 写训练循环,完全避开 C 层陷阱。
用 Miri 守护 AI 安全关键路径
阶段:测试与质量保障
解决的问题:AI 服务中长期运行的任务一旦踩到 UB,可能在模型峰值请求时崩溃,且极难复现。
具体案例:一家做实时语音识别的公司,发现服务偶发段错误(Segfault)。团队为所有涉及 FFI 的模块补充 #[cfg(miri)] 测试,在 CI 流水线里运行 cargo miri test,成功捕获一处回调函数里悬垂指针的解引用——那正是高峰期请求并发导致 C 侧提前释放缓冲区引发的。修复后,崩溃率降为零。
Python 演示
import ctypesimport platform class SafeWrapper: """模仿 Rust 的安全封装:内部藏住 C 指针,外部只给安全方法。""" def __init__(self, text): # 模拟 CString:结尾补 \0,持有底层的 C 缓冲区 self._buf = text.encode("utf-8") + b"\x00" self._c_buffer = ctypes.create_string_buffer(self._buf) def as_c_ptr(self): # 模拟 as_ptr():把内部缓冲区暴露给 C 侧 return ctypes.cast(self._c_buffer, ctypes.c_char_p) def demo_ffi(): # 1. 加载系统 libc —— 相当于 extern "C" { fn strlen(...) } lib = ( ctypes.CDLL(None) if platform.system() != "Windows" else ctypes.CDLL("msvcrt") ) c_strlen = lib.strlen c_strlen.argtypes = [ctypes.c_char_p] c_strlen.restype = ctypes.c_size_t # 2. 安全封装下使用裸 C 函数 msg = SafeWrapper("hello from ffi land") length = c_strlen(msg.as_c_ptr()) print("C strlen =", length) print("Python len =", len("hello from ffi land")) if __name__ == "__main__": demo_ffi()
ctypes 还原了 Rust 中 CString 封装 + extern "C" 调用的完整链路,跑通一次从安全代码进入 C 函数再返回的“跨境之旅”。class SafeWrapper对应 Rust 的安全封装层,内部持有 C 缓冲区的所有权,外部只暴露安全方法。self._buf = text.encode("utf-8") + b"\x00"模拟CString::new的补\0行为,让 C 侧能正确判断字符串终点。self._c_buffer = ctypes.create_string_buffer(self._buf)在内存中创建一块可供 C 代码读取的连续缓冲区。as_c_ptr对应 Rust 的.as_ptr(),把内部地址以c_char_p形式传给外部 C 函数。lib = ctypes.CDLL(None)加载系统标准 C 库,类似 Rust 里extern "C"声明并链接外部库。c_strlen.argtypes = [ctypes.c_char_p]声明 C 函数的参数类型,相当于 FFI 签名里的*const c_char。length = c_strlen(msg.as_c_ptr())在安全 API 内部完成一次不安全的 C 调用,调用方并不接触裸指针。
常见误区
误区我以为 `unsafe` 块里可以随便造悬垂引用、改任意内存,编译器全都不管了,就像打开了游戏里的无敌模式。
正解unsafe 并不停用借用检查器,它只是额外开放五类操作——解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait、读写联合体。其它所有安全规则照常生效。如果在一个 unsafe 块里写出两个互相冲突的可变借用,编译器依然会毫不留情地报错。它更像是给你五把备用钥匙,而不是拆掉整栋楼的安保系统。
为何常见:“unsafe(不安全)”这个名字太容易让人误解为“关闭安全机制”,初学者看到关键字就以为整段代码都脱离了编译器的掌控。
误区我整理依赖时,只要发现某个 crate 里出现 `unsafe`,就立刻判定它不靠谱,想换掉。
正解标准库和无数高质量 crate 内部都大量使用 unsafe,只是它们把不安全的实现藏在安全 API 后面。真正危险的不是“用了 unsafe”,而是“用了 unsafe 却没有安全封装和不变量注释”。判断一个库是否安全,要看它是否做到最小化 unsafe、是否有清晰的 SAFETY 注释、是否有测试覆盖。这就好比你看一家餐厅是否卫生,不能只看后厨有没有用刀,要看菜刀有没有戴上刀套。
为何常见:不了解 unsafe 的封装模式时,很容易把“出现关键字”等同于“存在风险”,忽略了工程纪律才是安全的核心。
误区我把 `CString::new(...).as_ptr()` 得到的裸指针存到一个全局变量里,过了一段时间再去用,结果程序崩溃了。
正解as_ptr() 返回的指针只在 CString 存活期间有效。一旦 CString 被 Drop,它管理的缓冲区就被释放,保存下来的指针变成悬垂指针。正确做法是:让 CString 的生命周期覆盖 C 函数调用的全过程,或者把 C 需要的数据复制到 C 自己管理的内存中。指针像一张门禁卡,卡的主人(CString)都离职了,你还能指望门禁还认这张卡吗?
为何常见:很多初学者把 FFI 里的裸指针当成普通整数看待,忽略了它背后还有一套所有权和生命周期的逻辑。
误区C 函数返回了一个 `*mut Foo`,我心想“既然都返回了,那肯定是有效的”,直接在 safe 代码里解引用。
正解裸指针不像引用那样自带“有效性证明”。C 函数可能返回空指针、指向栈上临时变量的指针、或需要额外调用释放函数的堆指针。正确处理方式是:通过 std::ptr::NonNull 去除空值可能性、确认生命周期、必要时用 Box::from_raw 接管所有权,并且所有这些检查/转换都必须在 unsafe 块中完成。这就像收到陌生人的名片,你不能直接按上面的地址开车冲过去,先得核实对方身份。
为何常见:C 函数返回的指针在大多数情况下确实是有效的,这种侥幸心理掩盖了少数失败路径,直到某一天在极端输入下爆出崩溃才追悔莫及。
面试题
Q1. 在 Rust 中,unsafe 块为开发者额外开放的能力不包括哪一项?
- A.解引用裸指针
- B.调用 unsafe 函数
- C.绕过借用检查器← 正确答案
- D.访问可变静态变量
答案:C。unsafe 开放的是五类特定能力,但借用检查器仍然全程在工作。它不会让你绕过借用规则——你在 unsafe 块里写两个冲突的可变借用依然编译失败。选项 C 是常见的误解,“关闭安全员”并不在列表里。
Q2. 裸指针 *const T 与引用 &T 的关键区别是什么?
- A.裸指针不能为空
- B.裸指针没有生命周期← 正确答案
- C.裸指针指向只读内存
- D.裸指针自动管理内存
答案:B。&T 由借用检查器保证生命周期内有效,必须非空且对齐;*const T 则没有任何这类保证,它可能为空、悬垂或指向任意地址,编译器不会替你把关。所以裸指针“下车自己看路”,引用则是“安全员全程护送”。
Q3. 在 Rust 中声明外部 C 库函数时,通常使用哪个语法块?
- A.extern "C"← 正确答案
- B.pub extern
- C.static extern
- D.unsafe extern
答案:A。正确答案是 extern "C" 块,它声明了使用 C 调用约定的外部函数。extern 是关键字,"C" 指定 ABI 为 C 约定。其它几个都是干扰项,Rust 中没有 pub extern 或 unsafe extern 这种写法。
Q4. Rust 与 C 互操作时,为什么结构体要加 #[repr(C)]?
- A.加快编译优化速度
- B.使用 C 语言的布局规则← 正确答案
- C.开启自动垃圾回收
- D.允许结构体参与继承
答案:B。Rust 默认允许编译器重排结构体字段以优化内存布局,而 C 结构体严格按声明顺序排列。#[repr(C)] 强制 Rust 一侧采用与 C 一致的布局规则,两边才能按同一张图纸对接内存。这个选择与优化速度、垃圾回收、继承无关。
Q5. CString::new("abc") 生成的 CString 与普通 &str 最大的区别是什么?
- A.CString 末尾追加了终止符← 正确答案
- B.CString 使用 UTF-16 编码
- C.CString 的长度一定更短
- D.CString 支持随机访问修改
答案:A。CString 在内部缓冲区的末尾追加了一个 \0 字节,因为 C 字符串靠它来判定终点;&str 则自己记录长度。CString 仍然是 UTF-8 编码,长度不一定更短,也不提供随机访问修改能力。
Q6. 安全封装模式的核心思路是哪一项?
- A.在 pub fn 里直接写 unsafe
- B.把裸指针返回给所有调用方
- C.内部 unsafe 外部安全 API← 正确答案
- D.在整个 crate 加 forbid(unsafe)
答案:C。安全封装模式把 unsafe 集中在类型内部实现(构造、析构、核心操作),对外暴露的 API 完全不出现 unsafe 关键字。调用方拿到的是一层“防弹玻璃”,既用上了底层 C 库能力,又不用亲自开手动模式。
Q7. FFI 回调函数在 Rust 侧应声明为哪种类型?
- A.extern "C" fn← 正确答案
- B.async fn
- C.const fn
- D.inline fn
答案:A。回调跨越 Rust 与 C 的边界,必须使用 extern "C" fn 保证调用约定一致。async fn 是异步协程,const fn 是编译期计算,inline fn 不是合法的 Rust 项声明,三者都无法作为 C 可调用的函数指针。
Q8. 下列哪些情况属于 Rust 中的未定义行为(UB)?
- A.解引用悬垂裸指针← 正确答案
- B.构造未对齐的引用← 正确答案
- C.使用 as 做类型宽窄转换
- D.在 unsafe 块中调用函数
答案:AB。解引用悬垂指针(A)和构造未对齐引用(B)都是典型 UB,Miri 能直接捕获。as 类型转换(C)本身是明确定义的语法操作,不属于 UB;在 unsafe 块中调用 unsafe 函数(D)正是其正常用途。UB 意味着“任何行为都可能发生”,包括崩溃和数据损坏。
Q9. 使用 Miri 可以检测出哪些问题?
- A.悬垂指针的解引用← 正确答案
- B.结构体字段的对齐错误← 正确答案
- C.多线程的数据竞争← 正确答案
- D.编译过程的速度瓶颈
答案:ABC。Miri 是 Rust 的解释器,在逐行执行时监控内存访问,能发现悬垂指针(A)、对齐错误(B)和数据竞争(C)。它做的是语义层面的动态检测,不关注编译性能(D)。想发现编译速度瓶颈应该用 cargo build --timings 或性能分析工具。
Q10. 在 unsafe 代码中,内存安全最终由谁负责保证?
- A.借用检查器的自动分析
- B.编译器的运行时插桩
- C.程序员手动证明不变量← 正确答案
- D.操作系统的内存保护
答案:C。unsafe 把内存安全的举证责任从编译器移交给了程序员。借用检查器对裸指针的合法性不做判断,编译器也不插桩;操作系统只提供基础页面保护,不能识别逻辑层面的悬垂引用。只有程序员在代码中验证前置条件、保证生命周期、注释不变量,才能守住这条边界。这正是“手动模式”契约的核心意义。