如果你對C/C++基本數(shù)據(jù)類型的內(nèi)存模型沒概念的話,可以先查看該傳送門《開篇1:C/C++ 內(nèi)存中的數(shù)據(jù)表示》,反正我覺得
- 先掌握了基本數(shù)據(jù)的內(nèi)存模型
- 再理解計(jì)算機(jī)的尋址模型和程序的內(nèi)存布局
這樣能夠?qū)/C++內(nèi)存管理方面的認(rèn)知起到以小見大的效果。
可尋址模型和內(nèi)存布局
我們知道,內(nèi)存是由操作系統(tǒng)統(tǒng)一管理的,內(nèi)存里面一個(gè)字節(jié)就等于8個(gè)二進(jìn)制位,然后操作系統(tǒng)就為內(nèi)存空間進(jìn)行編號,這就是我們所說尋址模型。那么我們經(jīng)常說的32位指的是什么呢?其實(shí)操作系統(tǒng)給內(nèi)存編號最大只編號到2的32次方(即只能編42,9496,7296個(gè)地址編號),而每個(gè)編號邏輯上喜歡使用十六進(jìn)制來表示,并且用于表示內(nèi)存的具體位置。形式通俗點(diǎn)說就是4GB的內(nèi)存大小。為什么32位x86的操作系統(tǒng)無法使用大于4GB的內(nèi)存條的額外空間?原因就在這里。
另外,像其他任何程序一樣,BIOS和OS都需要內(nèi)存(廢話),此處為了表示計(jì)算機(jī)內(nèi)存模型的完整性。我們都把所有內(nèi)存相關(guān)的內(nèi)存區(qū)域都一一列出了,對于程序員感興趣的主要內(nèi)存區(qū)域是代碼段,數(shù)據(jù)段和字面量池和bss,堆棧和堆。
從程序的組織的方式來查看程序的內(nèi)存布局
- 代碼段:程序的所有指令會存放在這個(gè)區(qū)域,這是已經(jīng)編譯后的機(jī)器碼。
- 字面量池是程序初始化時(shí)的一些字符串字面量,在程序中用于顯示文字
- 全局?jǐn)?shù)據(jù)段:程序初始化時(shí)的常量和全局/靜態(tài)的變量。C/C++ 用global/static聲明的變量都存放在這個(gè)區(qū)域,對所有函數(shù)公開可見。
- 堆:這里保存的數(shù)據(jù)只是為了臨時(shí)存儲一些值而創(chuàng)建的,而我們可能在程序運(yùn)行過程中可能會回收此內(nèi)存。因?yàn)槲覀冊诔绦驁?zhí)行期間不需要很長時(shí)間,所以使用C中的new或malloc這類內(nèi)存分配程序來為我們所需的特定數(shù)據(jù)類型提供新的空間,并且隨著我們要求越來越多的動態(tài)數(shù)據(jù)空間而該區(qū)域不斷擴(kuò)大,并且在內(nèi)存中逐漸增長到更高的地址。
- 棧:當(dāng)我們執(zhí)行這些過程調(diào)用時(shí),堆的基本特性是LIFO,存儲著該程序“上下文”,它將從內(nèi)存的高層地址開始,然后向另一個(gè)方向向下擴(kuò)展。上下文其實(shí)就是程序中各個(gè)函數(shù)之間調(diào)用的先后順序。
這種典型的內(nèi)存布局有一個(gè)比較有趣的地方,實(shí)際上棧向低層地址不斷增長,動態(tài)數(shù)據(jù)會向高層地址增長,只要你的程序足夠糟糕,例如用無止境的遞歸和不斷搶占堆可用的空間,這兩個(gè)貨始終會碰面,這將是一件非常糟糕的事情。這是一種嚴(yán)重的錯(cuò)誤,這種情況操作系統(tǒng)說它內(nèi)存不足時(shí),例如Windows臭名招囑的藍(lán)屏提示...!!
IA32平臺的程序棧
讓我們看一下ia-32體系結(jié)構(gòu)的調(diào)用堆棧,我們將堆棧的底部放在內(nèi)存的頂部,并將堆棧的頂部放在內(nèi)存的底部。 這只是我們使用的約定,因?yàn)槲揖拖矚g使用倒置的形式,也有人喜歡將棧頂定于為上方且棧底定義在下方,但如果沒有顯式標(biāo)注高地址和低地址,那就“誤人子弟”了。爭論這些毫無意義。
唯一要記住的是棧是朝著內(nèi)存低地址方向增長,iA32棧中有一個(gè)特殊的寄存器,稱為esp。該寄存器始終指向堆棧的頂部元素,即放置在堆棧上的最后一個(gè)元素。
push操作
好的,所以我們要看的第一個(gè)堆棧操作是push指令,這里我們展示的是
pushl 寄存器名稱 或 push 某個(gè)類型的指針
表示一個(gè)32位的值,并為其指定了要入棧的源寄存器或內(nèi)存位置,基本上它是它會從該源獲取值,無論它是寄存器還是內(nèi)存位置都會推入到棧頂。它還會將棧指針遞減4,為什么要減4,因?yàn)閜ushl剛好是4個(gè)字節(jié),并且是超低地址方向增長的,因此棧指針遞減,
如下圖所示,現(xiàn)在棧指針指向內(nèi)存中已將該值添加或復(fù)制到內(nèi)存中的新位置。
pop操作
popl 寄存器名稱 或 popl 某個(gè)類型的指針
popl指令將數(shù)據(jù)從堆棧中移出。在這種情況下,我們還為它提供了一個(gè)dst參數(shù),以獲取從棧中彈出的值,然后將該值放入某個(gè)內(nèi)存地址指向的位置或CPU中的寄存器。
我們從堆棧頂刪除某個(gè)值,并再次為該32位字的esp向上調(diào)整堆棧指針。
我們pop操作的時(shí)候是真的“刪除”原先的值嗎?
這個(gè)值并未刪除,它仍然存在于內(nèi)存中,只是我們不再引用它了。因?yàn)槲覀円呀?jīng)調(diào)整了堆棧指針,使其指向棧中的下一個(gè)值。 但是原先這些位的數(shù)據(jù)仍然駐留在原先的內(nèi)存位置,只是程序不再解釋解析這些位中的二進(jìn)制碼。 已經(jīng)在某種意義上有效地刪除了它們,因?yàn)槲覀兛梢曰厥赵摽臻g并將新數(shù)據(jù)壓入棧并覆蓋這些位。因此需要保留被彈出的數(shù)據(jù),只需將它們拷貝到指定的位置即可。
讓我們看看如何使用堆棧來跟蹤過程調(diào)用,以及如何記住過程調(diào)用結(jié)束時(shí)需要返回的返回地址以及需要從該過程獲取的返回值。
程序的過程調(diào)用概述
下面是一個(gè)過程調(diào)用的概述,而且是一個(gè)很簡陋的例子,有經(jīng)驗(yàn)的程序員可能已經(jīng)看出很多漏洞了-_-b!!我說明在先這個(gè)例子僅僅起到拋轉(zhuǎn)引玉的作用并且在最后通過該例子提出幾個(gè)問題,而這些問題會在以后的文章里會詳細(xì)得到解答,那么我們將從調(diào)用者和被調(diào)用者這兩個(gè)程序開始。
-
調(diào)用者將設(shè)置一些參數(shù),并在執(zhí)行call指令后,該指令將控制流跳轉(zhuǎn)到被調(diào)用被調(diào)用者的函數(shù),之前在被調(diào)用者初始化的參數(shù)也一同傳遞給被調(diào)用者。
-
此時(shí)控制權(quán)在被調(diào)用者的函數(shù)中,被調(diào)用者會創(chuàng)建一些局部變量,在執(zhí)行一些運(yùn)算的操作,并且運(yùn)算的結(jié)果設(shè)為一個(gè)返回值,該返回值是被調(diào)用函數(shù)返回給調(diào)用者函數(shù)的。
-
在被調(diào)用者函數(shù)執(zhí)行return之前,要清理創(chuàng)建的局部變量,并回收空間,最后執(zhí)行return指令以告訴CPU要把控制權(quán)交還給調(diào)用者函數(shù)。
并轉(zhuǎn)到調(diào)用者函數(shù)原先執(zhí)行點(diǎn)之后的下一條指令,由于調(diào)用者的下一條指令后沒有其他指令了就開始清理空間,該空間最初用于設(shè)置參數(shù)所占用的空間都會被回收。此處,我們應(yīng)該要清楚原先被調(diào)用者函數(shù)所占用的空間已被回收,并且調(diào)用者函數(shù)再執(zhí)行后也會銷毀自己,這就是調(diào)用過程設(shè)置。
以上的例子很簡單,基本稍微有一些代碼基礎(chǔ)的讀者不用看都知道,但我的目的是導(dǎo)出如下幾個(gè)問題點(diǎn)。
- 被調(diào)用者函數(shù)必須知道從哪里獲取參數(shù)?
- 被調(diào)用者必須知道從哪里獲取返回地址?
- 調(diào)用者必須知道從哪里獲取返回值?
由于調(diào)用和被調(diào)用方在同一個(gè)CPU上運(yùn)行,因此它們當(dāng)然使用該CPU中的同一寄存器,因此要有一種機(jī)制確保兩者之間不會同時(shí)爭奪CPU的資源。這種機(jī)制就是:
- 如果調(diào)用者要使用某個(gè)寄存器,而剛好被調(diào)用者也需要使用該寄存器,調(diào)用者在交出該寄存器的控制權(quán)之前,它會先保存該寄存器(通常是一個(gè)地址),當(dāng)這一步完成后,就將寄存器讓給被調(diào)用者。
- 同理,被調(diào)用者也可能會保存當(dāng)前使用的寄存器的地址后,才讓出寄存器的控制權(quán)。
這里也引出一個(gè)問題:究竟要賦予所有職責(zé)給調(diào)用者還是被調(diào)用者?或所有職責(zé)由兩者共同承擔(dān)?這就跟調(diào)用機(jī)制扯不上關(guān)系了,而是考驗(yàn)程序員如何合理設(shè)計(jì)函數(shù)的功能,明確函數(shù)之間的分工主次的問題了!!