使用lldb探索trait object的结构

trait object

rust中支持了静态派发(static dispatch)和动态派发(dynamic dispatch)两种方式

静态派发指的是具体调的函数,在编译阶段就能确定下来,静态派发通过泛型来完成。对于不同的泛型类型参数,编译器通过单态化(monomorphization)生成不同的函数实现,在编译阶段就确定了应该调用哪个函数。动态派发指的是具体调用哪个函数,在执行阶段才能确定。它通过 trait object 来完成。trait object类似golang中的interface,本质上是个胖指针,它可以指向不同的类型,从而动态地选择调用的方法

下面这张图展示了trait object在rust内部是如何表示的,图片来自这里

trait object

trait object的底层其实就是一个由两个word组成的fat pointer。其中,一个指针指向数据本身,另一个则指向虚函数表(vtable)。vtable 是一张静态表,rust 在编译时会给使用了 trait object 的类型的 trait 实现生成一张表,放在可执行文件中(一般位于 RODATA 段)

vtable中所包含的信息有:

  • destructor:当trait object被释放,用来释放其使用的所有资源
  • size:类型的大小
  • align:类型对齐方式
  • methods:trait中方法在具体类型中的实现

lldb调试

待调试的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait Trait {
fn foo(&self);
}

impl Trait for i32 {
fn foo(&self) {
println!("i32")
}
}

fn main() {
let t: &dyn Trait = &100;
t.foo()
}

执行lldb ./target/debug/lldb 进入调试

1
2
3
4
5
6
7
8
9
(lldb) target create "./target/debug/deps/lldb-2bf0ba3510ca81ff"
Current executable set to './target/debug/deps/lldb-2bf0ba3510ca81ff' (x86_64).
(lldb) l
11 fn main() {
12 let t: &dyn Trait = &100;
13 t.foo()
14 }
(lldb) b 12
Breakpoint 1: where = lldb-2bf0ba3510ca81ff`lldb::main::h83faa1216f9a9c91 + 8 at main.rs:12:25, address = 0x00000001000014e8

在第12行设置了断点之后可以用run命令启动程序,启动后它将在断点处停止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(lldb) run
Process 53222 launched: '/Users/caelansar/Rust/rs-playground/target/debug/deps/lldb-2bf0ba3510ca81ff' (x86_64)
Process 53222 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001000014e8 lldb-2bf0ba3510ca81ff`lldb::main::h83faa1216f9a9c91 at main.rs:12:25
9 }
10
11 fn main() {
-> 12 let t: &dyn Trait = &100;
13 t.foo()
14 }
Target 0: (lldb-2bf0ba3510ca81ff) stopped.
(lldb) n
Process 53222 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x00000001000014fe lldb-2bf0ba3510ca81ff`lldb::main::h83faa1216f9a9c91 at main.rs:13:5
10
11 fn main() {
12 let t: &dyn Trait = &100;
-> 13 t.foo()
14 }
Target 0: (lldb-2bf0ba3510ca81ff) stopped.
1
2
3
4
5
(lldb) p t
(&dyn lldb::Trait) $0 = {
pointer = 0x000000010003b284
vtable = 0x0000000100040238
}

如之前所说,trait object由一个指向实际数据的point和vtable组成。继续验证pointer的内容:

1
2
3
4
(lldb) x/g t.pointer
0x10003b284: 0x0000000000000064
(lldb) x/ug t.pointer
0x10003b284: 100

可以看到这里打印的100正是在代码let t: &dyn Trait = &100中创建trait object时对应的值

接着对vtable进行验证:

1
2
3
4
5
(lldb) x/4ag t.vtable
0x100040238: 0x0000000100001640 lldb-2bf0ba3510ca81ff`core::ptr::drop_in_place$LT$i32$GT$::h79b95ab35a5e7b63 at mod.rs:490
0x100040240: 0x0000000000000004
0x100040248: 0x0000000000000004
0x100040250: 0x00000001000014a0 lldb-2bf0ba3510ca81ff`_$LT$i32$u20$as$u20$lldb..Trait$GT$::foo::head3fb833b4fb39d at main.rs:6

这四个地址中的内容分别表示:

内容 表示
0x0000000100001640 i32 drop方法地址
0x0000000000000004 i32大小,为4
0x0000000000000004 i32的对齐,为4
0x00000001000014a0 i32实现的foo方法地址