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

简介: 最近阿里云最新的云原生技术Serverless发展也是如火如荼,我在前辈巨师的带领下,也进入到学习Rust的大军中,与其它语言一样,Rust最初的爬坡难点也在于字符串方面的处理。本文针对胖指针进行了相关详细介绍。

最近阿里云最新的云原生技术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胖指针的学习理解过程,欢迎各位读者一如既往的提出意见,咱们共同进步!

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
1月前
|
存储 Go iOS开发
掌握Go语言:探索Go语言指针,解锁高效内存操作与动态数据结构的奥秘(19)
掌握Go语言:探索Go语言指针,解锁高效内存操作与动态数据结构的奥秘(19)
|
2月前
|
网络协议 安全 Unix
深入剖析进程间通信:Unix 套接字、共享内存与IP协议栈的性能比较
深入剖析进程间通信:Unix 套接字、共享内存与IP协议栈的性能比较
77 2
|
2月前
|
存储 C语言 C++
动态内存分配与指向它的指针变量
动态内存分配与指向它的指针变量
21 1
|
1天前
|
存储 Arthas 监控
JVM工作原理与实战(三十):堆内存状况的对比分析
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了堆内存状况的对比分析、产生内存溢出的原因等内容。
|
1天前
|
存储 监控 Java
JVM工作原理与实战(十七):运行时数据区-栈内存溢出
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了栈内存溢出、设置虚拟机栈的大小等内容。
|
6天前
3.默认值不一样【重点】 局部变量:没有默认值,如果要想使用,必须手动进行赋值 成员变量:如果没有赋值,会有默认值,规则和数组一样 4.内存的位置不一样(了解) 局部变量:位于栈内存 成员变量:位于堆内存 5生命周期不一样(了解)
3.默认值不一样【重点】 局部变量:没有默认值,如果要想使用,必须手动进行赋值 成员变量:如果没有赋值,会有默认值,规则和数组一样 4.内存的位置不一样(了解) 局部变量:位于栈内存 成员变量:位于堆内存 5生命周期不一样(了解)
15 0
|
8天前
|
Java Go 区块链
【Go语言专栏】Go语言中的指针与内存管理
【4月更文挑战第30天】Go语言,由Google开发,是一种静态强类型、编译型、并发型语言,具有垃圾回收功能,常用于云计算、微服务、区块链等领域。本文聚焦Go中的指针和内存管理。指针表示变量内存地址,可用于直接访问和修改变量,如示例代码所示。指针运算有限制,仅支持相同类型变量和数组元素访问。内存管理由Go运行时的垃圾回收机制处理,自动回收无引用对象,简化管理但引入性能开销。可通过`runtime.GC()`手动触发垃圾回收。
|
9天前
|
存储 机器学习/深度学习 Java
【Java探索之旅】数组使用 初探JVM内存布局
【Java探索之旅】数组使用 初探JVM内存布局
23 0
|
10天前
|
程序员 编译器 C++
内存分区模型(代码区、全局区、栈区、堆区)
内存分区模型(代码区、全局区、栈区、堆区)
|
14天前
|
运维 Kubernetes 算法
Java堆内存又溢出了!教你一招必杀技
Java堆内存又溢出了!教你一招必杀技