記憶體中程式剖析

2022-06-05 07:03:06 字數 4097 閱讀 2444

記憶體管理一直是作業系統的核心問題,它對於程式設計和系統管理都是異常重要。接下來會有一系列博文從實際角度給大家介紹記憶體管理的一系列內容,儘管這一概念比較寬泛,但是博文中列舉的示例都是來自於linux或windows這些32位的x86系統。作為這系列的第一篇**,首先簡單描述一下程式如何在記憶體中布局。

每個多工系統中的程序都執行在它自己的記憶體「沙盒」裡,這個「沙盒」就是虛擬記憶體空間,這在32位模式下往往指4gb記憶體位址塊。這些虛擬位址通過頁表對映到物理記憶體,這些頁表是由作業系統核心來維護並且被處理器(cpu)來查詢。每個程序都有它自己的頁表,一旦虛擬位址啟動,這些葉表就必須適用於機器上的所有軟體程式,包括系統核心本身。因此,虛擬位址的一部分必須儲存在核心中:

這不是意味著核心使用頁表來匹配物理記憶體,只是核心需要使用那部分虛擬空間用來對映任意的物理空間。核心空間的頁面在頁表上被標記為特權級(環2或更低),因此,如果乙個使用者模式下的程式試圖去訪問一頁特權級記憶體頁,常常會導致乙個頁異常(page fault----程式設計中常見報錯總結)。在linux下,核心空間在所有使用者程序中的物理記憶體空間對映都是一致的。任何時候,記憶體**和資料,可以被中斷處理程式和系統呼叫所定址和使用。相比之下,對映為使用者模式那部分的位址空間,隨著乙個任務切換的發生,也會對映到不同的實體地址空間:

上圖中,藍色區域表示對映到實體地址空間的虛擬位址空間,而白色則是還未對映的虛擬位址。位址空間中不同的段對於不同的記憶體段----堆、棧等等。記住,這些段僅僅表示一段記憶體位址範圍,和實際的硬體架構中的段不同。總之,下圖就是linux下乙個程序的標準段布局:

當計算和程式上一切執行順利,那麼在機器上每個程序的不同段的布局基本一致,都如上圖所示,這樣有利於避免很多安全注入問題。一次注入攻擊常常需要訪問絕對記憶體位置:例如堆疊上的位址、函式庫的位址等等。遠端攻擊者往往是盲目選擇記憶體位置,主要是希望每個程序的位址空間都是一致的,如果是這樣的話,那麼個人的隱私就受到威脅;所以,如今執行緒位址空間的隨機對映大行其道。linux下,系統通過讓基位址隨機加上乙個偏移位址,從而得到棧位址、堆位址以及其他的記憶體對映段位址。然而,如今的32位位址空間顯得有點緊湊,這樣就導致隨機的空間很小,進而可能影響其安全性

在程序中最重要的段----堆疊段,很多程式語言下,它是用來存放區域性變數和函式引數的。呼叫乙個函式或者方法,就會在棧上壓入乙個新的棧幀,並且隨著函式返回,所對應的棧幀也就被釋放掉。這種簡單的設計,主要的思想**可能是資料遵從「先進先出」的規則,這也意味著不需要複雜的資料結構來追蹤棧內容,乙個簡單的棧頂指標就可以實現內容查詢----push和pop操作是非常迅速且穩定。同時,將那些被多次重複使用的棧內容存放到「cpu快取中(cache)」,可以帶來更快的訪問速度。

如果在記憶體空間中,放入過多的資料內容,會導致棧空間的耗盡。linux下,這樣所引起的頁異常,可以通過expand_stack()來解決,這就會導致呼叫acct_stack_growth()來檢查是否可以增長堆疊空間。如果棧空間尺度小於rlimit_stack(一般是8mb),那麼棧空間增長可以沒有問題的執行下去。但是,如果棧空間已經達到上限,那麼我們進行上述操作,最終會收到乙個段異常(segmentation fault)----也就是「棧溢位」。如果棧增長操作順序執行,娜美操作完成後不**縮棧大小。

動態堆疊增長是訪問未對映記憶體區域的唯一正確方式。任何其他訪問未對映記憶體區域,都會觸發乙個頁異常,進而導致段異常。某些對映記憶體區域是唯讀記憶體區域,因此,試圖寫這些區域同樣會導致異常。

在棧下面,就是記憶體對映段區域。這裡,記憶體直接將檔案內容對映到記憶體。任何應用可以通過呼叫linux下的mmap()/mapviewoffile()來實現。記憶體對映對於檔案i/o操作來說是非常方便且高效的,所以它經常被用來載入動態庫。也可以建立乙個匿名記憶體對映,不對應任何檔案,而專門用做程式資料。linux下,如果程式通過malloc()需求一片大的記憶體塊,c庫將會建立一片匿名對映空間,這裡的「大」是指大於mmap_threshold位元組,該字段的預設值是128kb,可以通過mallopt()動態修改。

接下來,就是堆的介紹。堆----常用來提供執行時記憶體分配,這裡點和棧比較類似;但是,堆也可以分配存放棧範圍之外的資料變數,這一點區別於棧。大部分語言都提供堆管理程式。因此,滿足記憶體請求是語言執行時和核心的共同事務。在c語言中,呼叫堆分配空間的介面是malloc(),以及在具有垃圾**機制的c#語言中,對應的介面的new關鍵字。

如果在堆上有足夠的記憶體空間滿足記憶體呼叫,那麼僅僅通過語言執行時就可以滿足相關操作,而不需要呼叫系統核心操作。否則的話,就需要通過呼叫brk()系統呼叫來分配更多的空間來滿足需求。在我們實際程式複雜的記憶體分配的模式下,堆的管理是很複雜的,需要精妙的演算法來平衡速度和記憶體使用效率,因而,用於分配堆空間的時間消耗可能差異很大;實時系統主要通過special-purpose allocators來處理這類問題。堆在記憶體中示意圖如下所示:

最後,我們來分析最下面的一系列記憶體段----bss、資料段和程式**段。在c語言中,bss、資料段都用來儲存靜態(static)變數。不同之處是,bss中儲存的內容是未初始化的靜態變數,這些變數的值是通過在程式**中進行設定的。bss記憶體區域是匿名的:它不會對映到任何檔案。如果,你輸入static int cntactiveusers;語句,那麼這個變數就存放在bss段。

資料段,存放原始碼中已經被初始化的靜態變數,所以,這段記憶體區域不是匿名的,它被對映到檔案的二進位制映象的某個部分,並且包含這些靜態變數在原始碼中被初始化的值。所以,當你輸入static int cntworkbees = 10;語句,則變數的內容存放於資料段且值為10。儘管資料段對映成乙個檔案,它仍是私有記憶體對映,這也意味著對記憶體資料的更新不會同步到所對映的檔案中。否則的話,對於靜態變數的賦值,會導致磁碟上檔案內容的變化。

下圖中,資料段的示例比較複雜,因為使用了指標。下例中,指標gonzo內容----也就是乙個4位元組記憶體位址----存放在資料段。但是,它指向的實際字串並不存放在資料段,而是在**段,也就是乙個唯讀段,也就是存放所有**和所有字串的區域。**段同樣對映記憶體中的二進位制檔案,但是要寫**段會導致乙個段異常。這有助於阻止很多指標錯誤,下圖是這些段的示意圖:

你可以通過閱讀linux原始碼檔案中的/proc/pid_of_process/maps來進一步了解記憶體區域,請記住,乙個段可能包含很多不同的記憶體區域。例如,每個記憶體對映檔案通常在mmap段中都有自己的區域,而動態庫具有類似於bss和資料的額外區域。你可以借助linux下的工具nm和objdump

OC中程式的記憶體分布 類載入

類載入 一句話形容就是在類第一次使用時載入到 段,直到程式結束時才釋放。oc中的記憶體分布 從下往上依次是 段 存放 資料段 已初始化全域性變數和靜態變數 bss段 未初始化的全域性變數和靜態變數 堆區 new malloc 等分配的空間 棧區 區域性變數 假設在main函式中宣告 假設person...

Linux中程式編譯

arm linux gnueabihf gcc eg arm linux gnueabihf gcc g c led.s o led.o g選項是產生除錯資訊,gdb除錯能夠利用這些資訊進行除錯 c選項是編譯原始檔,但是不產生連線 o是指定編譯產生的檔案名字,即指定編譯後產生led.o檔案arm l...

作業系統中程式的記憶體結構說明

乙個程式在記憶體上由bss段 data段 text段三個組成的。在沒有調入記憶體前,可執行程式分為 段 資料區和未初始化資料區三部分。bss段 bss segment 通常是指用來存放程式中未初始化的全域性變數的一塊記憶體區域。bss是英文block started by symbol的簡稱。bss...