程序员

从内存布局上看,Rust的胖指针到底是胖在栈上还是堆上了?

作者:admin 2021-07-05 我要评论

最近阿里云最新的云原生技术Serverless发展也是如火如荼 我在前辈巨师的带领下 也进入到学习Rust的大军中 与其它语言一样 Rust最初的爬坡难点也在于字符串方面的...

在说正事之前,我要推荐一个福利:你还在原价购买阿里云、腾讯云、华为云服务器吗?那太亏啦!来这里,新购、升级、续费都打折,能够为您省60%的钱呢!2核4G企业级云服务器低至69元/年,点击进去看看吧>>>)

最近阿里云最新的云原生技术Serverless发展也是如火如荼 我在前辈巨师的带领下 也进入到学习Rust的大军中 与其它语言一样 Rust最初的爬坡难点也在于字符串方面的处理。虽然说Rust与C一样也有指针概念 但是在字符串方面引用了胖指针 关于胖指针的内存布局 被引用最为广泛的一幅说明图如下

image.png

咱们先来说明一下这个胖指针的大致概念 字符串s1有三个元素分别是ptr、len、capacity 其中ptr是指向堆上实际字符串value的指针 len代表字符串的长度 capacity代表字符串的容量。这些值全部都存在栈上 而实际字符串的值则存在堆上。为了让便于说明 我转化了一下上面的图 大家可以看一下这幅图。

image.png

对于这幅图的理解真可谓是一波三折 我一开始以为这图画的不对 后来发现应该是对的 最后深入研究还是发现了一个小问题 最终正确的示意图如下

image.png

本文就和大家分享一下具体分析的过程

?

胖指针理解错误的起因

我们知道Rust在编译是可以通过-g参数保留符号信息 再通过objdump命令就可以将代码对应的汇编语言导出 具体指令如下

rustc -g 文件名.rs

objdump -S 文件名

先来看以下代码

fn main() {

????????let mut ?s1 String::from( hello

????????let len calculate_length( s1);

????????println!( The Length is {}. ,len);

?????}

fn calculate_length(s: String)- usize{

s.len()

}

将上述代码中字符串值进行微调之后的代码

fn main() {

????????let mut ?s1 String::from( hell00

????????let len calculate_length( s1);

????????println!( The Length is {}. ,len);

?????}

fn calculate_length(s: String)- usize{

s.len()

}

在得到相应的汇编代码以后 diff一下结果如下

2991c2991

????????let mut ?s1 String::from( hello

---

????????let mut ?s1 String::from( hell00

2994c2994

????a9f3: ????ba 05 00 00 00 ?????????mov ???$0x5,%edx

---

????a9f3: ????ba 06 00 00 00 ?????????mov ???$0x6,%edx

也就是说从执行码也就是汇编的角度上看 只有执行mov ???$0x6,%edx时 传递的参数一个是5一个是6 栈上的操作似乎只涉及长度len 这让我初步对于capacity这个值的存放位置产生了一定怀疑。

接下来我又用gdb调用了一下上面这个程序 其中print s1的结果如下

(gdb) print s1

$2 {

??vec {

????buf {

??????ptr {

????????pointer 0x5555557a0110 hello\177 ,

????????_marker { No data fields }

??????},

??????cap 5,

??????alloc { No data fields }

????},

????len 5

??}

}

在看到这个信息的时候 我想当然的以为cap是buf的一个item 而buf一般放在堆上 因此cap应该放在堆上 当时理解的图如下

image.png?

当然现在看这个结论的得出犯了想当然的经验主义错误 没有进行深入实证。

堆和栈到底是干嘛的

为了更好的向大家展示对于胖指针内存而已的验证方案 这里先简要介绍一下基本的汇编及gdb调试知识。

1.堆和栈 这里先来说一下运行时和编译时的概念 运行和编译其实是程序的两种时态 一些信息是程序运行之前就可以确定了 这种场景就对应编译时 另一类信息是程序真正运行起来才能确定的 这也就对应运行时。

一般来说栈用来对于分配编译时就可以确定的内存需求 比如某些运算任务我申请一些变量进行关联计算 这种场景下对于内存的需求在程序运行前就确定了 这种内存分配通过栈来解决就可以了 而堆则用来解决那些运行时才能确定的内存需求 其中最典型的就是字符串 由于字符串往往是由网络或者磁盘读出的 因此编译时无法确定其具体需求 这种情况下一般要通过堆分配内存。

栈的大小是提前确定的 比如我们在看汇编语言指令时函数的入口都是sub ???$0x**,%rsp也就是进行栈的构建动作 示例如下

000000000000aa00 _ZN6hello14main17h5a48792de9598b5bE :

aa00: ??????48 81 ec 98 00 00 00 ???sub ???$0x98,%rsp

let mut ?s1 String::from( hello

而堆上的内存分配是操作系统malloc的产物 都是动态分配的 示例如下

220a3: ??????ff 25 af 8c 22 00 ??????jmpq ??*0x228caf(%rip) ???????# 24ad58 malloc GLIBC_2.2.5

因此栈的特点就是满足那些可以提前确定的编译时内存需求 并且程序员可以不去关心栈上内存的分配与释放 这些都是由编译器完成的工作。

而堆的特点则是满足运行时的内存需求 灵活性强 但是分配与释放都需要程序员人为管理。

2.Gdb调试方法简要说明 用gdb调试rust程序也很简单 只需要在编译时加上-g参数 然后用gdb启动调试就可以了 具体的指令如下

rustc -g 文件名.rs

gdb 文件名

进入到gdb模式后

1.?用list指令查看代码

(gdb) list

1 ??????fn main() {

2 ??????????????let mut ?s1 String::from( hello

3 ??????????????let len calculate_length( s1);

4 ??????????????println!( The Length is {}. ,len);

5 ???????????}

6 ??????fn calculate_length(s: String)- usize{

7 ??????s.len()

8 ??????}

9

?

2.?使用b加行号设置断点 如

b 3

3.?使用r命令运行程序

r

4.?设置print的pretty参数为on

set print pretty on

5.?查看栈寄存器信息

info reg rsp

6.?打印变量信息

print s1

7.?查看内存信息x/长度xb 内存地址如下

X/5xb 0x5555557a0110

?

实锤证明胖指针的确胖在了栈上

说到这里其实相应的准备知识也就都有了。这里我们只需要进入到gdb去具体看一下情况就可以了。

1.确定栈空间位置 我们先按照上述gdb调试方法执行到第5步 确定rsp也就是栈顶的位置如下 image.png

从构建栈的语句上看从栈顶向下0x98的范围内都是栈空间

000000000000aa00 _ZN6hello14main17h5a48792de9598b5bE :

aa00: ??????48 81 ec 98 00 00 00 ???sub ???$0x98,%rsp

?

2.?确定胖指针中的ptr(指针)指向位置 接下来我们来看一下 变量s1的信息 得到了胖指针结构体中 指针指向的物理地址 并且这里还是要解释一下 初看cap属性和len属性的确不属于一个层级 这也是我一开始产生错误认识的原因。

3.?image.png

?

3.?确定ptr与字符串值 的实际对应关系 使用我们在上一节gdb调试的第7步命令 可以看到胖指针中ptr指向位置的内容分别对应”hello”的ascii码 因此可以确定指针指向堆上实际存放字符串的地址 这点没问题。image.png

4.?查看s1对象中ptr、len及cap属性的具体内存布局 我们刚刚已经确定了自栈顶 0x7fffffffe270 向下0x98范围内都属于栈空间 那么我们再通过x命令查看整个栈空间 具体注释如下 image.png

?

可以看到通过gdb实际查看我们基本可以确定字符串s1的三个属性ptr,cap和len都是存在栈上的 而具体字符串的值则在堆上。之前cap存在堆上的想法自然也就是错的了。

极致挑错 胖指针内存到底如何内存布局

还有一点没有确定 上图中的例子 cap和len都是5 因此无法知道具体排列顺序关系 那么我们再来看下面的代码

fn main() {

???????let mut s1 String::new();

???????s1.push_str( hello

????????println!( The length now is {}. ,s1.len());

???????println!( The cap now is {}. ,s1.capacity());

????????println!( Then addr now is {:p}. ,s1.as_ptr());

?

?????}

上述代码运行结果如下

The length now is 5.

The cap now is 8.

Then addr now is 0x55afa3255110.

可以看到使用?s1.push_str的方法可能会使len与cap值不相同 那么这种情况下也就便于我们进行具体跟踪了。

image.png

实际观察内存布局时我们看到 cap属性与ptr是相领的 而非之前广为流传的图示中所说len与ptr相领 虽然这个错误不大 但是有关内存布局还是不能马虎 因此修改后正确的胖指针示意如下

?image.png

?

以上就是我对于Rust胖指针的学习理解过程 欢迎各位读者一如既往的提出意见 咱们共同进步


本文转自网络,原文链接:https://developer.aliyun.com/article/785027

版权声明:本文转载自网络,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。本站转载出于传播更多优秀技术知识之目的,如有侵权请联系QQ/微信:153890879删除

相关文章
  • 从内存布局上看,Rust的胖指针到底是胖

    从内存布局上看,Rust的胖指针到底是胖

  • 聊一聊全球加速的原理和配置

    聊一聊全球加速的原理和配置

  • 架构之:微服务和单体服务之争

    架构之:微服务和单体服务之争

  • 为什么禁止使用BigDecimal的equals方法

    为什么禁止使用BigDecimal的equals方法

腾讯云代理商
海外云服务器