RFE-003: 工程师(粗)Rust入门之栈爆破
栈和值
在目前的范围内,Rust简直是可爱。有小龙同学的手速,配合Rust对变量名称的重复使用,把所有的值都放在栈内存中,简直是爽歪歪(小龙爽不爽?我就当他爽……)。什么内存,我们只计算值,值从本质上是没有变化的,当然一个值可以被用于构造另外一个值。每一个值都是独一无二的!骄傲不!
我们目前掌握的Rust变量使用方式,就是单纯值编程范式(Pure Value Programming Paradigm)。在这种范式下,变量名只是值的标签,变量名可以重复使用,变量名所指向的值是不可变的。每次使用let语句定义一个变量名时,实际上是创建了一个新的值,并将该值与变量名关联起来。之前的变量名所关联的值并没有被修改或者删除,而是依然存在于内存中,直到不再被引用时才会被回收。
当然,这个跟Rust放在栈区域的变量是一一对应的。Rust能够放在栈内存中的变量,有一个Copy trait约束,而Copy trait约束又是Clone trait约束的子集,Clone trait约束又是Sized trait约束的子集。所以呢,栈区域的主要特征就是线性内存空间、放置Copy类型的变量,而Copy类型的变量,必然实现知道尺寸。
栈区域这么好用,分配速度快,访问有序……那我们都用栈区域吼不吼啊,那当然吼啊~~~
那栈区域内存的边界在哪里呢?栈区域内存管理方法的问题在哪里呢?其实就是栈的大小限制,程序或者线程的栈区域必须实现分配好,毕竟能够全自主管理的内存需要赋予所有权(!第一次出现这个术语!)。每个线程都有自己的栈空间,栈的大小在创建线程时就确定了,通常是几百KB到几MB不等。如果栈空间不够用,就会发生栈溢出(stack overflow),导致程序崩溃。这个问题,工程师应该会很容易提出一个解决方案!给程序的栈空间增加大小不就行了嘛!
这个解决方案的问题,连小龙都知道,不能一开始把所有的扫描阀都给他……别人也要用嘛!其实类似于计算器就很好办,只有一个线程嘛,控制所有的内存……不过我们的电脑现在还是相当复杂的,操作系统、各种后台服务、各种前台应用程序,哪能把所有的栈空间都给一个程序用啊!
这也是单纯值编程范式的一个局限性,值编程范式要求所有的变量都放在栈内存中,栈内存空间有限,当我们需要处理的问题规模变大时,栈内存空间就会不够用,导致栈溢出错误。最大的问题还是,如果一个程序处理的数据量事先不清楚,就很难权衡如何分配栈内存空间。
栈爆破
对于我们目前掌握的命令式程序设计范式(如果有这个范式)来说,要搞栈爆破还得我这个师傅出马,因为小龙他还不会Array,也不会Loop,也不会函数。
哈哈哈哈!
在了解最小知识的情况下,如何把命令范式的程序搞出栈爆破呢?有两个技术路线:
- 申请一个非常大的数组,虽然不知道数组是什么
- 循环申请
i128变量,虽然不知道循环是什么 - 递归函数调用,虽然不知道函数是什么
看来,如论如何,小龙除了搞点当作值用的变量做计算产生新的值之外,必须得学会一些新东西了。
数组
Rust中数组是一种固定大小、同类型元素的集合。数组在栈内存中分配,适合存储少量数据。数组的大小在编译时确定,不能动态改变,因此,很符合我们工程师的视角,这是一个值!比如我们工程师要考虑一个位置的三维坐标,可以用一个包含三个f64类型元素的数组来表示:
1let position: [f64; 3] = [1.0, 2.0, 3.0];
因为大小和类型在编译时就确定了,Rust可以在栈内存中高效地分配和访问数组元素。对于小规模的数据处理,数组是一个非常合适的选择。
数组的标注方式是[T; N],其中T是元素类型,N是数组大小(元素个数)。数组的索引从0开始,可以通过索引访问数组元素。
数组初始化的方式有两种:1)显式指定每个元素的值:let arr = [1, 2, 3, 4, 5];;2)使用重复元素初始化数组:let arr = [0; 10];表示创建一个包含10个元素,值都为0的数组。
循环与条件判断
这两个玩意儿,一下子把我们命令式程序设计范式给干掉了,我们正式要进入结构化程序设计范式的领域了。
循环允许我们重复执行一段代码,直到满足某个条件为止。Rust中有三种主要的循环结构:loop、while和for。
loop和while本质上是一样的,都是重复执行代码块,区别在于:前者无限循环,直到通过break语句退出;后者当条件为真时重复执行代码块。 突然发现一个问题,在不依赖可变的量的情况下,怎么搞循环呢?
那算了,小龙,这个对你来说太难了,我们先搞for循环吧。
1fn main() {
2 let nums = [1, 2, 3, 4, 5];
3 for n in nums.iter() {
4 println!("Number: {}", n);
5 }
6}
这是我们循环数组。我们还可以按照一定的范围循环:
1fn main() {
2 for i in 1..=5 {
3 println!("Index: {}", i);
4 }
5}
如果去掉等号=,就是1到4。
函数
函数,在过程式程序设计范式中是一个非常重要的概念。函数允许我们将一段代码封装起来,赋予一个名称,以便在需要时重复使用。函数可以接受参数,并且可以返回值。
按照我们这里值编程的理念,函数的参数和返回值都是值,而不是引用或者指针。函数调用时,参数的值会被复制到函数内部,函数执行完毕后,返回值会被复制回调用者。这也是为什么函数调用不会改变外部变量的值。并且,Rust中栈内存的数据有什么Copy trait约束,函数参数和返回值也必须满足这个约束。
1fn add(a: i32, b: i32) -> i32 {
2 let result = a + b;
3 result
4}
5fn main() {
6 let a = 5;
7 let b = 10;
8 let result = add(a, b);
9 println!("Result: {}", result);
10}
我们如果按照前面的办法主程序中的变量(值)地址打印出来,把函数调用内部的变量(值)地址打印出来,会发现函数调用时,参数值被复制到函数内部,函数执行完毕后,返回值被复制回调用者。这个认知很关键,纯函数调用不会改变外部变量的值。
爆栈大法
有了上面的基础,我们就可以搞栈爆破了!
大数组
最简单的办法,就是申请一个非常大的数组:
1 let a = [0_i128; 62_765];
2 println!("Array address: {:p}", &a);
3 println!("Array size: {}", std::mem::size_of_val(&a));
4 println!("Array alignment: {}", std::mem::align_of_val(&a));
5 println!("Array type: {}", std::any::type_name_of_val(&a));
6 println!("Array length: {}", a.len());
7 // println!("Array elements: {:?}", a); // too large to print all
8
9 // pause to see output
10 println!("Press Enter to continue...");
11 std::io::stdin().read_line(&mut String::new()).unwrap();
大概这么点就能爆栈了,不同的计算机环境,栈大小不一样,能爆的大小也不一样。
因为Rust的编译器相当聪明,对变量的使用区间非常清楚,循环加码的办法没办法搞爆栈。只好去搞函数递归调用了。
递归函数
循环调用函数,是搞爆栈的终极大法!
1 fn deep(n: usize) {
2 println!("{:p} n={}", &n, n);
3 if n > 0 {
4 deep(n - 1)
5 }
6 }
7
8 deep(100_000);
实际上,我的电脑算到25207就爆栈了,大概深度就是74793差不多的数字。不同的计算机环境,栈大小不一样,能爆的大小也不一样。
1# ....
20x7ffd34f2d378 n=25207
3
4thread 'main' has overflowed its stack
5fatal runtime error: stack overflow, aborting
6Aborted (core dumped)
总结
好了,值编程在Rust中还是挺好玩的。只要我们理解了变量名只是值的标签,变量名可以重复使用,变量名所指向的值是不可变的,我们就可以在栈内存中愉快地进行计算。
文章标签
|-->rust |-->engineering |-->tutorial |-->stack |-->variables |-->memory |-->stack overflow |-->value
- 本站总访问量:loading次
- 本站总访客数:loading人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository