從內存佈局上看,Rust 的胖指針到底胖在棧上還是堆上?
作者 | 馬超 責編 | 張紅月
出品 | CSDN 博客
最近我在前輩巨師的帶領下,也進入到學習 Rust 的大軍中,與其它語言一樣,Rust 最初的爬坡難點也在於字符串方面的處理。雖然說 Rust 與 C 一樣也有指針概念,但是在字符串方面引用了胖指針,關於胖指針的內存佈局,被引用最爲廣泛的一幅說明圖如下:
咱們先來說明一下這個胖指針的大致概念,字符串 s1 有三個元素分別是 ptr、len、capacity,其中 ptr 是指向堆上實際字符串 value 的指針,len 代表字符串的長度,capacity 代表字符串的容量。這些值全部都存在棧上,而實際字符串的值則存在堆上。爲了讓便於說明,我轉化了一下上面的圖,大家可以看一下這幅圖。
對於這幅圖的理解真可謂是一波三折,我一開始以爲這圖畫的不對,後來發現應該是對的,最後深入研究還是發現了一個小問題,最終正確的示意圖如下:
本文就和大家分享一下具體分析的過程。
胖指針理解錯誤的起因
我們知道 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 應該放在堆上,當時理解的圖如下:
當然現在看這個結論的得出犯了想當然的經驗主義錯誤,沒有進行深入實證。
堆和棧到底是幹嘛的
爲了更好的向大家展示對於胖指針內存而已的驗證方案,這裏先簡要介紹一下基本的彙編及 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 模式後,用 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
使用 b 加行號設置斷點,如
b 3
使用 r 命令運行程序
r
設置 print 的 pretty 參數爲 on
set print pretty on
查看棧寄存器信息
info reg rsp
打印變量信息
print s1
查看內存信息 x / 長度 xb 內存地址如下:
X/5xb 0x5555557a0110
實錘證明胖指針的確胖在了棧上
說到這裏其實相應的準備知識也就都有了。這裏我們只需要進入到 gdb 去具體看一下情況就可以了。
- 確定棧空間位置:我們先按照上述 gdb 調試方法執行到第 5 步,確定 rsp 也就是棧頂的位置如下:
從構建棧的語句上看從棧頂向下 0x98 的範圍內都是棧空間:
000000000000aa00 <_ZN6hello14main17h5a48792de9598b5bE>:
aa00: 48 81 ec 98 00 00 00 sub $0x98,%rsp
確定胖指針中的 ptr(指針) 指向位置:接下來我們來看一下,變量 s1 的信息,得到了胖指針結構體中,指針指向的物理地址,並且這裏還是要解釋一下,初看 cap 屬性和 len 屬性的確不屬於一個層級,這也是我一開始產生錯誤認識的原因。
確定 ptr 與字符串值 的實際對應關係:使用我們在上一節 gdb 調試的第 7 步命令,可以看到胖指針中 ptr 指向位置的內容分別對應”hello” 的 ascii 碼,因此可以確定指針指向堆上實際存放字符串的地址,這點沒問題。
查看 s1 對象中 ptr、len 及 cap 屬性的具體內存佈局:我們剛剛已經確定了自棧頂(0x7fffffffe270)向下 0x98 範圍內都屬於棧空間,那麼我們再通過 x 命令查看整個棧空間,具體註釋如下:
可以看到通過 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 值不相同,那麼這種情況下也就便於我們進行具體跟蹤了。
實際觀察內存佈局時我們看到,cap 屬性與 ptr 是相領的,而非之前廣爲流傳的圖示中所說 len 與 ptr 相領,雖然這個錯誤不大,但是有關內存佈局還是不能馬虎,因此修改後正確的胖指針示意如下:
以上就是我對於 Rust 胖指針的學習理解過程,歡迎各位讀者一如既往的提出意見,咱們共同進步!
馬超,CSDN 博客專家,阿里雲 MVP、華爲雲 MVP,華爲 2020 年技術社區開發者之星。
原文鏈接:https://blog.csdn.net/BEYONDMA/article/details/118460702
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/a_WB2PUQsImTlgk4TI1UEw