摘要:本文深入讲解 Rust 不安全编程核心知识,涵盖 unsafe 关键字使用场景、裸指针操作、外部函数接口(FFI)、内联汇编、内存安全保证、unsafe 代码审查等核心内容。每个知识点配有完整代码示例、对比表格、实战场景及常见问题解答,帮助开发者在必要时安全地使用 unsafe 代码。
关键词:Rust、unsafe、不安全编程、裸指针、FFI、内联汇编、内存安全、外部函数接口
适合人群:已掌握 Rust 基础的开发者、需要与 C 代码交互的程序员、想深入理解内存模型的开发者
阅读时间:约 55 分钟
版本信息:Rust 1.70+ | 兼容 Windows/macOS/Linux
📖 目录导航
文章目录
- [📖 目录导航](#📖 目录导航)
- [一、unsafe 关键字](#一、unsafe 关键字)
-
- [1.1 unsafe 使用场景](#1.1 unsafe 使用场景)
- [1.2 unsafe 函数](#1.2 unsafe 函数)
- [1.3 unsafe trait](#1.3 unsafe trait)
- 二、裸指针操作
-
- [2.1 裸指针基础](#2.1 裸指针基础)
- [2.2 指针算术](#2.2 指针算术)
- [2.3 从指针创建引用](#2.3 从指针创建引用)
- [三、外部函数接口 FFI](#三、外部函数接口 FFI)
-
- [3.1 调用 C 代码](#3.1 调用 C 代码)
- [3.2 被 C 调用](#3.2 被 C 调用)
- [3.3 字符串 FFI](#3.3 字符串 FFI)
- 四、内联汇编
-
- [4.1 基本语法](#4.1 基本语法)
- [4.2 输入输出](#4.2 输入输出)
- [4.3 架构特定汇编](#4.3 架构特定汇编)
- 五、内存安全保证
-
- [5.1 内存安全规则](#5.1 内存安全规则)
- [5.2 安全封装](#5.2 安全封装)
- [六、unsafe 代码审查](#六、unsafe 代码审查)
-
- [6.1 审查清单](#6.1 审查清单)
- [6.2 文档化](#6.2 文档化)
- [💡 综合实战案例](#💡 综合实战案例)
-
- [实战:安全封装 C 库](#实战:安全封装 C 库)
- [❓ 常见问题 FAQ](#❓ 常见问题 FAQ)
- [📝 学习资源与建议](#📝 学习资源与建议)
- [📚 参考资料](#📚 参考资料)
一、unsafe 关键字
1.1 unsafe 使用场景
Rust 的 unsafe 关键字允许绕过编译器的安全检查,仅在必要时使用。
unsafe 允许的操作:
| 操作 | 说明 | 示例 |
|---|---|---|
| 解引用裸指针 | *const T、*mut T |
*ptr |
| 调用 unsafe 函数 | 标记为 unsafe 的函数 | unsafe { foo() } |
| 访问或修改可变静态变量 | static mut |
unsafe { STATIC_VAR = 1 } |
| 实现 unsafe trait | 标记为 unsafe 的 trait | unsafe impl Send for Foo |
| 访问 union 字段 | union 类型 | union.field |
1.2 unsafe 函数
rust
unsafe fn dangerous() {
println!("This is an unsafe function");
}
fn main() {
// 必须在 unsafe 块中调用
unsafe {
dangerous();
}
}
1.3 unsafe trait
rust
unsafe trait MyUnsafeTrait {
fn do_something(&self);
}
struct MyStruct;
unsafe impl MyUnsafeTrait for MyStruct {
fn do_something(&self) {
println!("Doing something unsafe");
}
}
unsafe 使用原则:
| 原则 | 说明 | 示例 |
|---|---|---|
| 最小化 unsafe | 仅在必要时使用 | 封装 unsafe 代码 |
| 文档化不变量 | 说明安全前提 | /// # Safety |
| 测试边界条件 | 验证安全性 | 单元测试 |
| 审查代码 | 仔细审查 unsafe 块 | Code Review |
二、裸指针操作
2.1 裸指针基础
裸指针不进行安全检查,需要手动管理。
rust
fn main() {
let mut num = 5;
// 创建裸指针
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 解引用(需要 unsafe)
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
// 修改值
*r2 = 10;
println!("num is now: {}", num);
}
}
裸指针类型对比:
| 类型 | 说明 | 可变性 | 示例 |
|---|---|---|---|
*const T |
不可变裸指针 | 不可变 | let ptr = &x as *const i32 |
*mut T |
可变裸指针 | 可变 | let ptr = &mut x as *mut i32 |
2.2 指针算术
rust
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let ptr = v.as_mut_ptr();
unsafe {
// 指针算术
let second = ptr.add(1);
println!("Second element: {}", *second);
// 修改值
*second = 20;
println!("Vector: {:?}", v);
}
}
指针操作对比:
| 操作 | 方法 | 说明 |
|---|---|---|
| 偏移 | ptr.add(offset) |
向前偏移 |
| 偏移 | ptr.sub(offset) |
向后偏移 |
| 差值 | ptr1.offset_from(ptr2) |
指针差值 |
| 比较 | ptr1 == ptr2 |
地址比较 |
| 对齐 | ptr.align_offset(align) |
对齐偏移 |
2.3 从指针创建引用
rust
fn main() {
let mut x = 42;
let ptr = &mut x as *mut i32;
unsafe {
// 从裸指针创建引用
let reference: &mut i32 = &mut *ptr;
*reference = 100;
}
println!("x = {}", x);
}
安全注意事项:
| 注意事项 | 说明 | 风险 |
|---|---|---|
| 空指针 | 检查是否为 null | 解引用 null 导致 UB |
| 悬垂指针 | 指向已释放内存 | 访问无效内存 |
| 数据竞争 | 多线程访问 | 未定义行为 |
| 对齐 | 指针必须对齐 | 未对齐访问 UB |
三、外部函数接口 FFI
3.1 调用 C 代码
Rust 可以通过 FFI 调用 C 函数。
rust
// 声明外部 C 函数
extern "C" {
fn abs(input: i32) -> i32;
fn printf(format: *const i8, ...) -> i32;
}
fn main() {
unsafe {
println!("Absolute value: {}", abs(-5));
}
}
3.2 被 C 调用
rust
// 导出 C 可调用的函数
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
FFI 类型映射:
| Rust 类型 | C 类型 | 说明 |
|---|---|---|
c_char |
char |
字符 |
c_int |
int |
整数 |
c_float |
float |
浮点数 |
c_double |
double |
双精度 |
c_void |
void |
空指针 |
3.3 字符串 FFI
rust
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn main() {
// Rust -> C 字符串
let c_string = CString::new("Hello from Rust").unwrap();
unsafe {
let len = strlen(c_string.as_ptr());
println!("String length: {}", len);
}
// C -> Rust 字符串
let c_str = CStr::from_bytes_with_nul(b"Hello\0").unwrap();
let rust_str = c_str.to_str().unwrap();
println!("Rust string: {}", rust_str);
}
字符串转换对比:
| 转换 | 方法 | 说明 |
|---|---|---|
| Rust -> C | CString::new() |
创建 C 字符串 |
| C -> Rust | CStr::from_ptr() |
从指针创建 |
| 字节转换 | to_bytes_with_nul() |
包含 null 终止符 |
| 字符串转换 | to_str() |
转为 Rust &str |
四、内联汇编
4.1 基本语法
Rust 1.59+ 支持稳定的内联汇编。
rust
fn main() {
let x: u64;
unsafe {
std::arch::asm!(
"mov {}, 5",
out(reg) x,
);
}
println!("x = {}", x);
}
4.2 输入输出
rust
fn main() {
let a: u64 = 10;
let b: u64 = 20;
let result: u64;
unsafe {
std::arch::asm!(
"add {0}, {1}",
inout(reg) a => result,
in(reg) b,
);
}
println!("{} + {} = {}", a, b, result);
}
汇编操作数对比:
| 操作数 | 说明 | 示例 |
|---|---|---|
in(reg) |
输入 | in(reg) value |
out(reg) |
输出 | out(reg) value |
inout(reg) |
输入输出 | inout(reg) value |
lateout(reg) |
延迟输出 | lateout(reg) value |
const |
常量 | const 5 |
sym |
符号 | sym function_name |
4.3 架构特定汇编
rust
#[cfg(target_arch = "x86_64")]
fn cpuid() -> (u32, u32, u32, u32) {
let eax;
let ebx;
let ecx;
let edx;
unsafe {
std::arch::asm!(
"cpuid",
in("eax") 0,
in("ecx") 0,
out("eax") eax,
out("ebx") ebx,
out("ecx") ecx,
out("edx") edx,
);
}
(eax, ebx, ecx, edx)
}
五、内存安全保证
5.1 内存安全规则
即使使用 unsafe,也必须保证内存安全。
内存安全规则:
| 规则 | 说明 | 违反后果 |
|---|---|---|
| 不悬垂 | 指针必须指向有效内存 | UB |
| 不空指针 | 解引用前检查 null | 崩溃 |
| 不数据竞争 | 同一时间只有一个可变引用 | UB |
| 对齐 | 指针必须正确对齐 | UB |
| 初始化 | 必须初始化后读取 | UB |
5.2 安全封装
rust
struct MyVec {
ptr: *mut i32,
len: usize,
cap: usize,
}
impl MyVec {
fn new() -> Self {
Self {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
}
}
// 安全接口
fn push(&mut self, value: i32) {
if self.len == self.cap {
self.grow();
}
unsafe {
*self.ptr.add(self.len) = value;
}
self.len += 1;
}
fn grow(&mut self) {
// 内部 unsafe 实现
// 对外提供安全接口
}
}
封装原则:
| 原则 | 说明 | 示例 |
|---|---|---|
| 安全接口 | 对外提供安全 API | fn push() |
| 内部 unsafe | 内部使用 unsafe | unsafe { *ptr = value } |
| 不变量检查 | 验证安全前提 | assert!(!ptr.is_null()) |
| 文档化 | 说明安全要求 | /// # Safety |
六、unsafe 代码审查
6.1 审查清单
| 检查项 | 说明 | 示例 |
|---|---|---|
| 指针有效性 | 指针指向有效内存 | !ptr.is_null() |
| 生命周期 | 引用生命周期正确 | 不返回悬垂引用 |
| 别名规则 | 遵守借用规则 | 不创建多个可变引用 |
| 对齐 | 指针正确对齐 | ptr.align_offset() |
| 初始化 | 内存已初始化 | 不读取未初始化值 |
6.2 文档化
rust
/// # Safety
///
/// This function is unsafe because:
/// - `ptr` must be a valid pointer
/// - `ptr` must be properly aligned
/// - `ptr` must point to initialized memory
/// - The memory must not be accessed elsewhere
unsafe fn process_data(ptr: *mut i32, len: usize) {
// 实现
}
💡 综合实战案例
实战:安全封装 C 库
rust
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// 外部 C 函数声明
extern "C" {
fn strdup(s: *const c_char) -> *mut c_char;
fn free(ptr: *mut c_void);
}
use std::os::raw::c_void;
/// 安全封装 C 字符串
pub struct SafeCString {
ptr: *mut c_char,
}
impl SafeCString {
/// 从 Rust 字符串创建
pub fn new(s: &str) -> Result<Self, std::ffi::NulError> {
let c_string = CString::new(s)?;
let ptr = unsafe { strdup(c_string.as_ptr()) };
Ok(Self { ptr })
}
/// 转为 Rust 字符串
pub fn to_str(&self) -> Result<&str, std::str::Utf8Error> {
unsafe {
let c_str = CStr::from_ptr(self.ptr);
c_str.to_str()
}
}
}
impl Drop for SafeCString {
fn drop(&mut self) {
unsafe {
free(self.ptr as *mut c_void);
}
}
}
fn main() {
let s = SafeCString::new("Hello, FFI!").unwrap();
println!("{}", s.to_str().unwrap());
}
项目知识点:
- FFI 调用 C 函数
- CString 转换
- 安全封装 unsafe 代码
- Drop 自动释放内存
❓ 常见问题 FAQ
Q1:什么时候应该使用 unsafe?
A:以下场景可以考虑使用 unsafe:
- 与 C 代码交互(FFI)
- 实现高性能数据结构
- 访问硬件或系统调用
- 实现底层抽象(智能指针、集合)
Q2:unsafe 代码一定不安全吗?
A:不是。unsafe 只是绕过编译器检查,开发者必须手动保证安全。正确编写的 unsafe 代码可以是安全的。
Q3:如何调试 unsafe 代码?
A:调试技巧:
- 使用 Miri 检测 UB:
cargo +nightly miri run - 使用 AddressSanitizer:
RUSTFLAGS="-Z sanitizer=address" - 编写单元测试验证安全性
- 仔细审查不变量
Q4:FFI 字符串转换有什么注意事项?
A:注意事项:
- C 字符串以 null 结尾
- Rust 字符串可能包含 null
- 使用
CString转换 Rust -> C - 使用
CStr转换 C -> Rust - 注意内存释放
Q5:如何保证 unsafe 代码的正确性?
A:保证正确性方法:
- 编写详细的
/// # Safety文档 - 使用 Miri 检测 UB
- 编写全面的测试
- Code Review 审查
- 最小化 unsafe 范围
📝 学习资源与建议
学习建议
1. 尽量避免 unsafe :优先使用安全抽象
2. 理解内存模型 :深入理解指针和内存布局
3. 使用 Miri :检测未定义行为
4. 封装 unsafe :对外提供安全接口
5. 文档化:详细说明安全前提