海口手机建站模板,论坛网站怎么做跳转,溧阳建设集团网站,wordpress会员列表文章目录 Reverse for Vecvec! 与 添加元素元素访问元素遍历枚举数组弹出最后一个元素——pop 总结 本文将对Rust中的通用集合类型——动态数组
Vec进行学习#xff0c;对应参考书中的第8章。 Reverse for Vec
Vec是Rust中的动态数据结构#xff0c;与C中的vector功能类似。… 文章目录 Reverse for Vecvec! 与 添加元素元素访问元素遍历枚举数组弹出最后一个元素——pop 总结 本文将对Rust中的通用集合类型——动态数组
Vec进行学习对应参考书中的第8章。 Reverse for Vec
Vec是Rust中的动态数据结构与C中的vector功能类似。实际上Rust中的String就是一个特殊的Vec这可以通过查看Rust的内核代码证实。
vec! 与 添加元素
vec!是一个宏用于快速初始化数组元素。
pub fn main() {let mut x vec![1, 2, 3];x.push(4);println!({}, x.len());
}example::main:sub rsp, 168mov edi, 12mov esi, 4call alloc::alloc::exchange_mallocmov qword ptr [rsp 32], raxand rax, 3cmp rax, 0sete altest al, 1jne .LBB31_1jmp .LBB31_2
.LBB31_1:mov rsi, qword ptr [rsp 32]mov dword ptr [rsi], 1mov dword ptr [rsi 4], 2mov dword ptr [rsi 8], 3mov rax, qword ptr [rip alloc::slice::impl [T]::into_vecGOTPCREL]lea rdi, [rsp 40]mov qword ptr [rsp 24], rdimov edx, 3call raxmov rdi, qword ptr [rsp 24]mov rax, qword ptr [rip alloc::vec::VecT,A::pushGOTPCREL]mov esi, 4call raxjmp .LBB31_5第一段中我们可以发现vec!宏执行时汇编实际上执行的是什么操作。首先调用了一个exchange_malloc函数传入第一个参数为12第二个参数为4根据源码可以判断出第一个参数应该是总的内存分配字节数量第二个参数为每个元素的字节数量。这个函数的返回值是Box[i32]这是Rust中的一个智能指针类型能够在堆分配内存并管理生命周期指针保存在栈中。后面对返回值进行了判断如果内存分配失败则会输出错误信息。Box的特性如下参考资料传送门 在栈上存储指针指向堆上的数据。 在转移所有权时负责释放堆上的内存。 大小固定适用于已知大小的类型。 只能有一个所有者不可共享引用。 随后代码中以rsi作为指针初始化了3个数组元素。初始化完成后调用into_vec将Box转换为Vec类型。可以说上面源码中的vec!宏基本等同于
let mut b: Box[i32] Box::new([1, 2, 3]);
let mut x b.into_vec();经过调试发现调用into_vec后Vec实例中的指针与Box的指针相同但现在Box类型已经不复存在了其所有权已经被转移到Vec中。
随后程序调用了push方法扩充了Vec的空间但原先的地址空间不足以容纳新的元素因此需要将原先的内存空间释放掉再重新分配。考虑到Rust在汇编层调用的是libc所以堆管理那套本质上还是malloc、free那些函数与C/C相同方便进行分析。
在动态数组大小发生改变时如果存在一个已有的对某个元素的引用那么大小改变后该引用可能会指向被释放的空间这是Rust所不能允许的这就要回到所有权规则的定义。考虑存在不可变引用的情况如果此时需要增加数组的长度那么首先在增加前必然需要获取该动态数组的可变引用而所有权规则不允许一个实例同时存在可变引用和不可变引用因此导致编译失败。
元素访问
Rust中有两种方式访问动态数组中的元素第一种是直接通过下标访问
pub fn main() {let mut x vec![1, 2, 3];x.push(4);let y x[2];println!({}, y);
}.LBB33_5:lea rdx, [rip .L__unnamed_6]mov rax, qword ptr [rip alloc::vec::VecT,A as core::ops::index::IndexI::indexGOTPCREL]lea rdi, [rsp 40]mov esi, 2call raxmov qword ptr [rsp 16], raxjmp .LBB33_6这是加的汇编代码第一个参数就是Vec实例地址第二个参数是索引值第三个参数疑似指向工程名的字符串切片推测是在索引越界后输出错误信息用的。这里实际上是调用了index方法进行索引。这个index函数的返回值是一个地址如果加了则直接对指针进行操作如果不加则会直接解引用。
; 不加
.LBB32_6:mov rax, qword ptr [rsp 16]mov eax, dword ptr [rax]mov dword ptr [rsp 68], eaxlea rax, [rsp 68]; 加
.LBB33_6:mov rax, qword ptr [rsp 16]mov qword ptr [rsp 64], raxlea rax, [rsp 64]第二种元素访问的方法是使用get方法
pub fn main() {let mut x vec![1, 2, 3];x.push(4);let y x.get(2).unwrap();println!({}, y);
}.LBB35_5:mov rax, qword ptr [rip alloc::vec::VecT,A as core::ops::deref::Deref::derefGOTPCREL]lea rdi, [rsp 72]call raxmov qword ptr [rsp 40], rdxmov qword ptr [rsp 48], raxjmp .LBB35_6
.LBB35_6:mov rsi, qword ptr [rsp 40]mov rdi, qword ptr [rsp 48]mov rax, qword ptr [rip core::slice::impl [T]::getGOTPCREL]mov edx, 2call raxmov qword ptr [rsp 32], raxjmp .LBB35_7
.LBB35_7:mov rdi, qword ptr [rsp 32]lea rsi, [rip .L__unnamed_7]mov rax, qword ptr [rip core::option::OptionT::unwrapGOTPCREL]call raxmov qword ptr [rsp 24], raxjmp .LBB35_8使用get函数前会首先调用deref方法解引用获取动态数组类型中保存的定长数组实例随后对这个实例使用get方法获取OptionT实例。可见如果使用get方法进行数组的越界访问那么get方法返回后不会立即panic!退出。
元素遍历
对于动态数组要遍历数组中的元素只需要使用for循环即可完成。但Rust源码看着简单实际在汇编层完成的工作可不少。
pub fn main() {let mut x vec![1, 2, 3];x.push(4);for i in x {println!({}, i);}
}.LBB46_5:mov byte ptr [rsp 247], 0mov rax, qword ptr [rsp 56]mov qword ptr [rsp 112], raxmovups xmm0, xmmword ptr [rsp 40]movaps xmmword ptr [rsp 96], xmm0mov rax, qword ptr [rip alloc::vec::VecT,A as core::iter::traits::collect::IntoIterator::into_iterGOTPCREL]lea rdi, [rsp 64]lea rsi, [rsp 96]call raxjmp .LBB46_6
.LBB46_6:mov rax, qword ptr [rsp 64]mov qword ptr [rsp 128], raxmov rax, qword ptr [rsp 72]mov qword ptr [rsp 136], raxmov rax, qword ptr [rsp 80]mov qword ptr [rsp 144], raxmov rax, qword ptr [rsp 88]mov qword ptr [rsp 152], rax
.LBB46_7:mov rax, qword ptr [rip alloc::vec::into_iter::IntoIterT,A as core::iter::traits::iterator::Iterator::nextGOTPCREL]lea rdi, [rsp 128]call raxmov dword ptr [rsp 16], edxmov dword ptr [rsp 20], eaxjmp .LBB46_10上面即为for循环的其中一段其中[rsp40]是Vec实例的地址。首先可以看到程序将Vec实例复制了一份随后调用了into_iter方法获取了一个迭代器实例该方法的第一个参数为需要初始化迭代器的地址第二个参数为复制的Vec的地址。这个方法是可以单独调用的返回一个迭代器fn into_iter(self) - Self::IntoIter。从下面的汇编代码复制到[rsp128]可以得知这个迭代器实例在栈中的大小为0x20。下面是这个迭代器在调试时获取的最初状态
08:0040│ rax rcx 0x7fffffffd840 —▸ 0x5555555b4ba0 ◂— 0x200000001
09:0048│ 0x7fffffffd848 ◂— 0x6
0a:0050│ 0x7fffffffd850 —▸ 0x5555555b4ba0 ◂— 0x200000001
0b:0058│ 0x7fffffffd858 —▸ 0x5555555b4bb0 ◂— 0x0其中第1个和第3个字保存的都是数组的起始地址第4个字保存的是数组的末尾地址第2个字的6保存的是数组的容量注意这里的容量与数组长度不同数组长度为4但容量为6只不过后面2个元素暂时还未被创建。
往下代码调用了next方法获取迭代器中的下一个元素下面是调用后迭代器的状态
10:0080│ rcx rdi 0x7fffffffd880 —▸ 0x5555555b4ba0 ◂— 0x200000001
11:0088│ 0x7fffffffd888 ◂— 0x6
12:0090│ 0x7fffffffd890 —▸ 0x5555555b4ba4 ◂— 0x300000002
13:0098│ 0x7fffffffd898 —▸ 0x5555555b4bb0 ◂— 0x0可以看到第三个字表示的实际上就是当前的指针。next方法返回的是一个OptionT实例索引值和数据分别被保存在rax和rdx中。这一点在下面的汇编代码中得以证实。
.LBB46_10:mov eax, dword ptr [rsp 16]mov ecx, dword ptr [rsp 20]mov dword ptr [rsp 164], ecxmov dword ptr [rsp 168], eaxmov eax, dword ptr [rsp 164]cmp rax, 0jne .LBB46_12mov rax, qword ptr [rip core::ptr::drop_in_placealloc::vec::into_iter::IntoIteri32GOTPCREL]lea rdi, [rsp 128]call raxjmp .LBB46_13下面的代码中进行了一个比较通过数据流分析可以发现这里是将next返回值与0进行比较在OptionT中如果T不是一个枚举类型那么枚举索引值为1表示有效值0则表示无效值。随后就是正常的宏展开与输出输出内容后无条件跳转回next方法调用前继续调用next方法获取下一个值。
当next方法调用失败即已经到达迭代器的终点时通过调试发现返回的rax值为0rdx值为0x5555。后续则是判断失败后跳出循环。
注意上面的代码是for i in x这里的x由于没有使用引用在for循环一开始就丧失了所有权其所有权会被转移到迭代器中当for循环结束后迭代器被销毁后续将不能使用变量x。
如果使用for i in x情况则会有些许的不同不仔细观察还真的容易忽略。
注意看下面是两个into_iter方法在IDA反汇编界面中的函数名
_$LT$$RF$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..iter..traits..collect..IntoIterator$GT$::into_iter::hed888fce85d317be_$LT$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..iter..traits..collect..IntoIterator$GT$::into_iter::he37dcd381eb06c85可能你会纳闷这里为啥会有这么多$符号实际上这是IDA用于表示某些标点符号的转义字符这个转义的规则与Javascript类似。$LT$表示$GT$表示$RF$表示$C$表示,$u??$表示\x??。因此上面的函数名就等同于
alloc::vec::VecT,A as core::iter::traits::collect::IntoIterator::into_iter::hed888fce85d317bealloc::vec::VecT,A as core::iter::traits::collect::IntoIterator::into_iter::he37dcd381eb06c85上面那个是for i in x调用的方法下面是for i in x调用的方法除了后面的哈希值之外函数名真的只有一个的差别。也即上面的方法是针对Vec下面的是针对Vec。二者的参数不同上面那个只有1个参数
.LBB33_5:mov rax, qword ptr [rip alloc::vec::VecT,A as core::iter::traits::collect::IntoIterator::into_iterGOTPCREL]lea rdi, [rsp 64]call raxmov qword ptr [rsp 32], rdxmov qword ptr [rsp 40], raxjmp .LBB33_6即Vec实例的地址。
且二者的返回值也不同对于alloc::vec::VecT,A as core::iter::traits::collect::IntoIterator::into_iter其返回值保存在rax和rdx中其中rax为数组的开始地址rdx为数组的结束地址。实际返回的迭代器的大小也只有16个字节。
for i in x后面的汇编代码段如下
.LBB33_6:mov rax, qword ptr [rsp 32]mov rcx, qword ptr [rsp 40]mov qword ptr [rsp 88], rcxmov qword ptr [rsp 96], rax
.LBB33_7:mov rax, qword ptr [rip core::slice::iter::IterT as core::iter::traits::iterator::Iterator::nextGOTPCREL]lea rdi, [rsp 88]call raxmov qword ptr [rsp 24], raxjmp .LBB33_8
.LBB33_8:mov rax, qword ptr [rsp 24]mov qword ptr [rsp 104], raxmov rdx, qword ptr [rsp 104]mov eax, 1xor ecx, ecxcmp rdx, 0cmove rax, rcxcmp rax, 0jne .LBB33_10可以看到这里调用的next方法也和不加的不一样参数只有1个即数组的开始地址返回值只有1个即下一个元素的地址该函数调用后迭代器中的指针位置向前移动。可见对于引用类型的迭代器结构更为简单只需要一个动态指针和一个结束指针即可什么时候动态指针等于结束指针迭代也就结束。
枚举数组
对于元素类型是枚举类型的数组目前只有一个疑问当枚举类型中不同枚举项所跟的数据类型不同占用内存大小不同时Rust将如何进行处理。
#[derive(Debug)]
enum Shapes {Round(f64),Rectangle(f64, f64),Triangle(f64, f64, f64),
}pub fn main() {let mut x vec![Shapes::Round(3.5),Shapes::Rectangle(7.5, 9.6),Shapes::Triangle(114.514, 19.1981, 1.57)];
}example::main:sub rsp, 136mov edi, 96mov esi, 8call alloc::alloc::exchange_mallocmov qword ptr [rsp 8], raxmovsd xmm0, qword ptr [rip .LCPI10_5]movsd qword ptr [rsp 48], xmm0mov qword ptr [rsp 40], 0movsd xmm0, qword ptr [rip .LCPI10_4]movsd qword ptr [rsp 80], xmm0movsd xmm0, qword ptr [rip .LCPI10_3]movsd qword ptr [rsp 88], xmm0mov qword ptr [rsp 72], 1movsd xmm0, qword ptr [rip .LCPI10_2]movsd qword ptr [rsp 112], xmm0movsd xmm0, qword ptr [rip .LCPI10_1]movsd qword ptr [rsp 120], xmm0movsd xmm0, qword ptr [rip .LCPI10_0]movsd qword ptr [rsp 128], xmm0mov qword ptr [rsp 104], 2and rax, 7cmp rax, 0sete altest al, 1jne .LBB10_1jmp .LBB10_2
.LBB10_1:mov rsi, qword ptr [rsp 8]mov rax, qword ptr [rsp 40]mov qword ptr [rsi], raxmov rax, qword ptr [rsp 48]mov qword ptr [rsi 8], raxmov rax, qword ptr [rsp 56]mov qword ptr [rsi 16], raxmov rax, qword ptr [rsp 64]mov qword ptr [rsi 24], raxmov rax, qword ptr [rsp 72]mov qword ptr [rsi 32], raxmov rax, qword ptr [rsp 80]mov qword ptr [rsi 40], raxmov rax, qword ptr [rsp 88]mov qword ptr [rsi 48], raxmov rax, qword ptr [rsp 96]mov qword ptr [rsi 56], raxmov rax, qword ptr [rsp 104]mov qword ptr [rsi 64], raxmov rax, qword ptr [rsp 112]mov qword ptr [rsi 72], raxmov rax, qword ptr [rsp 120]mov qword ptr [rsi 80], raxmov rax, qword ptr [rsp 128]mov qword ptr [rsi 88], raxlea rdi, [rsp 16]mov edx, 3call qword ptr [rip alloc::slice::impl [T]::into_vecGOTPCREL]可以看到Rust编译器似乎很喜欢通过大量的mov系列指令完成内存复制操作在上面的示例中可以发现Rust是将枚举类型可能占用的最大内存大小作为数组一个元素的大小进行存储在下面的内存拷贝操作中甚至还拷贝了未被初始化的内存区域。我们可以将每一个枚举类型后面跟的值视作一个大的union结构一个枚举类型的不同实例占用的内存大小相同即使其中一个实例只保存了8字节而另一个实例保存了80字节前者也需要80个字节的空间保存数据。这会造成一定的内存浪费但便于数组索引寻址。
弹出最后一个元素——pop
Vec的pop方法能够弹出数组中最后一个元素并在数组中将其删除。
pub fn main() {let mut x vec![1, 2, 3];x.push(4);let y x.pop().unwrap();
}.LBB31_5:mov rax, qword ptr [rip alloc::vec::VecT,A::popGOTPCREL]lea rdi, [rsp 32]call raxmov dword ptr [rsp 8], edxmov dword ptr [rsp 12], eaxjmp .LBB31_6
.LBB31_6:mov esi, dword ptr [rsp 8]mov edi, dword ptr [rsp 12]lea rdx, [rip .L__unnamed_5]mov rax, qword ptr [rip core::option::OptionT::unwrapGOTPCREL]call raxjmp .LBB31_7pop的参数只有一个即Vec实例地址返回值是OptionTrdx为有效值rax为是否有效的索引值1为有效。该方法调用后数组的大小会变化但容量不变真正保存值的静态数组指针中的值也不变而且也不需要改变因为数组大小变小所以后面的值在正常情况下无法访问。
在参考书中只给出了插入元素、获取元素、遍历元素等几个为数不多的Vec操作方法但实际上Vec能完成的功能远不止于此考虑到Vec的方法实在太多这里无法全部完成分析就先到这里了。不过我们已经掌握了Vec的基本结构对于其他方法的分析也就万变不离其宗。
总结
本文我们学习了
Vec动态数组结构在内存中的结构。Vec在最后添加、删除元素、遍历、访问值的相关方法分析。IDA中对一些含有特殊字符的Rust方法的转义方式与Javascript类似。枚举类型构成的数组中每个枚举类型占用的内存大小相同可能导致内存空间浪费。