开发环境:
- OS:Ubuntu 20.04.3 LTS
- 系统架构:x86_64
- Rust 版本:1.67.1
- GDB:9.2
Trait objects 简介
Trait objects 是 Rust 中一个重要的概念,它用于表示一个实现了一组 trait 的类型。除了在定义 Trait objects 时手动指定的 trait(也称为 base trait)之外,还包含了 auto traits[^1],以及 base trait 对应的 super traits[^2]。虽然 Trait objects 自身存在一些开销,但在一些场景下可以替代泛型,简化代码设计。
有稍微了解下 Trait objects 的 Rustacean 都知道,Trait objects 是一个 DST[^3],因此常以 &
或者 Box
的形式出现,而 Trait objects 自身由两个指针组成:
- 一个指针指向自身实例的数据部分;
- 一个指向虚方法表(vtable),该方法表存放所有实现了的 traits 的函数指针。除此之外,还会存放实际类型的大小以及对其信息。
其结构可以用下图表示:
然而,这些都只是概念范畴,在实际编码过程中看不见摸不着,容易让人感觉很”虚“。因此本文的目的是通过 GDB,带领大家从汇编这一层次,在内存中来查看 Trait objects 的指针结构。
[!note] 虽然本文提到了 GDB 以及汇编,但实际上并不会涉及到太多的汇编指令,甚至如何使用 GDB 的占比还更高一点,但也仅用到了几个简单的命令,可以放心食用。
GDB 简介
GDB[^4] 是 GNU 项目下的一个调试器,它能够在程序里的任意位置设置断点,打印变量值,查看内存里的值等,并且也支持多线程调试,远程调试等高级功能。
这里只会简单介绍几个用到的命令:
- list <num1>,<num2>,显示 <num1> 到 <num2> 行之间的代码;
- break <num>,在第 <num> 行一个打断点;
- next,执行一行代码;
- print <what>,打印 <what> 的内容,可以是一个类似于 C 的表达式(如变量,常量),也可以是一个内存地址,寄存器等。
- x/<n><format><unit> <address>,打印指定的内存地址里的内容。相比 print,更加专注于打印内存里的值。
- <n>,指定打印多少个单元;
- <format>,打印的格式,有 x(十六进制)、d(十进制)、a(指针)等;
- <unit>,指定打印的单元,有 b(Byte,1 byte)、h(Half-Word,2 bytes)、w(Word,4 bytes)、g(Giant Word,8 bytes);
- start,运行程序。
以上就是需要用到的 GDB 命令,其他常见的基本操作可以参考陈皓的《用GDB调试程序》[^5]。
创建 Rust 项目
接下来使用 cargo 创建一个 Rust 项目:cargo new trait-obj-test && cd trait-obj-test
,接着编写 src/main.rs
,代码如下:
#![allow(unused_variables)]
struct Foo(i64);
trait Bar {
fn bar(&self);
}
impl Bar for Foo {
fn bar(&self) {
println!("{}", self.0);
}
}
fn main() {
let foo: &dyn Bar = &Foo(123);
println!("end.");
}
代码中定义了一个大小为 8 bytes,对齐 为 8 bytes 的类型 Foo(i64)
,并实现了 Bar
trait。接着执行 cargo build
得到一个可执行文件:target/debug/trait-obj-test
。
打印 Trait objects 结构
接下来正式进入调试环节,执行 gdb target/debug/trait-obj-test
,进入如下界面:
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from target/debug/trait-obj-test...
warning: Missing auto-load script at offset 0 in section .debug_gdb_scripts
of file /home/zxk/rust/trait-obj-test/target/debug/trait-obj-test.
Use `info auto-load python-scripts [REGEXP]' to list them.
>>>
输入 list 1,18
查看代码:
>>> list 1,18
1 #![allow(unused_variables)]
2
3 struct Foo(i64);
4
5 trait Bar {
6 fn bar(&self);
7 }
8
9 impl Bar for Foo {
10 fn bar(&self) {
11 println!("{}", self.0);
12 }
13 }
14
15 fn main() {
16 let foo: &dyn Bar = &Foo(123);
17 println!("end.");
18 }
可以看到,我们在第 16 行代码创建了一个名为 foo
的 Trait objects,并且拥有一个类型为 i64
的字段,值为 123
。我们使用 break 16
在第 16 行打一个断点:
>>> break 16
Breakpoint 1 at 0x7c74: file src/main.rs, line 16.
万事俱备,接着我们直接输入 start
命令启动程序,程序会在第 16 行停下来:
>>> start
Temporary breakpoint 2 at 0x7c74: file src/main.rs, line 16.
Starting program: /home/zxk/rust/trait-obj-test/target/debug/trait-obj-test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, trait_obj_test::main () at src/main.rs:16
16 let foo: &dyn Bar = &Foo(123);
输入 next
,执行第 16 行的代码,此时会创建一个名为 foo
的变量,而程序则会停留在第 17 行:
>>> next
17 println!("end.");
接着使用 print foo
打印 foo
变量:
>>> print foo
$1 = &dyn trait_obj_test::Bar {
pointer: 0x555555591068,
vtable: 0x5555555a01a8
}
光从名字也可以猜测到,pointer
指向了 foo
的值,而 vtable
则指向了方法表。这里先对 pointer
进行验证,输入 x/g foo.pointer
查看内存地址 0x555555591068
里的内容:
# x/g 表示从给定的内存地址开始,打印一个 8 字节单元里的内存值
>>> x/g foo.pointer
0x555555591068: 123
可以看到,正是在代码里创建 foo
时指定的 123
。接着对 vtable
进行验证:
>>> x/g foo.vtable
0x5555555a01a8: 93824992262896
这时候发现,只能看到一连串意义不明的数字。实际上,这是因为 vtable 里存放的是一个个函数指针,因此我们需要指定打印的格式为指针类型:
# x/ag 其中的 a 表示以指针的形式打印内存值
>>> x/ag foo.vtable
0x5555555a01a8: 0x55555555baf0 <core::ptr::drop_in_place<trait_obj_test::Foo>>
这次打印的内容,与我们前面的图是符合的,第一个方法就是析构器,在 Rust 里即 drop_in_place
。我们可以多打印几条看看:
>>> x/4ag foo.vtable
0x5555555a01a8: 0x55555555baf0 <core::ptr::drop_in_place<trait_obj_test::Foo>> 0x8
0x5555555a01b8: 0x8 0x55555555bc10 <<trait_obj_test::Foo as trait_obj_test::Bar>::bar>
在析构器后面,分别是 0x8
、0x8
,正好符合类型 Foo
的大小和对齐,而 0x55555555bc10 <<trait_obj_test::Foo as trait_obj_test::Bar>::bar>
则对应了 Bar
trait 里的方法 bar
。
小结
通过 GDB 这一调试利器,我们可以清晰的观察到 Trait objects 的内部结构,而不再仅仅停留在概念的范畴里。这也是本人在 Rust 的路上通过实操学习 Rust 的一个小笔记,如果文章中有任何谬误,请各位大佬务必指出,期待与君共勉!
[^1]: The Rust Reference - Special types and traits: https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits
[^2]: The Rust Reference - Traits: https://doc.rust-lang.org/reference/items/traits.html#supertraits
[^3]: The Rust Reference - Dynamically Sized Types: https://doc.rust-lang.org/reference/dynamically-sized-types.html?highlight=dst#dynamically-sized-types
[^4]: GDB: The GNU Project Debugger: https://www.sourceware.org/gdb/
[^5]: 用GDB调试程序(一): https://blog.csdn.net/haoel/article/details/2879
评论区
写评论还没有评论