引用的内存结构 Memory layout
Rust 中的引用, 跟 C++ 中的引用一样, 在底层都是指针.
先看一段代码 C++ 代码片段:
#include <cassert>
int addr1() {
int x = 42;
int& x_ptr = x;
x_ptr += 1;
return x;
}
int main(void) {
assert(addr1() == 43);
return 0;
}
这段 C++ 代码对应的汇编代码如下:
.file "reference-address.cpp"
.text
.globl _Z5addr1v
.type _Z5addr1v, @function
_Z5addr1v:
.LFB3:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $42, -12(%rbp)
leaq -12(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
leal 1(%rax), %edx
movq -8(%rbp), %rax
movl %edx, (%rax)
movl -12(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.
然后我们用 C 语言重新写一遍相同的功能, 如下:
#include <stdlib.h>
#include <assert.h>
int addr1() {
int x = 42;
int* x_ptr = &x;
*x_ptr += 1;
return x;
}
int main(void) {
assert(addr1() == 43);
return 0;
}
这段 C 代码对应的汇编代码如下:
.file "reference-address.c"
.text
.globl addr1
.type addr1, @function
addr1:
.LFB6:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $42, -12(%rbp)
leaq -12(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
leal 1(%rax), %edx
movq -8(%rbp), %rax
movl %edx, (%rax)
movl -12(%rbp), %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
可以发现, 两段汇编代码是完全一样的. 简单地说, C++中的引用, 在底层就是通过指针实现的.
最后, 我们看一下对应的 Rust 代码如何写:
fn add1() -> i32 {
let mut x: i32 = 42;
let x_ref: &mut i32 = &mut x;
*x_ref += 1;
x
}
fn main() {
assert_eq!(add1(), 43);
}
当然, 也要把它生成汇编代码:
.section .text._ZN17reference_address4add117h385aa07c715333f2E,"ax",@progbits
.p2align 4, 0x90
.type _ZN17reference_address4add117h385aa07c715333f2E,@function
_ZN17reference_address4add117h385aa07c715333f2E:
.cfi_startproc
pushq %rax
.cfi_def_cfa_offset 16
movl $42, 4(%rsp)
movl 4(%rsp), %eax
incl %eax
movl %eax, (%rsp)
seto %al
jo .LBB12_2
movl (%rsp), %eax
movl %eax, 4(%rsp)
movl 4(%rsp), %eax
popq %rcx
.cfi_def_cfa_offset 8
retq
.LBB12_2:
.cfi_def_cfa_offset 16
leaq .L__unnamed_3(%rip), %rdi
movq _ZN4core9panicking11panic_const24panic_const_add_overflow17h343c6c3f46bad3f5E@GOTPCREL(%rip), %rax
callq *%rax
.Lfunc_end12:
.size _ZN17reference_address4add117h385aa07c715333f2E, .Lfunc_end12-_ZN17reference_address4add117h385aa07c715333f2E
.cfi_endproc
可以看到, 生成的汇编代码里压根就没有用到 x_ref 这个引用. 这是因为表达式太简单, 被优化掉了,
我们使用 rustc --emit asm -g reference-address.rs 重新生成汇编, 得到以下内容:
.section .text._ZN17reference_address4add117h385aa07c715333f2E,"ax",@progbits
.p2align 4, 0x90
.type _ZN17reference_address4add117h385aa07c715333f2E,@function
_ZN17reference_address4add117h385aa07c715333f2E:
.Lfunc_begin12:
.file 11 "/home/shaohua/dev/rust/intro-to-rust/src/references/assets" "reference-address.rs"
.loc 11 5 0
.cfi_startproc
subq $24, %rsp
.cfi_def_cfa_offset 32
; let x = 42;
movl $42, 12(%rsp)
; let x_ref = &mut x;
leaq 12(%rsp), %rax
movq %rax, 16(%rsp)
movl 12(%rsp), %eax
incl %eax
movl %eax, 8(%rsp)
seto %al
jo .LBB12_2
movl 8(%rsp), %eax
movl %eax, 12(%rsp)
movl 12(%rsp), %eax
addq $24, %rsp
.cfi_def_cfa_offset 8
retq
.LBB12_2:
.cfi_def_cfa_offset 32
.Ltmp40:
.loc 11 8 5
leaq .L__unnamed_3(%rip), %rdi
movq _ZN4core9panicking11panic_const24panic_const_add_overflow17h343c6c3f46bad3f5E@GOTPCREL(%rip), %rax
callq *%rax
.Ltmp41:
.Lfunc_end12:
.size _ZN17reference_address4add117h385aa07c715333f2E, .Lfunc_end12-_ZN17reference_address4add117h385aa07c715333f2E
.cfi_endproc
上面几行重要的汇编代码都加了注释, 可以看到:
x_ref存储的也是x的内存地址
这个跟上面 C/C++ 代码里的行为是一致的.
简单地说, Rust 中的引用, 在底层也是通过指针实现的.
但是, 除了我们用过的这些引用类型之外, Rust 还有一些更复杂的引用类型, 它们的内存结构也更复杂.
切片 slice 的引用
上段介绍过了引用本身占用的内存大小只是一个指针大小, 即 usize. 这个类似于 C/C++ 中的
指针.
但切片以及 trait object 的引用, 这两类都是胖指针(fat pointer), 即除了指向内存地址之外, 还有别的属性.
指向 slice 的指针包含两个成员, 第一个是指向 slice 某个元素的内存地址; 第二个 是定义了该引用可访问的元素个数.
指向 trait object 的引用包含了两个 field, 第一个是指向该值的内存地址, 第二个指向 该值对该 trait 的实现的地址, 以方便调用该 trait 定义了的方法.
但还有两个特殊类型的引用, 它们都占两个指针大小, 即 usize * 2:
切片 slice 的引用是一个胖指针, 该指针包含两个字段:
- slice 起始地址
- slice 长度
先看一段代码示例:
use std::mem::size_of_val;
fn main() {
// 整数数组
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
assert_eq!(size_of_val(&numbers), 20);
// 数组的引用
let numbers_ref: &[i32; 5] = &numbers;
assert_eq!(size_of_val(numbers_ref), 20);
// 切片引用, 该切片指向数组中的第 3 个元素, 切片长度为 3
let slice_ref: &[i32] = &numbers[2..];
assert_eq!(size_of_val(slice_ref), 12);
assert_eq!(slice_ref.len(), 3);
}
这里面创建了两个引用:
numbers_ref只是一般的引用, 它指向数组numbers第 1 个元素的地址slice_ref是一个切片引用, 它里面的指针指向numbers的第 3 个元素的地址; 而且切片的长度为 3
其内存结构如下图如示:
如果还不相信的话, 我们可以用调试器直接查看它们的内存, 首先是 numbers 数组, 注意它的第 3 个元素的内存地址:

可以看到, 这个整数数组的内存都是连续存放的, 每个元素占用 4 个字节.
然后是 slice_ref 的内存结构:

可以看到, slice_ref 确实包含两个成员:
- 指针, 指向数组
numbers的第 3 个元素 - 切片长度, 是 3
trait object 的引用
trait object 的引用也是一个胖指针, 包含两部分:
- object 数据地址
- object virtual table 地址, 即该值对该 trait 的具体实现