Rust 所有权:内存管理新流派

在 Rust 中每个值都只能被一个所有者拥有,当这个值被赋给其他所有者,原所有者无法再使用。正是这种机制保证了 Rust 语言的内存安全,从而无需自动垃圾回收,也无需手动释放。所有权是 Rust 最重要的特性之一。下面来看一个简单的例子。

fn main() {
    let s = String::from("hello");
    let a = s;          // 字符串对象“hello” 的所有权被转移
    println!("{}", s); // error! 无法再使用 s
} 

上面展示了所有权转移的基本案例, 由于a获得了字符串的所有权,s无法再使用。这可能和现存的编程语言有很大的区别,事实上在 Rust 之前,我所了解的所有语言中这样用是完全正确的,但是现在我明白了这么做的奇妙之处。

为什么要转移所有权?

Rust 定位系统编程语言,要求内存安全以及内存管理无运行时开销。

何为内存管理的运行时开销,这里要拿 Java 做个例子,Java 自称是内存安全的语言,因为 Java 中程序员无需手动管理内存(程序员自己管理是内存不安全的源头), Java 采用垃圾自动回收,所有的 Java 程序都运行在 Jvm 中,Jvm 在 Java 程序运行期间,必须时刻监控、遍历 Java 对象树,以鉴别出堆上哪些变量不再被引用,在一定的时间周期到达时自动释放那些不被引用变量的内存。

也就是说,Jvm 在运行着一个和实际程序完全无关的垃圾回收进程,这被称为运行时开销。当然,由于 Java 的定位,这些开销是可以完全不用考虑的。

C/C++ 作为系统编程语言,由程序员手动管理内存,在mallocfree必须被使用,否则会发生内存泄漏,最终占满进程的所有内存空间,在 C++ 中是newdelete这一对好兄弟。目测这很好处理,只要记得同时使用就好了,但是当问题变得复杂,这将变得困难而又容易出错。来看一段 C 代码:

#include <stdio.h>
#include <malloc.h>
 
int* func()
{
    int *p = malloc(sizeof(int));
    /*
       do something
    */
    return p;
} 

谁能保证在这个函数的外部,有人记得这个指针是 指向堆而不是栈上的,并且记得调用free()?这很难说, 尤其是情况变得更加复杂的时候… 因此,C/C++ 高手很难炼成,连 Goole 都因它犯难,试图用Go取代部分 C/C++ 的应用场景,Go的特性这里不多提,它无疑是一个优秀的编程语言,出身名门,虽然自我定位是系统编程语言,但是目前主要被用于网络编程,它也采用了垃圾自动回收机制,因此,运行时开销是无法避免的。

上面说到的 Java 和 C/C++两个例子,代表了当前内存管理的两个流派,两种方式都存在一定的痛点,这就是为什么 Rust 决定采用一种完全不同的管理方式,通过转移所有权,Rust 做到了安全的内存管理。那么现在回到主题,来看 Rust 是如何管理内存的。

Rust 的内存管理

Rust 中没有自动垃圾回收(Auto GC), 也不需手动管理,这一工作在编译阶段,由编译器来负责。编译成功后,变量内存何时回收已经被确定,硬编码到二进制程序中了,程序自己运行到该回收的时候就自动回收了。编译器如何做到如此智能?Rust 中的所有权系统功绩首屈一指。下面来分别介绍所有权系统的各种特性。

作用域

每一个变量被限定在一个作用域内有效,和大多数编程语言一样,{}被看作一个作用域的标志,但不同的是,在运行到}时,不仅回收栈上的变量,也回收堆上的内存。

fn func() {
    let n = 2;  // 在栈上分配
    let s = String::new(); // 在堆上分配
} 

如上,在堆上分配的空间也被回收了,这看似很正常,但是如果 s被作为返回值,它的作用域改变了,它仍然能够最终在某个}(所处作用域结束时)处被释放,作用域保证了变量一定会被回收,也就避免了像上面 C 语言忘记调用free()的情况了。

到底是如何回收的?在 Rust 中,类使用struct定义,或者你可以不叫它“类”,而是别的名字。每个对象都实现了一个trait, 即Drop(如果你熟悉 Java 可以把trait理解为“接口”),Drop 包含一个方法drop(), 在任何对象离开作用域的时候,它的drop()会被自动调用,从而释放自身内存。

转移

正如本文开始提到,一个值只能有一个拥有者,因此当赋值给其他的变量时,所有权被转移,原所有者不能继续使用。在 Rust 中,所有权转移称为 move. 本文开头的赋值是一个所有权转移的基本例子,下面我们再来看一个稍微复杂的。

fn main() {
    let s = String::from("hello");
    func(s);    // 字符串的所有权转移到了func()的内部
    let a = s;  // error  s 已经无法被使用
}
 
fn func(s: String) {
    println!("{}", s);  // s 将在离开作用域时被释放
} 

但是有时在作为函数参数使用后,仍要使用怎么办,在函数结尾将其 return是一个解决办法,但不是好办法,后面马上会讲到的借用,会很好的解决这个问题。

值得注意的是,move的例子中我都使用的是 String::new()或者String::from()来创建一个字符串对象, 为什么不直接用字符串类型例如let s = "hello"或者其他类型如i32做演示,因为 move 规则对它们并不适用!

fn main() {
    let n = 2;   // i32 类型
    let a = n;
    println!("{}", n);  // success! 并没有问题
} 

看起来这和之前的理论矛盾了,但实际上所有权规则对所有类型都是适用的,只不过 Rust 为了减少编程的复杂度,在基本类型赋值的时候, 拷贝了一份原内存作为新变量,而不是转移所有权。也就是说本例中 a 是一个独立的变量,拥有独立的内存,而不是从n处获得。n也得以保留了对应值的内存,因而可以继续使用。

以上说的“基本类型”到底是哪些类型,常用的 i32, bool等都是。具体来说是实现了Copy这个trait的类型,基本类型 Rust 已经内置实现了,也就是说,我们完全可以自己为String类型实现Copy ,从而在字符串对象赋值的时候,拷贝而不转移。

注意! let s = "hello"中的 s 并不是基本类型变量,虽然赋值也不会转移所有权,那是因为 s的类型是&str, 是借用在起作用而不是拷贝!

上面的 赋值传参move的隐式调用,有些情况下,必须通过关键字move显式指定,否则无法编译通过,比如闭包就是一个常见的情况。文章篇幅考虑,这里先不介绍闭包,让我们快速进入前面多次提到的借用,这也是本节最后一部分。

借用

使用转移所有权有的时候还是太麻烦了,正如现实中一样,我可以把自己的东西借给被人用,但仍然具有所有权,Rust 中支持借用(borrow),用&表示,有些文章中也称为“引用”,但是我觉得这样并不好,因为这里的&与在 C/C++ 中&有很大的区别, 而且编译器都叫它 “borrow” 而非 “refer” !

来看一下如何用借用解决所有权问题。

fn main() {
    let s = String::from("hello");
    let a = &s;
    println!("{}", s);  // success
    println!("{}", a);  // success, print "hello"
    func(&s);  // success
}
 
fn func(s: &String) {
    println!("{}", s);
} 

这段代码是借用的基本用法,a通过&借用了s的内存,并没有转移,但现在a能访问s的空间了,Rust 允许有多个借用者,传入到函数func()的也是s的一个借用,但是它在func()结束时被释放了。

但是,在被借用期间,拥有者不允许修改变量,或者转移所有权!这看似是一个礼节问题,但实则是为内存安全考虑,修改值将导致这些借用的值与本身不一致,引发逻辑错误,转移所有权必将导致借用失效,因此,这不被允许!让我们尝试在func(&s)之后转移所有权。

fn main() {
    let s = String::from("hello");
    ...
    func(&s);
    let b = s;  // error  s 已被借用,无法转移
} 

到这里为止,提到的借用都是指不可变借用,也可以说是“只读借用”,借用者允许有多个,借用者不允许修改值,这不难理解,如果有一个借用者修改了值,必将造成数据的不一致。

但有时我们还真的需要修改值!比如我们熟悉的swap(), 这时 Rust 提供了可变借用(&mut),可变借用者能够对数据进行修改,当然前提是这个值本身是可变的(mut)。简单考虑,这里用字符串连接作为例子。

fn main() {
    let mut s = String::from("hello");
    func(&mut s);
}
 
fn func(s: &mut String) {
    s.push_str(" world");  // s = "hello world"
} 

通过可变借用,func()函数得以修改了s的值,但是可变借用有一个非常严格的限制,那就是只能有一个可变借用。可变借用期间,不允许有其他的任何借用,包括不可变借用; 可变借用期间,拥有者本身也不能进行任何操作,不能转移,不能修改值。

从某种角度来看,可变借用和转移没什么区别,它相当于一个临时的所有权转移,当接收转移的那个变量离开作用域,所有权自动物归原主 。

到此为止,Rust 的所有权系统基本介绍完毕,正是这些规则撑起了 Rust 内存安全的大旗,完备而相互论证,借用理论来源于生活,符合情理,不得不说 Rust 内存管理设计的非常精妙!

Tips

提炼出本文得出的几个有用的 Tip:

  • 基本变量(实现了Copy)的变量赋值时不转移所有权,而是拷贝
  • 被借用期间,不允许修改值
  • 可变借用只允许一个,借用期间,拥有者不允许任何操作
  • 可变借用相当于临时的所有权转移,借用释放后,物归原主

完。

× 如转载请注明出处,谢谢 ×

接下来我将给各位同学划分一张学习计划表!

学习计划

那么问题又来了,作为萌新小白,我应该先学什么,再学什么?
既然你都问的这么直白了,我就告诉你,零基础应该从什么开始学起:

阶段一:初级网络安全工程师

接下来我将给大家安排一个为期1个月的网络安全初级计划,当你学完后,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web渗透、安全服务、安全分析等岗位;其中,如果你等保模块学的好,还可以从事等保工程师。

综合薪资区间6k~15k

1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)

2、渗透测试基础(1周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等

3、操作系统基础(1周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)

4、计算机网络基础(1周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现

5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固

6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)

那么,到此为止,已经耗时1个月左右。你已经成功成为了一名“脚本小子”。那么你还想接着往下探索吗?

阶段二:中级or高级网络安全工程师(看自己能力)

综合薪资区间15k~30k

7、脚本编程学习(4周)
在网络安全领域。是否具备编程能力是“脚本小子”和真正网络安全工程师的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力。

零基础入门的同学,我建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习
搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP,IDE强烈推荐Sublime;

Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,没必要看完

用Python编写漏洞的exp,然后写一个简单的网络爬虫

PHP基本语法学习并书写一个简单的博客系统

熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选)

了解Bootstrap的布局或者CSS。

阶段三:顶级网络安全工程师

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

学习资料分享

当然,只给予计划不给予学习资料的行为无异于耍流氓,这里给大家整理了一份【282G】的网络安全工程师从入门到精通的学习资料包,可点击下方二维码链接领取哦。

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先需要了解得物网站的数据结构和爬取方式,得物网站比较复杂,需要使用Selenium+BeautifulSoup进行爬取。 以下是一个简单的得物爬虫Python代码实现(注:仅供学习参考,请勿用于商业用途): ```python import time from selenium import webdriver from selenium.webdriver.chrome.options import Options from bs4 import BeautifulSoup options = Options() options.add_argument('--no-sandbox') # 解决DevToolsActivePort文件不存在报错的问题 options.add_argument('window-size=1920x3000') # 指定浏览器分辨率 options.add_argument('--disable-gpu') # 谷歌文档提到需要加上这个属性来规避bug options.add_argument('--hide-scrollbars') # 隐藏滚动条, 应对一些特殊页面 options.add_argument('blink-settings=imagesEnabled=false') # 不加载图片, 提升速度 options.add_argument('--headless') # 无界面 driver = webdriver.Chrome(options=options) url = 'https://www.dewu.com/' driver.get(url) # 等待页面加载完成 time.sleep(3) # 模拟鼠标点击,展开商品列表 driver.find_element_by_xpath('//div[text()="全部商品"]').click() # 等待页面加载完成 time.sleep(3) # 获取页面源代码 html = driver.page_source # 解析页面 soup = BeautifulSoup(html, 'html.parser') # 获取商品列表 items = soup.find_all('div', {'class': 'item-card'}) for item in items: # 获取商品标题 title = item.find('div', {'class': 'title'}).text.strip() # 获取商品价格 price = item.find('div', {'class': 'price'}).text.strip() # 获取商品链接 link = item.find('a', {'class': 'item-link'})['href'] print(title, price, link) # 关闭浏览器 driver.quit() ``` 这里的代码仅仅是一个简单的爬虫示例,如果想要更加深入地了解得物网站的数据结构和爬取方式,需要结合具体的需求进行更加详细的分析和实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值